From c6e780628d6d0395d3cfc307e6a925005b3a470e Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Wed, 22 Apr 2026 21:42:57 +0200 Subject: [PATCH 1/3] feat: Port RFC 2047 support methods from the MIME package to break circular dependency --- src/Rfc2047.php | 241 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 src/Rfc2047.php diff --git a/src/Rfc2047.php b/src/Rfc2047.php new file mode 100644 index 00000000..da1a0717 --- /dev/null +++ b/src/Rfc2047.php @@ -0,0 +1,241 @@ + + * @author Michael Slusarz + * @category Horde + * @copyright 1999-2026 Horde LLC + * @license http://www.horde.org/licenses/bsd New BSD License + * @package Mail + */ + +namespace Horde\Mail; + +use Horde\Util\HordeString; + +/** + * RFC 2047 MIME header encoding and decoding. + * + * Extracted from Horde_Mime to break the Mail ↔ Mime circular dependency. + */ +final class Rfc2047 +{ + private const EOL = "\r\n"; + + /** + * Use windows-1252 charset when decoding ISO-8859-1 data? + * HTML 5 requires this behavior, so it is the default. + */ + public static bool $decodeWindows1252 = true; + + public static function is8bit(string $string): bool + { + for ($i = 0, $len = strlen($string); $i < $len; ++$i) { + if (ord($string[$i]) > 127) { + return true; + } + } + + return false; + } + + public static function encode(string $text, string $charset = 'UTF-8'): string + { + $charset = HordeString::lower($charset); + $text = HordeString::convertCharset($text, 'UTF-8', $charset); + + $encoded = $is_encoded = false; + $lwsp = $word = null; + $out = ''; + + /* 0 = word unencoded + * 1 = word encoded + * 2 = spaces */ + $parts = []; + + for ($i = 0, $len = strlen($text); $i < $len; ++$i) { + switch ($text[$i]) { + case "\t": + case "\r": + case "\n": + if (!is_null($word)) { + $parts[] = [intval($encoded), $word, $i - $word]; + $word = null; + } elseif (!is_null($lwsp)) { + $parts[] = [2, $lwsp, $i - $lwsp]; + $lwsp = null; + } + + $parts[] = [0, $i, 1]; + break; + + case ' ': + if (!is_null($word)) { + $parts[] = [intval($encoded), $word, $i - $word]; + $word = null; + } + if (is_null($lwsp)) { + $lwsp = $i; + } + break; + + default: + if (is_null($word)) { + $encoded = false; + $word = $i; + if (!is_null($lwsp)) { + $parts[] = [2, $lwsp, $i - $lwsp]; + $lwsp = null; + } + + if (($text[$i] === '=') + && (($i + 1) < $len) + && ($text[$i + 1] === '?')) { + ++$i; + $encoded = $is_encoded = true; + } + } + + if (!$encoded) { + $c = ord($text[$i]); + if ($encoded = (($c & 0x80) || ($c < 32))) { + $is_encoded = true; + } + } + break; + } + } + + if (!$is_encoded) { + return $text; + } + + if (is_null($lwsp)) { + $parts[] = [intval($encoded), $word, $len]; + } else { + $parts[] = [2, $lwsp, $len]; + } + + for ($i = 0, $cnt = count($parts); $i < $cnt; ++$i) { + $val = $parts[$i]; + + switch ($val[0]) { + case 0: + case 2: + $out .= substr($text, $val[1], $val[2]); + break; + + case 1: + $j = $i; + for ($k = $i + 1; $k < $cnt; ++$k) { + switch ($parts[$k][0]) { + case 0: + break 2; + + case 1: + $i = $k; + break; + } + } + + $encode = ''; + for (; $j <= $i; ++$j) { + $encode .= substr($text, $parts[$j][1], $parts[$j][2]); + } + + $delim = '=?' . $charset . '?b?'; + $e_parts = explode( + self::EOL, + rtrim( + chunk_split( + base64_encode($encode), + intval((75 - strlen($delim) + 2) / 4) * 4 + ) + ) + ); + + $tmp = []; + foreach ($e_parts as $val) { + $tmp[] = $delim . $val . '?='; + } + + $out .= implode(' ', $tmp); + break; + } + } + + return rtrim($out); + } + + public static function decode(string $string): string + { + $old_pos = 0; + $out = ''; + + while (($pos = strpos($string, '=?', $old_pos)) !== false) { + $pre = substr($string, $old_pos, $pos - $old_pos); + if (!$old_pos + || (strspn($pre, " \t\n\r") != strlen($pre))) { + $out .= $pre; + } + + if (($d1 = strpos($string, '?', $pos + 2)) === false) { + break; + } + + $orig_charset = substr($string, $pos + 2, $d1 - $pos - 2); + if (self::$decodeWindows1252 + && (HordeString::lower($orig_charset) == 'iso-8859-1')) { + $orig_charset = 'windows-1252'; + } + + if (($d2 = strpos($string, '?', $d1 + 1)) === false) { + break; + } + + $encoding = substr($string, $d1 + 1, $d2 - $d1 - 1); + + if (($end = strpos($string, '?=', $d2 + 1)) === false) { + break; + } + + $encoded_text = substr($string, $d2 + 1, $end - $d2 - 1); + + switch ($encoding) { + case 'Q': + case 'q': + $out .= HordeString::convertCharset( + quoted_printable_decode( + str_replace('_', ' ', $encoded_text) + ), + $orig_charset, + 'UTF-8' + ); + break; + + case 'B': + case 'b': + $out .= HordeString::convertCharset( + base64_decode($encoded_text), + $orig_charset, + 'UTF-8' + ); + break; + + default: + break; + } + + $old_pos = $end + 2; + } + + return $out . substr($string, $old_pos); + } +} From 17027329334e2003199e376c89e12cd020849c05 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Wed, 22 Apr 2026 21:45:00 +0200 Subject: [PATCH 2/3] style: php-cs-fixer --- lib/Horde/Mail/Exception.php | 7 +- lib/Horde/Mail/Mbox/Parse.php | 46 ++-- lib/Horde/Mail/Rfc822.php | 271 ++++++++++++----------- lib/Horde/Mail/Rfc822/Address.php | 16 +- lib/Horde/Mail/Rfc822/Group.php | 22 +- lib/Horde/Mail/Rfc822/GroupList.php | 4 +- lib/Horde/Mail/Rfc822/Identification.php | 4 +- lib/Horde/Mail/Rfc822/List.php | 54 ++--- lib/Horde/Mail/Rfc822/Object.php | 10 +- lib/Horde/Mail/Translation.php | 3 +- lib/Horde/Mail/Transport.php | 42 ++-- lib/Horde/Mail/Transport/Lmtphorde.php | 3 +- lib/Horde/Mail/Transport/Mail.php | 11 +- lib/Horde/Mail/Transport/Mock.php | 29 +-- lib/Horde/Mail/Transport/Null.php | 7 +- lib/Horde/Mail/Transport/Sendmail.php | 110 ++++----- lib/Horde/Mail/Transport/Smtphorde.php | 19 +- 17 files changed, 334 insertions(+), 324 deletions(-) diff --git a/lib/Horde/Mail/Exception.php b/lib/Horde/Mail/Exception.php index 2b540dbb..d00313c5 100644 --- a/lib/Horde/Mail/Exception.php +++ b/lib/Horde/Mail/Exception.php @@ -1,6 +1,7 @@ _parsed[] = array( + $this->_parsed[] = [ 'date' => $date, - 'start' => ftell($this->_data) - ); + 'start' => ftell($this->_data), + ]; } /* Strip all empty lines before first data. */ @@ -122,10 +122,10 @@ public function __construct($data, $limit = null) /* This was a single message, not a MBOX file. */ if (empty($this->_parsed)) { - $this->_parsed[] = array( + $this->_parsed[] = [ 'date' => false, - 'start' => $start - ); + 'start' => $start, + ]; } } @@ -133,7 +133,7 @@ public function __construct($data, $limit = null) /** */ - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function offsetExists($offset) { return isset($this->_parsed[$offset]); @@ -141,7 +141,7 @@ public function offsetExists($offset) /** */ - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function offsetGet($offset) { if (!isset($this->_parsed[$offset])) { @@ -169,11 +169,11 @@ public function offsetGet($offset) ); } - $out = array( + $out = [ 'data' => $fd, 'date' => ($p['date'] === false) ? null : $p['date'], - 'size' => intval(ftell($fd)) - ); + 'size' => intval(ftell($fd)), + ]; rewind($fd); return $out; @@ -181,7 +181,7 @@ public function offsetGet($offset) /** */ - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function offsetSet($offset, $value) { // NOOP @@ -189,7 +189,7 @@ public function offsetSet($offset, $value) /** */ - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function offsetUnset($offset) { // NOOP @@ -202,7 +202,7 @@ public function offsetUnset($offset) * * @return integer The number of messages. */ - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function count() { return count($this->_parsed); @@ -223,7 +223,7 @@ public function __toString() /* Iterator methods. */ - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function current() { $key = $this->key(); @@ -233,13 +233,13 @@ public function current() : $this[$key]; } - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function key() { return key($this->_parsed); } - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function next() { if ($this->valid()) { @@ -247,13 +247,13 @@ public function next() } } - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function rewind() { reset($this->_parsed); } - #[\ReturnTypeWillChange] + #[ReturnTypeWillChange] public function valid() { return !is_null($this->key()); diff --git a/lib/Horde/Mail/Rfc822.php b/lib/Horde/Mail/Rfc822.php index a6ea2f42..e94295af 100644 --- a/lib/Horde/Mail/Rfc822.php +++ b/lib/Horde/Mail/Rfc822.php @@ -1,7 +1,8 @@ @[\\]\177"; + public const ENCODE_FILTER = "\0\1\2\3\4\5\6\7\10\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\"(),:;<>@[\\]\177"; /** * The address string to parse. @@ -93,7 +94,7 @@ class Horde_Mail_Rfc822 * * @var string */ - protected $_comments = array(); + protected $_comments = []; /** * List object to return in parseAddressList(). @@ -107,7 +108,7 @@ class Horde_Mail_Rfc822 * * @var array */ - protected $_params = array(); + protected $_params = []; /** * Data pointer. @@ -142,7 +143,7 @@ class Horde_Mail_Rfc822 * * @throws Horde_Mail_Exception */ - public function parseAddressList($address, array $params = array()) + public function parseAddressList($address, array $params = []) { if ($address instanceof Horde_Mail_Rfc822_List) { return $address; @@ -152,20 +153,20 @@ public function parseAddressList($address, array $params = array()) $params['limit'] = -1; } - $this->_params = array_merge(array( + $this->_params = array_merge([ 'default_domain' => null, - 'validate' => false - ), $params); + 'validate' => false, + ], $params); $this->_listob = empty($this->_params['group']) ? new Horde_Mail_Rfc822_List() : new Horde_Mail_Rfc822_GroupList(); if (!is_array($address)) { - $address = array($address); + $address = [$address]; } - $tmp = array(); + $tmp = []; foreach ($address as $val) { if ($val instanceof Horde_Mail_Rfc822_Object) { $this->_listob->add($val); @@ -188,34 +189,34 @@ public function parseAddressList($address, array $params = array()) return $ret; } - /** - * Quotes and escapes the given string if necessary using rules contained - * in RFC 2822 [3.2.5]. - * - * @param string $str The string to be quoted and escaped. - * @param string $type Either 'address', 'comment' (@since 2.6.0), or - * 'personal'. - * - * @return string The correctly quoted and escaped string. - */ + /** + * Quotes and escapes the given string if necessary using rules contained + * in RFC 2822 [3.2.5]. + * + * @param string $str The string to be quoted and escaped. + * @param string $type Either 'address', 'comment' (@since 2.6.0), or + * 'personal'. + * + * @return string The correctly quoted and escaped string. + */ public function encode($str, $type = 'address') { switch ($type) { - case 'comment': - // RFC 5322 [3.2.2]: Filter out non-printable US-ASCII and ( ) \ - $filter = "\0\1\2\3\4\5\6\7\10\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\50\51\134\177"; - break; + case 'comment': + // RFC 5322 [3.2.2]: Filter out non-printable US-ASCII and ( ) \ + $filter = "\0\1\2\3\4\5\6\7\10\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\50\51\134\177"; + break; - case 'personal': - // RFC 2822 [3.4]: Period not allowed in display name - $filter = self::ENCODE_FILTER . '.'; - break; + case 'personal': + // RFC 2822 [3.4]: Period not allowed in display name + $filter = self::ENCODE_FILTER . '.'; + break; - case 'address': - default: - // RFC 2822 [3.4.1]: (HTAB, SPACE) not allowed in address - $filter = self::ENCODE_FILTER . "\11\40"; - break; + case 'address': + default: + // RFC 2822 [3.4.1]: (HTAB, SPACE) not allowed in address + $filter = self::ENCODE_FILTER . "\11\40"; + break; } // Strip double quotes if they are around the string already. @@ -261,26 +262,26 @@ protected function _parseAddressList() try { $this->_parseAddress(); } catch (Horde_Mail_Exception $e) { - if ($this->_params['validate']) { - throw $e; - } - ++$this->_ptr; + if ($this->_params['validate']) { + throw $e; + } + ++$this->_ptr; } switch ($this->_curr()) { - case ',': - $this->_rfc822SkipLwsp(true); - break; - - case false: - // No-op - break; - - default: - if ($this->_params['validate']) { - throw new Horde_Mail_Exception('Error when parsing address list.'); - } - break; + case ',': + $this->_rfc822SkipLwsp(true); + break; + + case false: + // No-op + break; + + default: + if ($this->_params['validate']) { + throw new Horde_Mail_Exception('Error when parsing address list.'); + } + break; } } } @@ -334,16 +335,16 @@ protected function _parseGroup() $addresses->add($this->_parseMailbox()); switch ($this->_curr()) { - case ',': - $this->_rfc822SkipLwsp(true); - break; + case ',': + $this->_rfc822SkipLwsp(true); + break; - case ';': - // No-op - break; + case ';': + // No-op + break; - default: - break 2; + default: + break 2; } } @@ -357,11 +358,11 @@ protected function _parseGroup() */ protected function _parseMailbox() { - $this->_comments = array(); + $this->_comments = []; $start = $this->_ptr; if (!($ob = $this->_parseNameAddr())) { - $this->_comments = array(); + $this->_comments = []; $this->_ptr = $start; $ob = $this->_parseAddrSpec(); } @@ -495,7 +496,7 @@ protected function _parseAngleAddr() */ protected function _parseDomainList() { - $route = array(); + $route = []; while ($this->_curr() !== false) { $this->_rfc822ParseDomain($str); @@ -537,9 +538,9 @@ protected function _rfc822ParsePhrase(&$phrase) } $curr = $this->_curr(); - if (($curr != '"') && - ($curr != '.') && - !$this->_rfc822IsAtext($curr)) { + if (($curr != '"') + && ($curr != '.') + && !$this->_rfc822IsAtext($curr)) { break; } @@ -562,22 +563,22 @@ protected function _rfc822ParseQuotedString(&$str) while (($chr = $this->_curr(true)) !== false) { switch ($chr) { - case '"': - $this->_rfc822SkipLwsp(); - return; - - case "\n": - /* Folding whitespace, remove the (CR)LF. */ - if (substr($str, -1) == "\r") { - $str = substr($str, 0, -1); - } - continue 2; + case '"': + $this->_rfc822SkipLwsp(); + return; - case '\\': - if (($chr = $this->_curr(true)) === false) { - break 2; - } - break; + case "\n": + /* Folding whitespace, remove the (CR)LF. */ + if (substr($str, -1) == "\r") { + $str = substr($str, 0, -1); + } + continue 2; + + case '\\': + if (($chr = $this->_curr(true)) === false) { + break 2; + } + break; } $str .= $chr; @@ -643,9 +644,9 @@ protected function _rfc822ParseAtomOrDot(&$str) { while ($this->_ptr < $this->_datalen) { $chr = $this->_data[$this->_ptr]; - if (($chr != '.') && + if (($chr != '.') /* TODO: Optimize by duplicating rfc822IsAtext code here */ - !$this->_rfc822IsAtext($chr, ',<:')) { + && !$this->_rfc822IsAtext($chr, ',<:')) { $this->_rfc822SkipLwsp(); if (!$this->_params['validate'] && $str !== null) { $str = trim($str); @@ -702,15 +703,15 @@ protected function _rfc822ParseDomainLiteral(&$str) while (($chr = $this->_curr(true)) !== false) { switch ($chr) { - case '\\': - if (($chr = $this->_curr(true)) === false) { - break 2; - } - break; - - case ']': - $this->_rfc822SkipLwsp(); - return; + case '\\': + if (($chr = $this->_curr(true)) === false) { + break 2; + } + break; + + case ']': + $this->_rfc822SkipLwsp(); + return; } $str .= $chr; @@ -732,19 +733,19 @@ protected function _rfc822SkipLwsp($advance = false) while (($chr = $this->_curr()) !== false) { switch ($chr) { - case ' ': - case "\n": - case "\r": - case "\t": - ++$this->_ptr; - continue 2; - - case '(': - $this->_rfc822SkipComment(); - break; - - default: - return; + case ' ': + case "\n": + case "\r": + case "\t": + ++$this->_ptr; + continue 2; + + case '(': + $this->_rfc822SkipComment(); + break; + + default: + return; } } } @@ -763,22 +764,22 @@ protected function _rfc822SkipComment() while (($chr = $this->_curr(true)) !== false) { switch ($chr) { - case '(': - ++$level; - continue 2; - - case ')': - if (--$level == 0) { - $this->_comments[] = $comment; - return; - } - break; - - case '\\': - if (($chr = $this->_curr(true)) === false) { - break 2; - } - break; + case '(': + ++$level; + continue 2; + + case ')': + if (--$level == 0) { + $this->_comments[] = $comment; + return; + } + break; + + case '\\': + if (($chr = $this->_curr(true)) === false) { + break 2; + } + break; } $comment .= $chr; @@ -818,20 +819,20 @@ protected function _rfc822IsAtext($chr, $validate = null) /* "(),:;<>@[\] [DEL] */ switch ($ord) { - case 34: - case 40: - case 41: - case 44: - case 58: - case 59: - case 60: - case 62: - case 64: - case 91: - case 92: - case 93: - case 127: - return false; + case 34: + case 40: + case 41: + case 44: + case 58: + case 59: + case 60: + case 62: + case 64: + case 91: + case 92: + case 93: + case 127: + return false; } return true; @@ -889,7 +890,7 @@ public function isValidInetAddress($data, $strict = false) : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; return preg_match($regex, trim($data), $matches) - ? array($matches[1], $matches[2]) + ? [$matches[1], $matches[2]] : false; } diff --git a/lib/Horde/Mail/Rfc822/Address.php b/lib/Horde/Mail/Rfc822/Address.php index dcda008c..cc83ee2f 100644 --- a/lib/Horde/Mail/Rfc822/Address.php +++ b/lib/Horde/Mail/Rfc822/Address.php @@ -1,7 +1,7 @@ _personal = !empty($value) - ? Horde_Mime::decode($value) + ? Rfc2047::decode($value) : null; break; } @@ -129,7 +131,7 @@ public function __get($name) case 'eai': return is_null($this->mailbox) ? false - : Horde_Mime::is8bit($this->mailbox); + : Rfc2047::is8bit($this->mailbox); case 'encoded': return $this->writeAddress(true); @@ -151,7 +153,7 @@ public function __get($name) : $this->_personal; case 'personal_encoded': - return Horde_Mime::encode($this->personal); + return Rfc2047::encode($this->personal); case 'valid': return !empty($this->mailbox); @@ -164,7 +166,7 @@ protected function _writeAddress($opts) { $rfc822 = new Horde_Mail_Rfc822(); - $address = $rfc822->encode((string)$this->mailbox, 'address'); + $address = $rfc822->encode((string) $this->mailbox, 'address'); $host = empty($opts['idn']) ? $this->host : $this->host_idn; if (!empty($host)) { $address .= '@' . $host; @@ -172,7 +174,7 @@ protected function _writeAddress($opts) $personal = $this->personal; if (!empty($personal)) { if (!empty($opts['encode'])) { - $personal = Horde_Mime::encode($this->personal, $opts['encode']); + $personal = Rfc2047::encode($this->personal, $opts['encode']); } if (empty($opts['noquote'])) { $personal = $rfc822->encode($personal, 'personal'); diff --git a/lib/Horde/Mail/Rfc822/Group.php b/lib/Horde/Mail/Rfc822/Group.php index 3ca7fc1f..361a9518 100644 --- a/lib/Horde/Mail/Rfc822/Group.php +++ b/lib/Horde/Mail/Rfc822/Group.php @@ -1,7 +1,7 @@ addresses = clone $addresses; } else { $rfc822 = new Horde_Mail_Rfc822(); - $this->addresses = $rfc822->parseAddressList($addresses, array( - 'group' => true - )); + $this->addresses = $rfc822->parseAddressList($addresses, [ + 'group' => true, + ]); } } @@ -76,7 +78,7 @@ public function __set($name, $value) { switch ($name) { case 'groupname': - $this->_groupname = Horde_Mime::decode($value); + $this->_groupname = Rfc2047::decode($value); break; } } @@ -91,10 +93,10 @@ public function __get($name) return $this->_groupname; case 'groupname_encoded': - return Horde_Mime::encode($this->_groupname); + return Rfc2047::encode($this->_groupname); case 'valid': - return (bool)strlen($this->_groupname); + return (bool) strlen($this->_groupname); } } @@ -105,7 +107,7 @@ protected function _writeAddress($opts) $addr = $this->addresses->writeAddress($opts); $groupname = $this->groupname; if (!empty($opts['encode'])) { - $groupname = Horde_Mime::encode($groupname, $opts['encode']); + $groupname = Rfc2047::encode($groupname, $opts['encode']); } if (empty($opts['noquote'])) { $rfc822 = new Horde_Mail_Rfc822(); @@ -118,8 +120,8 @@ protected function _writeAddress($opts) } } - return ltrim($groupname) . ':' . - (strlen($addr) ? (' ' . $addr) : '') . ';'; + return ltrim($groupname) . ':' + . (strlen($addr) ? (' ' . $addr) : '') . ';'; } /** diff --git a/lib/Horde/Mail/Rfc822/GroupList.php b/lib/Horde/Mail/Rfc822/GroupList.php index ceab2570..1b9641af 100644 --- a/lib/Horde/Mail/Rfc822/GroupList.php +++ b/lib/Horde/Mail/Rfc822/GroupList.php @@ -1,7 +1,7 @@ setIteratorFilter($mask, empty($old['filter']) ? null : $old['filter']); - $out = array(); + $out = []; foreach ($this as $val) { switch ($name) { case 'addresses': @@ -138,7 +138,7 @@ public function remove($obs) $this->setIteratorFilter(self::HIDE_GROUPS | self::BASE_ELEMENTS); foreach ($this->_normalize($obs) as $val) { - $remove = array(); + $remove = []; foreach ($this as $key => $val2) { if ($val2->match($val)) { @@ -160,7 +160,7 @@ public function remove($obs) */ public function unique() { - $exist = $remove = array(); + $exist = $remove = []; $old = $this->_filter; $this->setIteratorFilter(self::HIDE_GROUPS | self::BASE_ELEMENTS); @@ -216,7 +216,7 @@ public function groupCount(): int */ public function setIteratorFilter($mask = 0, $filter = null) { - $this->_filter = array(); + $this->_filter = []; if ($mask) { $this->_filter['mask'] = $mask; @@ -232,7 +232,7 @@ public function setIteratorFilter($mask = 0, $filter = null) */ protected function _writeAddress($opts) { - $out = array(); + $out = []; foreach ($this->_data as $val) { $out[] = $val->writeAddress($opts); @@ -301,15 +301,15 @@ public function first() */ protected function _normalize($obs) { - $add = array(); + $add = []; if ($obs instanceof Horde_Mime_Headers_Addresses) { $obs = $obs->getAddressList(); } - if (!($obs instanceof Horde_Mail_Rfc822_List) && - !is_array($obs)) { - $obs = array($obs); + if (!($obs instanceof Horde_Mail_Rfc822_List) + && !is_array($obs)) { + $obs = [$obs]; } foreach ($obs as $val) { @@ -441,15 +441,15 @@ public function next(): void public function rewind(): void { - $this->_ptr = array( + $this->_ptr = [ 'idx' => 0, 'key' => 0, - 'subidx' => null - ); + 'subidx' => null, + ]; - if ($this->valid() && - !empty($this->_filter) && - $this->_iteratorFilter($this->current())) { + if ($this->valid() + && !empty($this->_filter) + && $this->_iteratorFilter($this->current())) { $this->next(); $this->_ptr['key'] = 0; } @@ -462,8 +462,8 @@ public function valid(): bool public function seek(int $offset): void { - if (!$this->valid() || - ($offset < $this->_ptr['key'])) { + if (!$this->valid() + || ($offset < $this->_ptr['key'])) { $this->rewind(); } @@ -482,19 +482,19 @@ public function seek(int $offset): void protected function _iteratorFilter($ob) { if (!empty($this->_filter['mask'])) { - if (($this->_filter['mask'] & self::HIDE_GROUPS) && - ($ob instanceof Horde_Mail_Rfc822_Group)) { + if (($this->_filter['mask'] & self::HIDE_GROUPS) + && ($ob instanceof Horde_Mail_Rfc822_Group)) { return true; } - if (($this->_filter['mask'] & self::BASE_ELEMENTS) && - !is_null($this->_ptr['subidx'])) { + if (($this->_filter['mask'] & self::BASE_ELEMENTS) + && !is_null($this->_ptr['subidx'])) { return true; } } - if (!empty($this->_filter['filter']) && - ($ob instanceof Horde_Mail_Rfc822_Address)) { + if (!empty($this->_filter['filter']) + && ($ob instanceof Horde_Mail_Rfc822_Address)) { foreach ($this->_filter['filter'] as $val) { if ($ob->match($val)) { return true; diff --git a/lib/Horde/Mail/Rfc822/Object.php b/lib/Horde/Mail/Rfc822/Object.php index 7597388f..788af7c9 100644 --- a/lib/Horde/Mail/Rfc822/Object.php +++ b/lib/Horde/Mail/Rfc822/Object.php @@ -1,7 +1,7 @@ 'UTF-8', - 'idn' => true - ); + 'idn' => true, + ]; } elseif (!empty($opts['encode']) && ($opts['encode'] === true)) { $opts['encode'] = 'UTF-8'; } diff --git a/lib/Horde/Mail/Translation.php b/lib/Horde/Mail/Translation.php index eb5da6c8..8b694bbd 100644 --- a/lib/Horde/Mail/Translation.php +++ b/lib/Horde/Mail/Translation.php @@ -1,6 +1,7 @@ $value) { if (strcasecmp($key, 'From') === 0) { $parser = new Horde_Mail_Rfc822(); - $addresses = $parser->parseAddressList($value, array( - 'validate' => $this->eai ? 'eai' : true - )); + $addresses = $parser->parseAddressList($value, [ + 'validate' => $this->eai ? 'eai' : true, + ]); $from = $addresses[0]->bare_address; // Reject envelope From: addresses with spaces. @@ -145,9 +145,9 @@ public function prepareHeaders(array $headers) $lines[] = $key . ': ' . $this->_normalizeEOL($value); } elseif (!$raw && (strcasecmp($key, 'Received') === 0)) { - $received = array(); + $received = []; if (!is_array($value)) { - $value = array($value); + $value = [$value]; } foreach ($value as $line) { @@ -168,7 +168,7 @@ public function prepareHeaders(array $headers) } } - return array($from, $raw ? $raw : implode($this->sep, $lines)); + return [$from, $raw ? $raw : implode($this->sep, $lines)]; } /** @@ -189,9 +189,9 @@ public function parseRecipients($recipients) // for smtp recipients, etc. All relevant personal information // should already be in the headers. $rfc822 = new Horde_Mail_Rfc822(); - return $rfc822->parseAddressList($recipients, array( - 'validate' => $this->eai ? 'eai' : true - ))->bare_addresses_idn; + return $rfc822->parseAddressList($recipients, [ + 'validate' => $this->eai ? 'eai' : true, + ])->bare_addresses_idn; } /** @@ -207,7 +207,7 @@ public function parseRecipients($recipients) */ protected function _sanitizeHeaders($headers) { - foreach (array_diff(array_keys($headers), array('_raw')) as $key) { + foreach (array_diff(array_keys($headers), ['_raw']) as $key) { $headers[$key] = preg_replace('=((||0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', '', $headers[$key]); } @@ -223,11 +223,11 @@ protected function _sanitizeHeaders($headers) */ protected function _normalizeEOL($data) { - return strtr($data, array( + return strtr($data, [ "\r\n" => $this->sep, "\r" => $this->sep, - "\n" => $this->sep - )); + "\n" => $this->sep, + ]); } /** diff --git a/lib/Horde/Mail/Transport/Lmtphorde.php b/lib/Horde/Mail/Transport/Lmtphorde.php index 471fae2c..675c5743 100644 --- a/lib/Horde/Mail/Transport/Lmtphorde.php +++ b/lib/Horde/Mail/Transport/Lmtphorde.php @@ -1,6 +1,7 @@ _params = array_merge($this->_params, $params); } @@ -77,7 +78,7 @@ public function send($recipients, array $headers, $body) } // Flatten the headers out. - list(, $text_headers) = $this->prepareHeaders($headers); + [, $text_headers] = $this->prepareHeaders($headers); // mail() requires a string for $body. If resource, need to convert // to a string. @@ -85,7 +86,7 @@ public function send($recipients, array $headers, $body) $body_str = ''; stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol'); - stream_filter_append($body, 'horde_eol', STREAM_FILTER_READ, array('eol' => $this->sep)); + stream_filter_append($body, 'horde_eol', STREAM_FILTER_READ, ['eol' => $this->sep]); rewind($body); while (!feof($body)) { @@ -102,7 +103,7 @@ public function send($recipients, array $headers, $body) if (empty($this->_params) || ini_get('safe_mode')) { $result = mail($recipients, $subject, $body, $text_headers); } else { - $result = mail($recipients, $subject, $body, $text_headers, isset($this->_params['args']) ? $this->_params['args'] : ''); + $result = mail($recipients, $subject, $body, $text_headers, $this->_params['args'] ?? ''); } // If the mail() function returned failure, we need to create an diff --git a/lib/Horde/Mail/Transport/Mock.php b/lib/Horde/Mail/Transport/Mock.php index e791e534..577c30df 100644 --- a/lib/Horde/Mail/Transport/Mock.php +++ b/lib/Horde/Mail/Transport/Mock.php @@ -1,6 +1,7 @@ _preSendCallback = $params['preSendCallback']; } - if (isset($params['postSendCallback']) && - is_callable($params['postSendCallback'])) { + if (isset($params['postSendCallback']) + && is_callable($params['postSendCallback'])) { $this->_postSendCallback = $params['postSendCallback']; } } @@ -91,15 +92,15 @@ public function __construct(array $params = array()) public function send($recipients, array $headers, $body) { if ($this->_preSendCallback) { - call_user_func_array($this->_preSendCallback, array($this, $recipients, $headers, $body)); + call_user_func_array($this->_preSendCallback, [$this, $recipients, $headers, $body]); } $headers = $this->_sanitizeHeaders($headers); - list($from, $text_headers) = $this->prepareHeaders($headers); + [$from, $text_headers] = $this->prepareHeaders($headers); if (is_resource($body)) { stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol'); - stream_filter_append($body, 'horde_eol', STREAM_FILTER_READ, array('eol' => $this->sep)); + stream_filter_append($body, 'horde_eol', STREAM_FILTER_READ, ['eol' => $this->sep]); rewind($body); $body_txt = stream_get_contents($body); @@ -110,16 +111,16 @@ public function send($recipients, array $headers, $body) $from = $this->_getFrom($from, $headers); $recipients = $this->parseRecipients($recipients); - $this->sentMessages[] = array( + $this->sentMessages[] = [ 'body' => $body_txt, 'from' => $from, 'headers' => $headers, 'header_text' => $text_headers, - 'recipients' => $recipients - ); + 'recipients' => $recipients, + ]; if ($this->_postSendCallback) { - call_user_func_array($this->_postSendCallback, array($this, $recipients, $headers, $body_txt)); + call_user_func_array($this->_postSendCallback, [$this, $recipients, $headers, $body_txt]); } } diff --git a/lib/Horde/Mail/Transport/Null.php b/lib/Horde/Mail/Transport/Null.php index 8c5264f9..3d9ade5f 100644 --- a/lib/Horde/Mail/Transport/Null.php +++ b/lib/Horde/Mail/Transport/Null.php @@ -1,6 +1,7 @@ _sendmailArgs = $params['sendmail_args']; @@ -91,7 +92,7 @@ public function send($recipients, array $headers, $body) $recipients = implode(' ', array_map('escapeshellarg', $this->parseRecipients($recipients))); $headers = $this->_sanitizeHeaders($headers); - list($from, $text_headers) = $this->prepareHeaders($headers); + [$from, $text_headers] = $this->prepareHeaders($headers); $from = $this->_getFrom($from, $headers); $mail = @popen($this->_sendmailPath . (empty($this->_sendmailArgs) ? '' : ' ' . $this->_sendmailArgs) . ' -f ' . escapeshellarg($from) . ' -- ' . $recipients, 'w'); @@ -105,7 +106,7 @@ public function send($recipients, array $headers, $body) if (is_resource($body)) { stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol'); - stream_filter_append($body, 'horde_eol', STREAM_FILTER_READ, array('eol' => $this->sep)); + stream_filter_append($body, 'horde_eol', STREAM_FILTER_READ, ['eol' => $this->sep]); rewind($body); while (!feof($body)) { @@ -121,72 +122,73 @@ public function send($recipients, array $headers, $body) } switch ($result) { - case 64: // EX_USAGE - $msg = 'command line usage error'; - break; + case 64: // EX_USAGE + $msg = 'command line usage error'; + break; - case 65: // EX_DATAERR - $msg = 'data format error'; - break; + case 65: // EX_DATAERR + $msg = 'data format error'; + break; - case 66: // EX_NOINPUT - $msg = 'cannot open input'; - break; + case 66: // EX_NOINPUT + $msg = 'cannot open input'; + break; - case 67: // EX_NOUSER - $msg = 'addressee unknown'; - break; + case 67: // EX_NOUSER + $msg = 'addressee unknown'; + break; - case 68: // EX_NOHOST - $msg = 'host name unknown'; - break; + case 68: // EX_NOHOST + $msg = 'host name unknown'; + break; - case 69: // EX_UNAVAILABLE - $msg = 'service unavailable'; - break; + case 69: // EX_UNAVAILABLE + $msg = 'service unavailable'; + break; - case 70: // EX_SOFTWARE - $msg = 'internal software error'; - break; + case 70: // EX_SOFTWARE + $msg = 'internal software error'; + break; - case 71: // EX_OSERR - $msg = 'system error'; - break; + case 71: // EX_OSERR + $msg = 'system error'; + break; - case 72: // EX_OSFILE - $msg = 'critical system file missing'; - break; + case 72: // EX_OSFILE + $msg = 'critical system file missing'; + break; - case 73: // EX_CANTCREAT - $msg = 'cannot create output file'; - break; + case 73: // EX_CANTCREAT + $msg = 'cannot create output file'; + break; - case 74: // EX_IOERR - $msg = 'input/output error'; + case 74: // EX_IOERR + $msg = 'input/output error'; - case 75: // EX_TEMPFAIL - $msg = 'temporary failure'; - break; + // no break + case 75: // EX_TEMPFAIL + $msg = 'temporary failure'; + break; - case 76: // EX_PROTOCOL - $msg = 'remote error in protocol'; - break; + case 76: // EX_PROTOCOL + $msg = 'remote error in protocol'; + break; - case 77: // EX_NOPERM - $msg = 'permission denied'; - break; + case 77: // EX_NOPERM + $msg = 'permission denied'; + break; - case 78: // EX_CONFIG - $msg = 'configuration error'; - break; + case 78: // EX_CONFIG + $msg = 'configuration error'; + break; - case 79: // EX_NOTFOUND - $msg = 'entry not found'; - break; + case 79: // EX_NOTFOUND + $msg = 'entry not found'; + break; - default: - $msg = 'unknown error'; - break; + default: + $msg = 'unknown error'; + break; } throw new Horde_Mail_Exception('sendmail: ' . $msg . ' (' . $result . ')', $result); diff --git a/lib/Horde/Mail/Transport/Smtphorde.php b/lib/Horde/Mail/Transport/Smtphorde.php index ee9037ca..ebf89e70 100644 --- a/lib/Horde/Mail/Transport/Smtphorde.php +++ b/lib/Horde/Mail/Transport/Smtphorde.php @@ -1,6 +1,7 @@ _params = $params; @@ -114,9 +115,9 @@ public function __construct(array $params = array()) public function __get($name) { switch ($name) { - case 'eai': - $this->getSMTPObject(); - return $this->_smtp->data_intl; + case 'eai': + $this->getSMTPObject(); + return $this->_smtp->data_intl; } return parent::__get($name); @@ -130,14 +131,14 @@ public function send($recipients, array $headers, $body) $this->getSMTPObject(); $headers = $this->_sanitizeHeaders($headers); - list($from, $textHeaders) = $this->prepareHeaders($headers); + [$from, $textHeaders] = $this->prepareHeaders($headers); $from = $this->_getFrom($from, $headers); - $combine = Horde_Stream_Wrapper_Combine::getStream(array( + $combine = Horde_Stream_Wrapper_Combine::getStream([ rtrim($textHeaders, $this->sep), $this->sep . $this->sep, - $body - )); + $body, + ]); try { $this->_smtp->send($from, $recipients, $combine); From 8b55582fc19b5c6645a832048fd8605b02bce611 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Wed, 22 Apr 2026 21:48:34 +0200 Subject: [PATCH 3/3] test: Cover Rfc2047 class --- test/unit/Rfc2047Test.php | 131 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 test/unit/Rfc2047Test.php diff --git a/test/unit/Rfc2047Test.php b/test/unit/Rfc2047Test.php new file mode 100644 index 00000000..2a7074d7 --- /dev/null +++ b/test/unit/Rfc2047Test.php @@ -0,0 +1,131 @@ +assertSame($expected, Rfc2047::is8bit($data)); + } + + public static function is8bitProvider(): array + { + return [ + ['', false], + ['A', false], + ['a', false], + ['1', false], + ['!', false], + ["\0", false], + ["\10", false], + ["\127", false], + ["\x80", true], + ['ä', true], + ['A©B', true], + [' ® ', true], + [base64_decode('UnVubmVyc5IgQWxlcnQh='), true], + ]; + } + + #[DataProvider('encodeProvider')] + public function testEncode(string $data, string $charset, string $expected): void + { + $this->assertEquals($expected, Rfc2047::encode($data, $charset)); + } + + public static function encodeProvider(): array + { + return [ + ['a b', 'utf-8', 'a b'], + ['a bcäde f', 'utf-8', 'a =?utf-8?b?YmPDpGRl?= f'], + ['a ää ä b', 'utf-8', 'a =?utf-8?b?w6TDpCDDpA==?= b'], + ['ä a ä', 'utf-8', '=?utf-8?b?w6Q=?= a =?utf-8?b?w6Q=?='], + ['ää a ä', 'utf-8', '=?utf-8?b?w6TDpA==?= a =?utf-8?b?w6Q=?='], + ['=', 'utf-8', '='], + ['?', 'utf-8', '?'], + ['a=?', 'utf-8', 'a=?'], + ['=?', 'utf-8', '=?utf-8?b?PT8=?='], + ['=?x', 'utf-8', '=?utf-8?b?PT94?='], + ["a\n=?", 'utf-8', "a\n=?utf-8?b?PT8=?="], + ["a\t=?", 'utf-8', "a\t=?utf-8?b?PT8=?="], + ['a =?', 'utf-8', 'a =?utf-8?b?PT8=?='], + ["foo\001bar", 'utf-8', '=?utf-8?b?Zm9vAWJhcg==?='], + ["\x01\x02\x03\x04\x05\x06\x07\x08", 'utf-8', '=?utf-8?b?AQIDBAUGBwg=?='], + ["\x00", 'UTF-16LE', '=?utf-16le?b?AAA=?='], + ]; + } + + #[DataProvider('decodeProvider')] + public function testDecode(string $data, string $expected): void + { + $this->assertEquals($expected, Rfc2047::decode($data)); + } + + public static function decodeProvider(): array + { + return [ + [ + '=?utf-8?Q?_Fran=C3=A7ois_Xavier=2E_XXXXXX_?= ', + ' François Xavier. XXXXXX ', + ], + [ + " \t=?utf-8?q?=c3=a4?= =?utf-8?q?=c3=a4?= b \t\r\n ", + " \tää b \t\r\n ", + ], + [ + 'a =?utf-8?q?=c3=a4?= b', + 'a ä b', + ], + [ + "a =?utf-8?q?=c3=a4?=\t\t\r\n =?utf-8?q?=c3=a4?= b", + 'a ää b', + ], + [ + 'a =?utf-8?q?=c3=a4?= x =?utf-8?q?=c3=a4?= b', + 'a ä x ä b', + ], + [ + 'a =?utf-8?b?w6TDpCDDpA==?= b', + 'a ää ä b', + ], + [ + '=?utf-8?b?w6Qgw6Q=?=', + 'ä ä', + ], + [ + '=? required=?', + '=? required=?', + ], + ]; + } + + public function testDecodeWindows1252Fallback(): void + { + $original = Rfc2047::$decodeWindows1252; + + try { + Rfc2047::$decodeWindows1252 = true; + $this->assertTrue(Rfc2047::$decodeWindows1252); + + Rfc2047::$decodeWindows1252 = false; + $this->assertFalse(Rfc2047::$decodeWindows1252); + } finally { + Rfc2047::$decodeWindows1252 = $original; + } + } + + public function testDecodeWindows1252DefaultIsTrue(): void + { + $this->assertTrue(Rfc2047::$decodeWindows1252); + } +}