This commit is contained in:
Marcus Hill 2023-01-31 14:11:02 +00:00
parent 87fdad9105
commit e6cc458b1c
17 changed files with 2581 additions and 0 deletions

View File

@ -0,0 +1,276 @@
<?php
namespace PhpMimeMailParser;
use function var_dump;
/**
* Attachment of php-mime-mail-parser
*
* Fully Tested Mailparse Extension Wrapper for PHP 5.4+
*
*/
class Attachment
{
/**
* @var string $filename Filename
*/
protected $filename;
/**
* @var string $contentType Mime Type
*/
protected $contentType;
/**
* @var string $content File Content
*/
protected $content;
/**
* @var string $contentDisposition Content-Disposition (attachment or inline)
*/
protected $contentDisposition;
/**
* @var string $contentId Content-ID
*/
protected $contentId;
/**
* @var array $headers An Array of the attachment headers
*/
protected $headers;
/**
* @var resource $stream
*/
protected $stream;
/**
* @var string $mimePartStr
*/
protected $mimePartStr;
/**
* @var integer $maxDuplicateNumber
*/
public $maxDuplicateNumber = 100;
/**
* Attachment constructor.
*
* @param string $filename
* @param string $contentType
* @param resource $stream
* @param string $contentDisposition
* @param string $contentId
* @param array $headers
* @param string $mimePartStr
*/
public function __construct(
$filename,
$contentType,
$stream,
$contentDisposition = 'attachment',
$contentId = '',
$headers = [],
$mimePartStr = ''
) {
$this->filename = $filename;
$this->contentType = $contentType;
$this->stream = $stream;
$this->content = null;
$this->contentDisposition = $contentDisposition;
$this->contentId = $contentId;
$this->headers = $headers;
$this->mimePartStr = $mimePartStr;
}
/**
* retrieve the attachment filename
*
* @return string
*/
public function getFilename()
{
return $this->filename;
}
/**
* Retrieve the Attachment Content-Type
*
* @return string
*/
public function getContentType()
{
return $this->contentType;
}
/**
* Retrieve the Attachment Content-Disposition
*
* @return string
*/
public function getContentDisposition()
{
return $this->contentDisposition;
}
/**
* Retrieve the Attachment Content-ID
*
* @return string
*/
public function getContentID()
{
return $this->contentId;
}
/**
* Retrieve the Attachment Headers
*
* @return array
*/
public function getHeaders()
{
return $this->headers;
}
/**
* Get a handle to the stream
*
* @return resource
*/
public function getStream()
{
return $this->stream;
}
/**
* Rename a file if it already exists at its destination.
* Renaming is done by adding a duplicate number to the file name. E.g. existingFileName_1.ext.
* After a max duplicate number, renaming the file will switch over to generating a random suffix.
*
* @param string $fileName Complete path to the file.
* @return string The suffixed file name.
*/
protected function suffixFileName(string $fileName): string
{
$pathInfo = pathinfo($fileName);
$dirname = $pathInfo['dirname'].DIRECTORY_SEPARATOR;
$filename = $pathInfo['filename'];
$extension = empty($pathInfo['extension']) ? '' : '.'.$pathInfo['extension'];
$i = 0;
do {
$i++;
if ($i > $this->maxDuplicateNumber) {
$duplicateExtension = uniqid();
} else {
$duplicateExtension = $i;
}
$resultName = $dirname.$filename."_$duplicateExtension".$extension;
} while (file_exists($resultName));
return $resultName;
}
/**
* Read the contents a few bytes at a time until completed
* Once read to completion, it always returns false
*
* @param int $bytes (default: 2082)
*
* @return string|bool
*/
public function read($bytes = 2082)
{
return feof($this->stream) ? false : fread($this->stream, $bytes);
}
/**
* Retrieve the file content in one go
* Once you retrieve the content you cannot use MimeMailParser_attachment::read()
*
* @return string
*/
public function getContent()
{
if ($this->content === null) {
fseek($this->stream, 0);
while (($buf = $this->read()) !== false) {
$this->content .= $buf;
}
}
return $this->content;
}
/**
* Get mime part string for this attachment
*
* @return string
*/
public function getMimePartStr()
{
return $this->mimePartStr;
}
/**
* Save the attachment individually
*
* @param string $attach_dir
* @param string $filenameStrategy
*
* @return string
*/
public function save(
$attach_dir,
$filenameStrategy = Parser::ATTACHMENT_DUPLICATE_SUFFIX
) {
$attach_dir = rtrim($attach_dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
if (!is_dir($attach_dir)) {
mkdir($attach_dir);
}
// Determine filename
switch ($filenameStrategy) {
case Parser::ATTACHMENT_RANDOM_FILENAME:
$fileInfo = pathinfo($this->getFilename());
$extension = empty($fileInfo['extension']) ? '' : '.'.$fileInfo['extension'];
$attachment_path = $attach_dir.uniqid().$extension;
break;
case Parser::ATTACHMENT_DUPLICATE_THROW:
case Parser::ATTACHMENT_DUPLICATE_SUFFIX:
$attachment_path = $attach_dir.$this->getFilename();
break;
default:
throw new Exception('Invalid filename strategy argument provided.');
}
// Handle duplicate filename
if (file_exists($attachment_path)) {
switch ($filenameStrategy) {
case Parser::ATTACHMENT_DUPLICATE_THROW:
throw new Exception('Could not create file for attachment: duplicate filename.');
case Parser::ATTACHMENT_DUPLICATE_SUFFIX:
$attachment_path = $this->suffixFileName($attachment_path);
break;
}
}
/** @var resource $fp */
if ($fp = fopen($attachment_path, 'w')) {
while ($bytes = $this->read()) {
fwrite($fp, $bytes);
}
fclose($fp);
return realpath($attachment_path);
} else {
throw new Exception('Could not write attachments. Your directory may be unwritable by PHP.');
}
}
}

View File

@ -0,0 +1,370 @@
<?php namespace PhpMimeMailParser;
use PhpMimeMailParser\Contracts\CharsetManager;
class Charset implements CharsetManager
{
/**
* Charset Aliases
*/
private $charsetAlias = [
'ascii' => 'us-ascii',
'us-ascii' => 'us-ascii',
'ansi_x3.4-1968' => 'us-ascii',
'646' => 'us-ascii',
'iso-8859-1' => 'iso-8859-1',
'iso-8859-2' => 'iso-8859-2',
'iso-8859-3' => 'iso-8859-3',
'iso-8859-4' => 'iso-8859-4',
'iso-8859-5' => 'iso-8859-5',
'iso-8859-6' => 'iso-8859-6',
'iso-8859-6-i' => 'iso-8859-6-i',
'iso-8859-6-e' => 'iso-8859-6-e',
'iso-8859-7' => 'iso-8859-7',
'iso-8859-8' => 'iso-8859-8',
'iso-8859-8-i' => 'iso-8859-8',
'iso-8859-8-e' => 'iso-8859-8-e',
'iso-8859-9' => 'iso-8859-9',
'iso-8859-10' => 'iso-8859-10',
'iso-8859-11' => 'iso-8859-11',
'iso-8859-13' => 'iso-8859-13',
'iso-8859-14' => 'iso-8859-14',
'iso-8859-15' => 'iso-8859-15',
'iso-8859-16' => 'iso-8859-16',
'iso-ir-111' => 'iso-ir-111',
'iso-2022-cn' => 'iso-2022-cn',
'iso-2022-cn-ext' => 'iso-2022-cn',
'iso-2022-kr' => 'iso-2022-kr',
'iso-2022-jp' => 'iso-2022-jp',
'utf-16be' => 'utf-16be',
'utf-16le' => 'utf-16le',
'utf-16' => 'utf-16',
'windows-1250' => 'windows-1250',
'windows-1251' => 'windows-1251',
'windows-1252' => 'windows-1252',
'windows-1253' => 'windows-1253',
'windows-1254' => 'windows-1254',
'windows-1255' => 'windows-1255',
'windows-1256' => 'windows-1256',
'windows-1257' => 'windows-1257',
'windows-1258' => 'windows-1258',
'ibm866' => 'ibm866',
'ibm850' => 'ibm850',
'ibm852' => 'ibm852',
'ibm855' => 'ibm855',
'ibm857' => 'ibm857',
'ibm862' => 'ibm862',
'ibm864' => 'ibm864',
'utf-8' => 'utf-8',
'utf-7' => 'utf-7',
'shift_jis' => 'shift_jis',
'big5' => 'big5',
'euc-jp' => 'euc-jp',
'euc-kr' => 'euc-kr',
'gb2312' => 'gb2312',
'gb18030' => 'gb18030',
'viscii' => 'viscii',
'koi8-r' => 'koi8-r',
'koi8_r' => 'koi8-r',
'cskoi8r' => 'koi8-r',
'koi' => 'koi8-r',
'koi8' => 'koi8-r',
'koi8-u' => 'koi8-u',
'tis-620' => 'tis-620',
't.61-8bit' => 't.61-8bit',
'hz-gb-2312' => 'hz-gb-2312',
'big5-hkscs' => 'big5-hkscs',
'gbk' => 'gbk',
'cns11643' => 'x-euc-tw',
'x-imap4-modified-utf7' => 'x-imap4-modified-utf7',
'x-euc-tw' => 'x-euc-tw',
'x-mac-ce' => 'macce',
'x-mac-turkish' => 'macturkish',
'x-mac-greek' => 'macgreek',
'x-mac-icelandic' => 'macicelandic',
'x-mac-croatian' => 'maccroatian',
'x-mac-romanian' => 'macromanian',
'x-mac-cyrillic' => 'maccyrillic',
'x-mac-ukrainian' => 'macukrainian',
'x-mac-hebrew' => 'machebrew',
'x-mac-arabic' => 'macarabic',
'x-mac-farsi' => 'macfarsi',
'x-mac-devanagari' => 'macdevanagari',
'x-mac-gujarati' => 'macgujarati',
'x-mac-gurmukhi' => 'macgurmukhi',
'armscii-8' => 'armscii-8',
'x-viet-tcvn5712' => 'x-viet-tcvn5712',
'x-viet-vps' => 'x-viet-vps',
'iso-10646-ucs-2' => 'utf-16be',
'x-iso-10646-ucs-2-be' => 'utf-16be',
'x-iso-10646-ucs-2-le' => 'utf-16le',
'x-user-defined' => 'x-user-defined',
'x-johab' => 'x-johab',
'latin1' => 'iso-8859-1',
'iso_8859-1' => 'iso-8859-1',
'iso8859-1' => 'iso-8859-1',
'iso8859-2' => 'iso-8859-2',
'iso8859-3' => 'iso-8859-3',
'iso8859-4' => 'iso-8859-4',
'iso8859-5' => 'iso-8859-5',
'iso8859-6' => 'iso-8859-6',
'iso8859-7' => 'iso-8859-7',
'iso8859-8' => 'iso-8859-8',
'iso8859-9' => 'iso-8859-9',
'iso8859-10' => 'iso-8859-10',
'iso8859-11' => 'iso-8859-11',
'iso8859-13' => 'iso-8859-13',
'iso8859-14' => 'iso-8859-14',
'iso8859-15' => 'iso-8859-15',
'iso_8859-1:1987' => 'iso-8859-1',
'iso-ir-100' => 'iso-8859-1',
'l1' => 'iso-8859-1',
'ibm819' => 'iso-8859-1',
'cp819' => 'iso-8859-1',
'csisolatin1' => 'iso-8859-1',
'latin2' => 'iso-8859-2',
'iso_8859-2' => 'iso-8859-2',
'iso_8859-2:1987' => 'iso-8859-2',
'iso-ir-101' => 'iso-8859-2',
'l2' => 'iso-8859-2',
'csisolatin2' => 'iso-8859-2',
'latin3' => 'iso-8859-3',
'iso_8859-3' => 'iso-8859-3',
'iso_8859-3:1988' => 'iso-8859-3',
'iso-ir-109' => 'iso-8859-3',
'l3' => 'iso-8859-3',
'csisolatin3' => 'iso-8859-3',
'latin4' => 'iso-8859-4',
'iso_8859-4' => 'iso-8859-4',
'iso_8859-4:1988' => 'iso-8859-4',
'iso-ir-110' => 'iso-8859-4',
'l4' => 'iso-8859-4',
'csisolatin4' => 'iso-8859-4',
'cyrillic' => 'iso-8859-5',
'iso_8859-5' => 'iso-8859-5',
'iso_8859-5:1988' => 'iso-8859-5',
'iso-ir-144' => 'iso-8859-5',
'csisolatincyrillic' => 'iso-8859-5',
'arabic' => 'iso-8859-6',
'iso_8859-6' => 'iso-8859-6',
'iso_8859-6:1987' => 'iso-8859-6',
'iso-ir-127' => 'iso-8859-6',
'ecma-114' => 'iso-8859-6',
'asmo-708' => 'iso-8859-6',
'csisolatinarabic' => 'iso-8859-6',
'csiso88596i' => 'iso-8859-6-i',
'csiso88596e' => 'iso-8859-6-e',
'greek' => 'iso-8859-7',
'greek8' => 'iso-8859-7',
'sun_eu_greek' => 'iso-8859-7',
'iso_8859-7' => 'iso-8859-7',
'iso_8859-7:1987' => 'iso-8859-7',
'iso-ir-126' => 'iso-8859-7',
'elot_928' => 'iso-8859-7',
'ecma-118' => 'iso-8859-7',
'csisolatingreek' => 'iso-8859-7',
'hebrew' => 'iso-8859-8',
'iso_8859-8' => 'iso-8859-8',
'visual' => 'iso-8859-8',
'iso_8859-8:1988' => 'iso-8859-8',
'iso-ir-138' => 'iso-8859-8',
'csisolatinhebrew' => 'iso-8859-8',
'csiso88598i' => 'iso-8859-8',
'iso-8859-8i' => 'iso-8859-8',
'logical' => 'iso-8859-8',
'csiso88598e' => 'iso-8859-8-e',
'latin5' => 'iso-8859-9',
'iso_8859-9' => 'iso-8859-9',
'iso_8859-9:1989' => 'iso-8859-9',
'iso-ir-148' => 'iso-8859-9',
'l5' => 'iso-8859-9',
'csisolatin5' => 'iso-8859-9',
'unicode-1-1-utf-8' => 'utf-8',
'utf8' => 'utf-8',
'x-sjis' => 'shift_jis',
'shift-jis' => 'shift_jis',
'ms_kanji' => 'shift_jis',
'csshiftjis' => 'shift_jis',
'windows-31j' => 'shift_jis',
'cp932' => 'shift_jis',
'sjis' => 'shift_jis',
'cseucpkdfmtjapanese' => 'euc-jp',
'x-euc-jp' => 'euc-jp',
'csiso2022jp' => 'iso-2022-jp',
'iso-2022-jp-2' => 'iso-2022-jp',
'csiso2022jp2' => 'iso-2022-jp',
'csbig5' => 'big5',
'cn-big5' => 'big5',
'x-x-big5' => 'big5',
'zh_tw-big5' => 'big5',
'cseuckr' => 'euc-kr',
'ks_c_5601-1987' => 'euc-kr',
'iso-ir-149' => 'euc-kr',
'ks_c_5601-1989' => 'euc-kr',
'ksc_5601' => 'euc-kr',
'ksc5601' => 'euc-kr',
'korean' => 'euc-kr',
'csksc56011987' => 'euc-kr',
'5601' => 'euc-kr',
'windows-949' => 'euc-kr',
'gb_2312-80' => 'gb2312',
'iso-ir-58' => 'gb2312',
'chinese' => 'gb2312',
'csiso58gb231280' => 'gb2312',
'csgb2312' => 'gb2312',
'zh_cn.euc' => 'gb2312',
'gb_2312' => 'gb2312',
'x-cp1250' => 'windows-1250',
'x-cp1251' => 'windows-1251',
'x-cp1252' => 'windows-1252',
'x-cp1253' => 'windows-1253',
'x-cp1254' => 'windows-1254',
'x-cp1255' => 'windows-1255',
'x-cp1256' => 'windows-1256',
'x-cp1257' => 'windows-1257',
'x-cp1258' => 'windows-1258',
'windows-874' => 'windows-874',
'ibm874' => 'windows-874',
'dos-874' => 'windows-874',
'macintosh' => 'macintosh',
'x-mac-roman' => 'macintosh',
'mac' => 'macintosh',
'csmacintosh' => 'macintosh',
'cp866' => 'ibm866',
'cp-866' => 'ibm866',
'866' => 'ibm866',
'csibm866' => 'ibm866',
'cp850' => 'ibm850',
'850' => 'ibm850',
'csibm850' => 'ibm850',
'cp852' => 'ibm852',
'852' => 'ibm852',
'csibm852' => 'ibm852',
'cp855' => 'ibm855',
'855' => 'ibm855',
'csibm855' => 'ibm855',
'cp857' => 'ibm857',
'857' => 'ibm857',
'csibm857' => 'ibm857',
'cp862' => 'ibm862',
'862' => 'ibm862',
'csibm862' => 'ibm862',
'cp864' => 'ibm864',
'864' => 'ibm864',
'csibm864' => 'ibm864',
'ibm-864' => 'ibm864',
't.61' => 't.61-8bit',
'iso-ir-103' => 't.61-8bit',
'csiso103t618bit' => 't.61-8bit',
'x-unicode-2-0-utf-7' => 'utf-7',
'unicode-2-0-utf-7' => 'utf-7',
'unicode-1-1-utf-7' => 'utf-7',
'csunicode11utf7' => 'utf-7',
'csunicode' => 'utf-16be',
'csunicode11' => 'utf-16be',
'iso-10646-ucs-basic' => 'utf-16be',
'csunicodeascii' => 'utf-16be',
'iso-10646-unicode-latin1' => 'utf-16be',
'csunicodelatin1' => 'utf-16be',
'iso-10646' => 'utf-16be',
'iso-10646-j-1' => 'utf-16be',
'latin6' => 'iso-8859-10',
'iso-ir-157' => 'iso-8859-10',
'l6' => 'iso-8859-10',
'csisolatin6' => 'iso-8859-10',
'iso_8859-15' => 'iso-8859-15',
'csisolatin9' => 'iso-8859-15',
'l9' => 'iso-8859-15',
'ecma-cyrillic' => 'iso-ir-111',
'csiso111ecmacyrillic' => 'iso-ir-111',
'csiso2022kr' => 'iso-2022-kr',
'csviscii' => 'viscii',
'zh_tw-euc' => 'x-euc-tw',
'iso88591' => 'iso-8859-1',
'iso88592' => 'iso-8859-2',
'iso88593' => 'iso-8859-3',
'iso88594' => 'iso-8859-4',
'iso88595' => 'iso-8859-5',
'iso88596' => 'iso-8859-6',
'iso88597' => 'iso-8859-7',
'iso88598' => 'iso-8859-8',
'iso88599' => 'iso-8859-9',
'iso885910' => 'iso-8859-10',
'iso885911' => 'iso-8859-11',
'iso885912' => 'iso-8859-12',
'iso885913' => 'iso-8859-13',
'iso885914' => 'iso-8859-14',
'iso885915' => 'iso-8859-15',
'tis620' => 'tis-620',
'cp1250' => 'windows-1250',
'cp1251' => 'windows-1251',
'cp1252' => 'windows-1252',
'cp1253' => 'windows-1253',
'cp1254' => 'windows-1254',
'cp1255' => 'windows-1255',
'cp1256' => 'windows-1256',
'cp1257' => 'windows-1257',
'cp1258' => 'windows-1258',
'x-gbk' => 'gbk',
'windows-936' => 'gbk',
'ansi-1251' => 'windows-1251',
];
/**
* {@inheritdoc}
*/
public function decodeCharset($encodedString, $charset)
{
$charset = $this->getCharsetAlias($charset);
if ($charset == 'utf-8' || $charset == 'us-ascii') {
return $encodedString;
}
if (function_exists('mb_convert_encoding')) {
if ($charset == 'iso-2022-jp') {
return mb_convert_encoding($encodedString, 'utf-8', 'iso-2022-jp-ms');
}
if (array_search($charset, $this->getSupportedEncodings())) {
return mb_convert_encoding($encodedString, 'utf-8', $charset);
}
}
return iconv($charset, 'utf-8//translit//ignore', $encodedString);
}
/**
* {@inheritdoc}
*/
public function getCharsetAlias($charset)
{
$charset = strtolower($charset);
if (array_key_exists($charset, $this->charsetAlias)) {
return $this->charsetAlias[$charset];
}
return 'us-ascii';
}
private function getSupportedEncodings()
{
return
array_map(
'strtolower',
array_unique(
array_merge(
$enc = mb_list_encodings(),
call_user_func_array(
'array_merge',
array_map(
"mb_encoding_aliases",
$enc
)
)
)
)
);
}
}

View File

@ -0,0 +1,24 @@
<?php namespace PhpMimeMailParser\Contracts;
interface CharsetManager
{
/**
* Decode the string from Charset
*
* @param string $encodedString The string in its original encoded state
* @param string $charset The Charset header of the part.
*
* @return string The decoded string
*/
public function decodeCharset($encodedString, $charset);
/**
* Get charset alias
*
* @param string $charset .
*
* @return string The charset alias
*/
public function getCharsetAlias($charset);
}

View File

@ -0,0 +1,23 @@
<?php
namespace PhpMimeMailParser\Contracts;
use PhpMimeMailParser\MimePart;
use PhpMimeMailParser\MiddlewareStack;
/**
* Process Mime parts by either:
* processing the part or calling the $next MiddlewareStack
*/
interface Middleware
{
/**
* Process a mime part, optionally delegating parsing to the $next MiddlewareStack
*
* @param MimePart $part
* @param MiddlewareStack $next
*
* @return MimePart
*/
public function parse(MimePart $part, MiddlewareStack $next);
}

View File

@ -0,0 +1,8 @@
<?php
namespace PhpMimeMailParser;
class Exception extends \RuntimeException
{
}

View File

@ -0,0 +1,29 @@
<?php
namespace PhpMimeMailParser;
/**
* Wraps a callable as a Middleware
*/
class Middleware implements Contracts\Middleware
{
protected $parser;
/**
* Create a middleware using a callable $fn
*
* @param callable $fn
*/
public function __construct(callable $fn)
{
$this->parser = $fn;
}
/**
* Process a mime part, optionally delegating parsing to the $next MiddlewareStack
*/
public function parse(MimePart $part, MiddlewareStack $next)
{
return call_user_func($this->parser, $part, $next);
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace PhpMimeMailParser;
use PhpMimeMailParser\Contracts\MiddleWare as MiddleWareContracts;
/**
* A stack of middleware chained together by (MiddlewareStack $next)
*/
class MiddlewareStack
{
/**
* Next MiddlewareStack in chain
*
* @var MiddlewareStack
*/
protected $next;
/**
* Middleware in this MiddlewareStack
*
* @var Middleware
*/
protected $middleware;
/**
* Construct the first middleware in this MiddlewareStack
* The next middleware is chained through $MiddlewareStack->add($Middleware)
*
* @param Middleware $middleware
*/
public function __construct(MiddleWareContracts $middleware = null)
{
$this->middleware = $middleware;
}
/**
* Creates a chained middleware in MiddlewareStack
*
* @param Middleware $middleware
* @return MiddlewareStack Immutable MiddlewareStack
*/
public function add(MiddleWareContracts $middleware)
{
$stack = new static($middleware);
$stack->next = $this;
return $stack;
}
/**
* Parses the MimePart by passing it through the Middleware
* @param MimePart $part
* @return MimePart
*/
public function parse(MimePart $part)
{
if (!$this->middleware) {
return $part;
}
$part = call_user_func(array($this->middleware, 'parse'), $part, $this->next);
return $part;
}
/**
* Creates a MiddlewareStack based on an array of middleware
*
* @param Middleware[] $middlewares
* @return MiddlewareStack
*/
public static function factory(array $middlewares = array())
{
$stack = new static;
foreach ($middlewares as $middleware) {
$stack = $stack->add($middleware);
}
return $stack;
}
/**
* Allow calling MiddlewareStack instance directly to invoke parse()
*
* @param MimePart $part
* @return MimePart
*/
public function __invoke(MimePart $part)
{
return $this->parse($part);
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace PhpMimeMailParser;
/**
* Mime Part
* Represents the results of mailparse_msg_get_part_data()
*
* Note ArrayAccess::offsetSet() cannot modify deeply nestated arrays.
* When modifying use getPart() and setPart() for deep nested data modification
*
* @example
*
* $MimePart['headers']['from'] = 'modified@example.com' // fails
*
* // correct
* $part = $MimePart->getPart();
* $part['headers']['from'] = 'modified@example.com';
* $MimePart->setPart($part);
*/
class MimePart implements \ArrayAccess
{
/**
* Internal mime part
*
* @var array
*/
protected $part = array();
/**
* Immutable Part Id
*
* @var string
*/
private $id;
/**
* Create a mime part
*
* @param array $part
* @param string $id
*/
public function __construct($id, array $part)
{
$this->part = $part;
$this->id = $id;
}
/**
* Retrieve the part Id
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Retrieve the part data
*
* @return array
*/
public function getPart()
{
return $this->part;
}
/**
* Set the mime part data
*
* @param array $part
* @return void
*/
public function setPart(array $part)
{
$this->part = $part;
}
/**
* ArrayAccess
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->part[] = $value;
return;
}
$this->part[$offset] = $value;
}
/**
* ArrayAccess
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return isset($this->part[$offset]);
}
/**
* ArrayAccess
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
unset($this->part[$offset]);
}
/**
* ArrayAccess
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return isset($this->part[$offset]) ? $this->part[$offset] : null;
}
}

View File

@ -0,0 +1,923 @@
<?php
namespace PhpMimeMailParser;
use PhpMimeMailParser\Contracts\CharsetManager;
/**
* Parser of php-mime-mail-parser
*
* Fully Tested Mailparse Extension Wrapper for PHP 5.4+
*
*/
class Parser
{
/**
* Attachment filename argument option for ->saveAttachments().
*/
const ATTACHMENT_DUPLICATE_THROW = 'DuplicateThrow';
const ATTACHMENT_DUPLICATE_SUFFIX = 'DuplicateSuffix';
const ATTACHMENT_RANDOM_FILENAME = 'RandomFilename';
/**
* PHP MimeParser Resource ID
*
* @var resource $resource
*/
protected $resource;
/**
* A file pointer to email
*
* @var resource $stream
*/
protected $stream;
/**
* A text of an email
*
* @var string $data
*/
protected $data;
/**
* Parts of an email
*
* @var array $parts
*/
protected $parts;
/**
* @var CharsetManager object
*/
protected $charset;
/**
* Valid stream modes for reading
*
* @var array
*/
protected static $readableModes = [
'r', 'r+', 'w+', 'a+', 'x+', 'c+', 'rb', 'r+b', 'w+b', 'a+b',
'x+b', 'c+b', 'rt', 'r+t', 'w+t', 'a+t', 'x+t', 'c+t'
];
/**
* Stack of middleware registered to process data
*
* @var MiddlewareStack
*/
protected $middlewareStack;
/**
* Parser constructor.
*
* @param CharsetManager|null $charset
*/
public function __construct(CharsetManager $charset = null)
{
if ($charset == null) {
$charset = new Charset();
}
$this->charset = $charset;
$this->middlewareStack = new MiddlewareStack();
}
/**
* Free the held resources
*
* @return void
*/
public function __destruct()
{
// clear the email file resource
if (is_resource($this->stream)) {
fclose($this->stream);
}
// clear the MailParse resource
if (is_resource($this->resource)) {
mailparse_msg_free($this->resource);
}
}
/**
* Set the file path we use to get the email text
*
* @param string $path File path to the MIME mail
*
* @return Parser MimeMailParser Instance
*/
public function setPath($path)
{
if (is_writable($path)) {
$file = fopen($path, 'a+');
fseek($file, -1, SEEK_END);
if (fread($file, 1) != "\n") {
fwrite($file, PHP_EOL);
}
fclose($file);
}
// should parse message incrementally from file
$this->resource = mailparse_msg_parse_file($path);
$this->stream = fopen($path, 'r');
$this->parse();
return $this;
}
/**
* Set the Stream resource we use to get the email text
*
* @param resource $stream
*
* @return Parser MimeMailParser Instance
* @throws Exception
*/
public function setStream($stream)
{
// streams have to be cached to file first
$meta = @stream_get_meta_data($stream);
if (!$meta || !$meta['mode'] || !in_array($meta['mode'], self::$readableModes, true) || $meta['eof']) {
throw new Exception(
'setStream() expects parameter stream to be readable stream resource.'
);
}
/** @var resource $tmp_fp */
$tmp_fp = tmpfile();
if ($tmp_fp) {
while (!feof($stream)) {
fwrite($tmp_fp, fread($stream, 2028));
}
if (fread($tmp_fp, 1) != "\n") {
fwrite($tmp_fp, PHP_EOL);
}
fseek($tmp_fp, 0);
$this->stream = &$tmp_fp;
} else {
throw new Exception(
'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'
);
}
fclose($stream);
$this->resource = mailparse_msg_create();
// parses the message incrementally (low memory usage but slower)
while (!feof($this->stream)) {
mailparse_msg_parse($this->resource, fread($this->stream, 2082));
}
$this->parse();
return $this;
}
/**
* Set the email text
*
* @param string $data
*
* @return Parser MimeMailParser Instance
*/
public function setText($data)
{
if (empty($data)) {
throw new Exception('You must not call MimeMailParser::setText with an empty string parameter');
}
if (substr($data, -1) != "\n") {
$data = $data.PHP_EOL;
}
$this->resource = mailparse_msg_create();
// does not parse incrementally, fast memory hog might explode
mailparse_msg_parse($this->resource, $data);
$this->data = $data;
$this->parse();
return $this;
}
/**
* Parse the Message into parts
*
* @return void
*/
protected function parse()
{
$structure = mailparse_msg_get_structure($this->resource);
$this->parts = [];
foreach ($structure as $part_id) {
$part = mailparse_msg_get_part($this->resource, $part_id);
$part_data = mailparse_msg_get_part_data($part);
$mimePart = new MimePart($part_id, $part_data);
// let each middleware parse the part before saving
$this->parts[$part_id] = $this->middlewareStack->parse($mimePart)->getPart();
}
}
/**
* Retrieve a specific Email Header, without charset conversion.
*
* @param string $name Header name (case-insensitive)
*
* @return string|bool
* @throws Exception
*/
public function getRawHeader($name)
{
$name = strtolower($name);
if (isset($this->parts[1])) {
$headers = $this->getPart('headers', $this->parts[1]);
return isset($headers[$name]) ? $headers[$name] : false;
} else {
throw new Exception(
'setPath() or setText() or setStream() must be called before retrieving email headers.'
);
}
}
/**
* Retrieve a specific Email Header
*
* @param string $name Header name (case-insensitive)
*
* @return string|bool
*/
public function getHeader($name)
{
$rawHeader = $this->getRawHeader($name);
if ($rawHeader === false) {
return false;
}
return $this->decodeHeader($rawHeader);
}
/**
* Retrieve all mail headers
*
* @return array
* @throws Exception
*/
public function getHeaders()
{
if (isset($this->parts[1])) {
$headers = $this->getPart('headers', $this->parts[1]);
foreach ($headers as &$value) {
if (is_array($value)) {
foreach ($value as &$v) {
$v = $this->decodeSingleHeader($v);
}
} else {
$value = $this->decodeSingleHeader($value);
}
}
return $headers;
} else {
throw new Exception(
'setPath() or setText() or setStream() must be called before retrieving email headers.'
);
}
}
/**
* Retrieve the raw mail headers as a string
*
* @return string
* @throws Exception
*/
public function getHeadersRaw()
{
if (isset($this->parts[1])) {
return $this->getPartHeader($this->parts[1]);
} else {
throw new Exception(
'setPath() or setText() or setStream() must be called before retrieving email headers.'
);
}
}
/**
* Retrieve the raw Header of a MIME part
*
* @return String
* @param $part Object
* @throws Exception
*/
protected function getPartHeader(&$part)
{
$header = '';
if ($this->stream) {
$header = $this->getPartHeaderFromFile($part);
} elseif ($this->data) {
$header = $this->getPartHeaderFromText($part);
}
return $header;
}
/**
* Retrieve the Header from a MIME part from file
*
* @return String Mime Header Part
* @param $part Array
*/
protected function getPartHeaderFromFile(&$part)
{
$start = $part['starting-pos'];
$end = $part['starting-pos-body'];
fseek($this->stream, $start, SEEK_SET);
$header = fread($this->stream, $end - $start);
return $header;
}
/**
* Retrieve the Header from a MIME part from text
*
* @return String Mime Header Part
* @param $part Array
*/
protected function getPartHeaderFromText(&$part)
{
$start = $part['starting-pos'];
$end = $part['starting-pos-body'];
$header = substr($this->data, $start, $end - $start);
return $header;
}
/**
* Checks whether a given part ID is a child of another part
* eg. an RFC822 attachment may have one or more text parts
*
* @param string $partId
* @param string $parentPartId
* @return bool
*/
protected function partIdIsChildOfPart($partId, $parentPartId)
{
$parentPartId = $parentPartId.'.';
return substr($partId, 0, strlen($parentPartId)) == $parentPartId;
}
/**
* Whether the given part ID is a child of any attachment part in the message.
*
* @param string $checkPartId
* @return bool
*/
protected function partIdIsChildOfAnAttachment($checkPartId)
{
foreach ($this->parts as $partId => $part) {
if ($this->getPart('content-disposition', $part) == 'attachment') {
if ($this->partIdIsChildOfPart($checkPartId, $partId)) {
return true;
}
}
}
return false;
}
/**
* Returns the email message body in the specified format
*
* @param string $type text, html or htmlEmbedded
*
* @return string Body
* @throws Exception
*/
public function getMessageBody($type = 'text')
{
$mime_types = [
'text' => 'text/plain',
'html' => 'text/html',
'htmlEmbedded' => 'text/html',
];
if (in_array($type, array_keys($mime_types))) {
$part_type = $type === 'htmlEmbedded' ? 'html' : $type;
$inline_parts = $this->getInlineParts($part_type);
$body = empty($inline_parts) ? '' : $inline_parts[0];
} else {
throw new Exception(
'Invalid type specified for getMessageBody(). Expected: text, html or htmlEmbeded.'
);
}
if ($type == 'htmlEmbedded') {
$attachments = $this->getAttachments();
foreach ($attachments as $attachment) {
if ($attachment->getContentID() != '') {
$body = str_replace(
'"cid:'.$attachment->getContentID().'"',
'"'.$this->getEmbeddedData($attachment->getContentID()).'"',
$body
);
}
}
}
return $body;
}
/**
* Returns the embedded data structure
*
* @param string $contentId Content-Id
*
* @return string
*/
protected function getEmbeddedData($contentId)
{
foreach ($this->parts as $part) {
if ($this->getPart('content-id', $part) == $contentId) {
$embeddedData = 'data:';
$embeddedData .= $this->getPart('content-type', $part);
$embeddedData .= ';'.$this->getPart('transfer-encoding', $part);
$embeddedData .= ','.$this->getPartBody($part);
return $embeddedData;
}
}
return '';
}
/**
* Return an array with the following keys display, address, is_group
*
* @param string $name Header name (case-insensitive)
*
* @return array
*/
public function getAddresses($name)
{
$value = $this->getRawHeader($name);
$value = (is_array($value)) ? $value[0] : $value;
$addresses = mailparse_rfc822_parse_addresses($value);
foreach ($addresses as $i => $item) {
$addresses[$i]['display'] = $this->decodeHeader($item['display']);
}
return $addresses;
}
/**
* Returns the attachments contents in order of appearance
*
* @return Attachment[]
*/
public function getInlineParts($type = 'text')
{
$inline_parts = [];
$mime_types = [
'text' => 'text/plain',
'html' => 'text/html',
];
if (!in_array($type, array_keys($mime_types))) {
throw new Exception('Invalid type specified for getInlineParts(). "type" can either be text or html.');
}
foreach ($this->parts as $partId => $part) {
if ($this->getPart('content-type', $part) == $mime_types[$type]
&& $this->getPart('content-disposition', $part) != 'attachment'
&& !$this->partIdIsChildOfAnAttachment($partId)
) {
$headers = $this->getPart('headers', $part);
$encodingType = array_key_exists('content-transfer-encoding', $headers) ?
$headers['content-transfer-encoding'] : '';
$undecoded_body = $this->decodeContentTransfer($this->getPartBody($part), $encodingType);
$inline_parts[] = $this->charset->decodeCharset($undecoded_body, $this->getPartCharset($part));
}
}
return $inline_parts;
}
/**
* Returns the attachments contents in order of appearance
*
* @return Attachment[]
*/
public function getAttachments($include_inline = true)
{
$attachments = [];
$dispositions = $include_inline ? ['attachment', 'inline'] : ['attachment'];
$non_attachment_types = ['text/plain', 'text/html'];
$nonameIter = 0;
foreach ($this->parts as $part) {
$disposition = $this->getPart('content-disposition', $part);
$filename = 'noname';
if (isset($part['disposition-filename'])) {
$filename = $this->decodeHeader($part['disposition-filename']);
} elseif (isset($part['content-name'])) {
// if we have no disposition but we have a content-name, it's a valid attachment.
// we simulate the presence of an attachment disposition with a disposition filename
$filename = $this->decodeHeader($part['content-name']);
$disposition = 'attachment';
} elseif (in_array($part['content-type'], $non_attachment_types, true)
&& $disposition !== 'attachment') {
// it is a message body, no attachment
continue;
} elseif (substr($part['content-type'], 0, 10) !== 'multipart/'
&& $part['content-type'] !== 'text/plain; (error)') {
// if we cannot get it by getMessageBody(), we assume it is an attachment
$disposition = 'attachment';
}
if (in_array($disposition, ['attachment', 'inline']) === false && !empty($disposition)) {
$disposition = 'attachment';
}
if (in_array($disposition, $dispositions) === true) {
if ($filename == 'noname') {
$nonameIter++;
$filename = 'noname'.$nonameIter;
} else {
// Escape all potentially unsafe characters from the filename
$filename = preg_replace('((^\.)|\/|[\n|\r|\n\r]|(\.$))', '_', $filename);
}
$headersAttachments = $this->getPart('headers', $part);
$contentidAttachments = $this->getPart('content-id', $part);
$attachmentStream = $this->getAttachmentStream($part);
$mimePartStr = $this->getPartComplete($part);
$attachments[] = new Attachment(
$filename,
$this->getPart('content-type', $part),
$attachmentStream,
$disposition,
$contentidAttachments,
$headersAttachments,
$mimePartStr
);
}
}
return $attachments;
}
/**
* Save attachments in a folder
*
* @param string $attach_dir directory
* @param bool $include_inline
* @param string $filenameStrategy How to generate attachment filenames
*
* @return array Saved attachments paths
* @throws Exception
*/
public function saveAttachments(
$attach_dir,
$include_inline = true,
$filenameStrategy = self::ATTACHMENT_DUPLICATE_SUFFIX
) {
$attachments = $this->getAttachments($include_inline);
$attachments_paths = [];
foreach ($attachments as $attachment) {
$attachments_paths[] = $attachment->save($attach_dir, $filenameStrategy);
}
return $attachments_paths;
}
/**
* Read the attachment Body and save temporary file resource
*
* @param array $part
*
* @return resource Mime Body Part
* @throws Exception
*/
protected function getAttachmentStream(&$part)
{
/** @var resource $temp_fp */
$temp_fp = tmpfile();
$headers = $this->getPart('headers', $part);
$encodingType = array_key_exists('content-transfer-encoding', $headers) ?
$headers['content-transfer-encoding'] : '';
if ($temp_fp) {
if ($this->stream) {
$start = $part['starting-pos-body'];
$end = $part['ending-pos-body'];
fseek($this->stream, $start, SEEK_SET);
$len = $end - $start;
$written = 0;
while ($written < $len) {
$write = $len;
$data = fread($this->stream, $write);
fwrite($temp_fp, $this->decodeContentTransfer($data, $encodingType));
$written += $write;
}
} elseif ($this->data) {
$attachment = $this->decodeContentTransfer($this->getPartBodyFromText($part), $encodingType);
fwrite($temp_fp, $attachment, strlen($attachment));
}
fseek($temp_fp, 0, SEEK_SET);
} else {
throw new Exception(
'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'
);
}
return $temp_fp;
}
/**
* Decode the string from Content-Transfer-Encoding
*
* @param string $encodedString The string in its original encoded state
* @param string $encodingType The encoding type from the Content-Transfer-Encoding header of the part.
*
* @return string The decoded string
*/
protected function decodeContentTransfer($encodedString, $encodingType)
{
if (is_array($encodingType)) {
$encodingType = $encodingType[0];
}
$encodingType = strtolower($encodingType);
if ($encodingType == 'base64') {
return base64_decode($encodedString);
} elseif ($encodingType == 'quoted-printable') {
return quoted_printable_decode($encodedString);
} else {
return $encodedString;
}
}
/**
* $input can be a string or array
*
* @param string|array $input
*
* @return string
*/
protected function decodeHeader($input)
{
//Sometimes we have 2 label From so we take only the first
if (is_array($input)) {
return $this->decodeSingleHeader($input[0]);
}
return $this->decodeSingleHeader($input);
}
/**
* Decodes a single header (= string)
*
* @param string $input
*
* @return string
*/
protected function decodeSingleHeader($input)
{
// For each encoded-word...
while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)((\s+)=\?)?/i', $input, $matches)) {
$encoded = $matches[1];
$charset = $matches[2];
$encoding = $matches[3];
$text = $matches[4];
$space = isset($matches[6]) ? $matches[6] : '';
switch (strtolower($encoding)) {
case 'b':
$text = $this->decodeContentTransfer($text, 'base64');
break;
case 'q':
$text = str_replace('_', ' ', $text);
preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
foreach ($matches[1] as $value) {
$text = str_replace('='.$value, chr(hexdec($value)), $text);
}
break;
}
$text = $this->charset->decodeCharset($text, $this->charset->getCharsetAlias($charset));
$input = str_replace($encoded.$space, $text, $input);
}
return $input;
}
/**
* Return the charset of the MIME part
*
* @param array $part
*
* @return string
*/
protected function getPartCharset($part)
{
if (isset($part['charset'])) {
return $this->charset->getCharsetAlias($part['charset']);
} else {
return 'us-ascii';
}
}
/**
* Retrieve a specified MIME part
*
* @param string $type
* @param array $parts
*
* @return string|array
*/
protected function getPart($type, $parts)
{
return (isset($parts[$type])) ? $parts[$type] : false;
}
/**
* Retrieve the Body of a MIME part
*
* @param array $part
*
* @return string
*/
protected function getPartBody(&$part)
{
$body = '';
if ($this->stream) {
$body = $this->getPartBodyFromFile($part);
} elseif ($this->data) {
$body = $this->getPartBodyFromText($part);
}
return $body;
}
/**
* Retrieve the Body from a MIME part from file
*
* @param array $part
*
* @return string Mime Body Part
*/
protected function getPartBodyFromFile(&$part)
{
$start = $part['starting-pos-body'];
$end = $part['ending-pos-body'];
$body = '';
if ($end - $start > 0) {
fseek($this->stream, $start, SEEK_SET);
$body = fread($this->stream, $end - $start);
}
return $body;
}
/**
* Retrieve the Body from a MIME part from text
*
* @param array $part
*
* @return string Mime Body Part
*/
protected function getPartBodyFromText(&$part)
{
$start = $part['starting-pos-body'];
$end = $part['ending-pos-body'];
return substr($this->data, $start, $end - $start);
}
/**
* Retrieve the content of a MIME part
*
* @param array $part
*
* @return string
*/
protected function getPartComplete(&$part)
{
$body = '';
if ($this->stream) {
$body = $this->getPartFromFile($part);
} elseif ($this->data) {
$body = $this->getPartFromText($part);
}
return $body;
}
/**
* Retrieve the content from a MIME part from file
*
* @param array $part
*
* @return string Mime Content
*/
protected function getPartFromFile(&$part)
{
$start = $part['starting-pos'];
$end = $part['ending-pos'];
$body = '';
if ($end - $start > 0) {
fseek($this->stream, $start, SEEK_SET);
$body = fread($this->stream, $end - $start);
}
return $body;
}
/**
* Retrieve the content from a MIME part from text
*
* @param array $part
*
* @return string Mime Content
*/
protected function getPartFromText(&$part)
{
$start = $part['starting-pos'];
$end = $part['ending-pos'];
return substr($this->data, $start, $end - $start);
}
/**
* Retrieve the resource
*
* @return resource resource
*/
public function getResource()
{
return $this->resource;
}
/**
* Retrieve the file pointer to email
*
* @return resource stream
*/
public function getStream()
{
return $this->stream;
}
/**
* Retrieve the text of an email
*
* @return string data
*/
public function getData()
{
return $this->data;
}
/**
* Retrieve the parts of an email
*
* @return array parts
*/
public function getParts()
{
return $this->parts;
}
/**
* Retrieve the charset manager object
*
* @return CharsetManager charset
*/
public function getCharset()
{
return $this->charset;
}
/**
* Add a middleware to the parser MiddlewareStack
* Each middleware is invoked when:
* a MimePart is retrieved by mailparse_msg_get_part_data() during $this->parse()
* The middleware will receive MimePart $part and the next MiddlewareStack $next
*
* Eg:
*
* $Parser->addMiddleware(function(MimePart $part, MiddlewareStack $next) {
* // do something with the $part
* return $next($part);
* });
*
* @param callable $middleware Plain Function or Middleware Instance to execute
* @return void
*/
public function addMiddleware(callable $middleware)
{
if (!$middleware instanceof Middleware) {
$middleware = new Middleware($middleware);
}
$this->middlewareStack = $this->middlewareStack->add($middleware);
}
}

View File

@ -0,0 +1,63 @@
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: [8.0, 8.1, 8.2]
dependency-version: [prefer-lowest, prefer-stable]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mailparse
coverage: none
- name: Install dependencies
run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest --no-progress
- name: Execute tests
run: vendor/bin/phpunit
php-cs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install dependencies
run: composer update --no-progress --ignore-platform-reqs
- name: Execute phpcs
run: vendor/bin/phpcs src tests --standard=psr2
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.0
extensions: mailparse
- name: Install dependencies
run: composer update --prefer-dist --no-interaction --no-suggest --no-progress
- name: Execute code coverage
run: |
vendor/bin/phpunit --coverage-clover=coverage.xml --whitelist src
bash <(curl -s https://codecov.io/bash) -t ${{ secrets.COVERALLS_REPO_TOKEN }}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Vincent Dauce
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,257 @@
# php-mime-mail-parser
A fully tested email parser for PHP 8.0+ (mailparse extension wrapper).
It's the most effective php email parser around in terms of performance, foreign character encoding, attachment handling, and ease of use.
Internet Message Format RFC [822](https://tools.ietf.org/html/rfc822), [2822](https://tools.ietf.org/html/rfc2822), [5322](https://tools.ietf.org/html/rfc5322).
[![Latest Version](https://img.shields.io/packagist/v/php-mime-mail-parser/php-mime-mail-parser.svg?style=flat-square)](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases)
[![Total Downloads](https://img.shields.io/packagist/dt/php-mime-mail-parser/php-mime-mail-parser.svg?style=flat-square)](https://packagist.org/packages/php-mime-mail-parser/php-mime-mail-parser)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
## Why?
This extension can be used to...
* Parse and read email from Postfix
* For reading messages (Filename extension: eml)
* Create webmail
* Store email information such a subject, HTML body, attachments, and etc. into a database
## Is it reliable?
Yes. All known issues have been reproduced, fixed and tested.
We use GitHub Actions, Codecov, Codacy to help ensure code quality. You can see real-time statistics below:
[![CI](https://github.com/php-mime-mail-parser/php-mime-mail-parser/actions/workflows/main.yml/badge.svg?style=flat-square)](https://github.com/php-mime-mail-parser/php-mime-mail-parser/actions/workflows/main.yml)
[![Coverage](https://codecov.io/gh/php-mime-mail-parser/php-mime-mail-parser/branch/main/graph/badge.svg?token=wTSIbXJDL0)](https://codecov.io/gh/php-mime-mail-parser/php-mime-mail-parser)
[![Code Quality](https://app.codacy.com/project/badge/Grade/8cbfe0fcd84c4b2b9282b9a0b4467607)](https://www.codacy.com/gh/php-mime-mail-parser/php-mime-mail-parser/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=php-mime-mail-parser/php-mime-mail-parser&amp;utm_campaign=Badge_Grade)
## How do I install it?
The easiest way is via [Composer](https://getcomposer.org/).
To install the latest version of PHP MIME Mail Parser, run the command below:
composer require php-mime-mail-parser/php-mime-mail-parser
## Requirements
The following versions of PHP are supported:
* PHP 8.0
* PHP 8.1
* PHP 8.2
Previous Versions:
| PHP Compatibility | Version |
|-------------------|-----------------------------|
| HHVM | php-mime-mail-parser 2.11.1 |
| PHP 5.4 | php-mime-mail-parser 2.11.1 |
| PHP 5.5 | php-mime-mail-parser 2.11.1 |
| PHP 5.6 | php-mime-mail-parser 3.0.4 |
| PHP 7.0 | php-mime-mail-parser 3.0.4 |
| PHP 7.1 | php-mime-mail-parser 5.0.5 |
| PHP 7.2 | php-mime-mail-parser 7.1.2 |
| PHP 7.3 | php-mime-mail-parser 7.1.2 |
| PHP 7.4 | php-mime-mail-parser 7.1.2 |
Make sure you have the mailparse extension (http://php.net/manual/en/book.mailparse.php) properly installed. The command line `php -m | grep mailparse` need to return "mailparse".
### Install mailparse extension
#### Ubuntu, Debian & derivatives
```
sudo apt install php-cli php-mailparse
```
#### Others platforms
```
sudo apt install php-cli php-pear php-dev php-mbstring
pecl install mailparse
```
#### From source
AAAAMMDD should be `php-config --extension-dir`
```
git clone https://github.com/php/pecl-mail-mailparse.git
cd pecl-mail-mailparse
phpize
./configure
sed -i 's/#if\s!HAVE_MBSTRING/#ifndef MBFL_MBFILTER_H/' ./mailparse.c
make
sudo mv modules/mailparse.so /usr/lib/php/AAAAMMDD/
echo "extension=mailparse.so" | sudo tee /etc/php/7.1/mods-available/mailparse.ini
sudo phpenmod mailparse
```
#### Windows
You need to download mailparse DLL from http://pecl.php.net/package/mailparse and add the line "extension=php_mailparse.dll" to php.ini accordingly.
## How do I use it?
### Loading an email
You can load an email with 4 differents ways. You only need to use one of the following four.
```php
require_once __DIR__.'/vendor/autoload.php';
$path = 'path/to/email.eml';
$parser = new PhpMimeMailParser\Parser();
// 1. Specify a file path (string)
$parser->setPath($path);
// 2. Specify the raw mime mail text (string)
$parser->setText(file_get_contents($path));
// 3. Specify a php file resource (stream)
$parser->setStream(fopen($path, "r"));
// 4. Specify a stream to work with mail server (stream)
$parser->setStream(fopen("php://stdin", "r"));
```
### Get the metadata of the message
Get the sender and the receiver:
```php
$rawHeaderTo = $parser->getHeader('to');
// return "test" <test@example.com>, "test2" <test2@example.com>
$arrayHeaderTo = $parser->getAddresses('to');
// return [["display"=>"test", "address"=>"test@example.com", false]]
$rawHeaderFrom = $parser->getHeader('from');
// return "test" <test@example.com>
$arrayHeaderFrom = $parser->getAddresses('from');
// return [["display"=>"test", "address"=>"test@example.com", "is_group"=>false]]
```
Get the subject:
```php
$subject = $parser->getHeader('subject');
```
Get other headers:
```php
$stringHeaders = $parser->getHeadersRaw();
// return all headers as a string, no charset conversion
$arrayHeaders = $parser->getHeaders();
// return all headers as an array, with charset conversion
```
### Get the body of the message
```php
$text = $parser->getMessageBody('text');
// return the text version
$html = $parser->getMessageBody('html');
// return the html version
$htmlEmbedded = $parser->getMessageBody('htmlEmbedded');
// return the html version with the embedded contents like images
```
### Get attachments
Save all attachments in a directory
```php
$parser->saveAttachments('/path/to/save/attachments/');
// return all attachments saved in the directory (include inline attachments)
$parser->saveAttachments('/path/to/save/attachments/', false);
// return all attachments saved in the directory (exclude inline attachments)
// Save all attachments with the strategy ATTACHMENT_DUPLICATE_SUFFIX (default)
$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_DUPLICATE_SUFFIX);
// return all attachments saved in the directory: logo.jpg, logo_1.jpg, ..., logo_100.jpg, YY34UFHBJ.jpg
// Save all attachments with the strategy ATTACHMENT_RANDOM_FILENAME
$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_RANDOM_FILENAME);
// return all attachments saved in the directory: YY34UFHBJ.jpg and F98DBZ9FZF.jpg
// Save all attachments with the strategy ATTACHMENT_DUPLICATE_THROW
$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_DUPLICATE_THROW);
// return an exception when there is attachments duplicate.
```
Get all attachments
```php
$attachments = $parser->getAttachments();
// return an array of all attachments (include inline attachments)
$attachments = $parser->getAttachments(false);
// return an array of all attachments (exclude inline attachments)
```
Loop through all the Attachments
```php
foreach ($attachments as $attachment) {
echo 'Filename : '.$attachment->getFilename().'<br />';
// return logo.jpg
echo 'Filesize : '.filesize($attach_dir.$attachment->getFilename()).'<br />';
// return 1000
echo 'Filetype : '.$attachment->getContentType().'<br />';
// return image/jpeg
echo 'MIME part string : '.$attachment->getMimePartStr().'<br />';
// return the whole MIME part of the attachment
$attachment->save('/path/to/save/myattachment/', Parser::ATTACHMENT_DUPLICATE_SUFFIX);
// return the path and the filename saved (same strategy available than saveAttachments)
}
```
## Postfix configuration to manage email from a mail server
Next you need to forward emails to this script above. For that I'm using [Postfix](http://www.postfix.org/) like a mail server, you need to configure /etc/postfix/master.cf
Add this line at the end of the file (specify myhook to send all emails to the script test.php)
```
myhook unix - n n - - pipe
flags=F user=www-data argv=php -c /etc/php5/apache2/php.ini -f /var/www/test.php ${sender} ${size} ${recipient}
```
Edit this line (register myhook)
```
smtp inet n - - - - smtpd
-o content_filter=myhook:dummy
```
The php script must use the fourth method to work with this configuration.
And finally the easiest way is to use my SaaS https://mailcare.io
## Can I contribute?
Feel free to contribute!
git clone https://github.com/php-mime-mail-parser/php-mime-mail-parser
cd php-mime-mail-parser
composer install
./vendor/bin/phpunit
If you report an issue, please provide the raw email that triggered it. This helps us reproduce the issue and fix it more quickly.
## License
The php-mime-mail-parser/php-mime-mail-parser is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)

View File

@ -0,0 +1,10 @@
#!/bin/sh
git clone https://github.com/php/pecl-mail-mailparse.git
cd pecl-mail-mailparse
phpize
./configure
sed -i 's/#if\s!HAVE_MBSTRING/#ifndef MBFL_MBFILTER_H/' ./mailparse.c
make
sudo mv modules/mailparse.so /home/travis/.phpenv/versions/7.3.2/lib/php/extensions/no-debug-zts-20180731/
echo 'extension=mailparse.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini

View File

@ -0,0 +1,60 @@
{
"name": "php-mime-mail-parser/php-mime-mail-parser",
"type": "library",
"description": "A fully tested email parser for PHP 8.0+ (mailparse extension wrapper).",
"keywords": ["mime", "mail", "mailparse", "MimeMailParser", "parser", "php"],
"homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser",
"license": "MIT",
"authors": [
{
"name":"eXorus",
"email":"exorus.spam@gmail.com",
"homepage":"https://github.com/eXorus/",
"role":"Developer"
},
{
"name":"M.Valinskis",
"email":"M.Valins@gmail.com",
"homepage":"https://code.google.com/p/php-mime-mail-parser",
"role":"Developer"
},
{
"name":"eugene.emmett.wood",
"email":"gene_w@cementhorizon.com",
"homepage":"https://code.google.com/p/php-mime-mail-parser",
"role":"Developer"
},
{
"name":"alknetso",
"email":"alkne@gmail.com",
"homepage":"https://code.google.com/p/php-mime-mail-parser",
"role":"Developer"
},
{
"name":"bucabay",
"email":"gabe@fijiwebdesign.com",
"homepage":"http://www.fijiwebdesign.com",
"role":"Developer"
}
],
"repository":{
"type":"git",
"url":"https://github.com/php-mime-mail-parser/php-mime-mail-parser.git"
},
"require": {
"php": "^8.0",
"ext-mailparse": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
"php-coveralls/php-coveralls": "^2.2",
"squizlabs/php_codesniffer": "^3.5"
},
"replace": {
"exorus/php-mime-mail-parser": "*",
"messaged/php-mime-mail-parser": "*"
},
"autoload": {
"psr-4": { "PhpMimeMailParser\\": "src/" }
}
}

View File

@ -0,0 +1,303 @@
<?php
/**
* @link http://php.net/manual/en/mailparse.constants.php
*/
define('MAILPARSE_EXTRACT_OUTPUT', 0);
/**
* @link http://php.net/manual/en/mailparse.constants.php
*/
define('MAILPARSE_EXTRACT_STREAM', 1);
/**
* @link http://php.net/manual/en/mailparse.constants.php
*/
define('MAILPARSE_EXTRACT_RETURN', 2);
/**
* Parses a file. This is the optimal way of parsing a mail file that you have on
* disk.
*
* @link http://php.net/manual/en/functions.mailparse-msg-parse-file.php
*
* @param string $filename Path to the file holding the message. The file is opened
* and streamed through the parser
*
* @return resource Returns a MIME resource representing the structure, or false on error
*/
function mailparse_msg_parse_file($filename)
{
}
/**
* .
*
* @link http://php.net/manual/en/functions.mailparse-msg-get-part.php
*
* @param resource $mimemail A valid MIME resource
* @param string $mimesection
*
* @return resource
*/
function mailparse_msg_get_part($mimemail, $mimesection)
{
}
/**
* .
*
* @link http://php.net/manual/en/functions.mailparse-msg-get-structure.php
*
* @param resource $mimemail A valid MIME resource
*
* @return array
*/
function mailparse_msg_get_structure($mimemail)
{
}
/**
* .
*
* @link http://php.net/manual/en/functions.mailparse-msg-get-part-data.php
*
* @param resource $mimemail A valid MIME resource
*
* @return array
*/
function mailparse_msg_get_part_data($mimemail)
{
}
/**
* .
*
* @link http://php.net/manual/en/functions.mailparse-msg-extract-part.php
*
* @param resource $mimemail A valid MIME resource
* @param string $msgbody
* @param callable $callbackfunc
*
* @return void
*/
function mailparse_msg_extract_part($mimemail, $msgbody, $callbackfunc)
{
}
/**
* Extracts/decodes a message section from the supplied filename.
*
* @link http://php.net/manual/en/functions.mailparse-msg-extract-part-file.php
*
* @param resource $mimemail A valid MIME resource, created with
* mailparse_msg_create
* @param mixed $filename Can be a file name or a valid stream resource
* @param callable $callbackfunc If set, this must be either a valid callback that
* will be passed the extracted section, or null to make this function return the
* extracted section
*
* @return string If $callbackfunc is not null returns true on success
*/
function mailparse_msg_extract_part_file($mimemail, $filename, $callbackfunc = false)
{
}
/**
* .
*
* @link http://php.net/manual/en/functions.mailparse-msg-extract-whole-part-file.php
*
* @param resource $mimemail A valid MIME resource
* @param string $filename
* @param callable $callbackfunc
*
* @return string
*/
function mailparse_msg_extract_whole_part_file($mimemail, $filename, $callbackfunc)
{
}
/**
* Create a MIME mail resource.
*
* @link http://php.net/manual/en/functions.mailparse-msg-create.php
* @return resource Returns a handle that can be used to parse a message
*/
function mailparse_msg_create()
{
}
/**
* Frees a MIME resource.
*
* @link http://php.net/manual/en/functions.mailparse-msg-free.php
*
* @param resource $mimemail A valid MIME resource allocated by
* mailparse_msg_create or mailparse_msg_parse_file
*
* @return boolean|null
*/
function mailparse_msg_free($mimemail)
{
}
/**
* Incrementally parse data into the supplied mime mail resource.
*
* @link http://php.net/manual/en/functions.mailparse-msg-parse.php
*
* @param resource $mimemail A valid MIME resource
* @param string $data
*
* @return boolean|null
*/
function mailparse_msg_parse($mimemail, $data)
{
}
/**
* Parses a RFC 822 compliant recipient list, such as that found in the To: header.
*
* @link http://php.net/manual/en/functions.mailparse-rfc822-parse-addresses.php
*
* @param string $addresses A string containing addresses, like in: Wez Furlong
* wez@example.com, doe@example.com
*
* @return array Returns an array of associative arrays with the following keys for each
* recipient: display The recipient name, for display purpose. If this part is not
* set for a recipient, this key will hold the same value as address. address The
* email address is_group true if the recipient is a newsgroup, false otherwise
*/
function mailparse_rfc822_parse_addresses($addresses)
{
}
/**
* Figures out the best way of encoding the content read from the given file
* pointer.
*
* @link http://php.net/manual/en/functions.mailparse-determine-best-xfer-encoding.php
*
* @param resource $fp A valid file pointer, which must be seek-able
*
* @return string Returns one of the character encodings supported by the mbstring module
*/
function mailparse_determine_best_xfer_encoding($fp)
{
}
/**
* Streams data from the source file pointer, apply $encoding and write to the
* destination file pointer.
*
* @link http://php.net/manual/en/functions.mailparse-stream-encode.php
*
* @param resource $sourcefp A valid file handle. The file is streamed through the
* parser
* @param resource $destfp The destination file handle in which the encoded data
* will be written
* @param string $encoding One of the character encodings supported by the mbstring
* module
*
* @return boolean|null
*/
function mailparse_stream_encode($sourcefp, $destfp, $encoding)
{
}
/**
* Scans the data from the given file pointer and extract each embedded uuencoded
* file into a temporary file.
*
* @link http://php.net/manual/en/functions.mailparse-uudecode-all.php
*
* @param resource $fp A valid file pointer
*
* @return array Returns an array of associative arrays listing filename information.
* filename Path to the temporary file name created origfilename The original
* filename, for uuencoded parts only The first filename entry is the message body.
* The next entries are the decoded uuencoded files
*/
function mailparse_uudecode_all($fp)
{
}
/**
* @return
*/
function mailparse_test()
{
}
class mimemessage
{
/**
* @return
*/
public function mimemessage()
{
}
/**
* @return
*/
public function get_child()
{
}
/**
* @return
*/
public function get_child_count()
{
}
/**
* @return
*/
public function get_parent()
{
}
/**
* @return
*/
public function extract_headers()
{
}
/**
* @return
*/
public function extract_body()
{
}
/**
* @return
*/
public function enum_uue()
{
}
/**
* @return
*/
public function extract_uue()
{
}
/**
* @return
*/
public function remove()
{
}
/**
* @return
*/
public function add_child()
{
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true" bootstrap="vendor/autoload.php">
<testsuite name="eXorus PhpMimeMailParser Test Suite">
<directory suffix="Test.php">tests</directory>
</testsuite>
</phpunit>