git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@5471 bf3deb0d-5f1a-0410-827f-c0cc1f45334c
1570 lines
55 KiB
PHP
1570 lines
55 KiB
PHP
<?php
|
|
/** DomFramework
|
|
@package domframework
|
|
@author Dominique Fournier <dominique@fournier38.fr> */
|
|
|
|
/** The class to create a complete email. Can read an email from a content */
|
|
class mail
|
|
{
|
|
|
|
/** The complete of the mail
|
|
*/
|
|
private $completeEmailEML = "";
|
|
|
|
/** Private the separator between the headers and the mail. Should be
|
|
* \r\n on a line, but special crafted mails may use something else
|
|
*/
|
|
private $headerBodySeparator = "\r\n";
|
|
|
|
/** Sections definitions
|
|
* Will store the mails sections with the parameters :
|
|
* _parent string|null The parent identifier
|
|
* _boundary string|null The Boundary identifier
|
|
* _contentType string The simplified Content Type
|
|
* Content-Type string The complete Content-Type used in previous headers
|
|
* _partsIDchild array the parts ID linked to this section
|
|
*/
|
|
private $sections = array ();
|
|
|
|
/** Counter for the recursion */
|
|
private $recurse;
|
|
|
|
/** Display the existing sections lighter than print_r, without the Carriage
|
|
* returns
|
|
*/
|
|
private function printSections ()
|
|
{
|
|
foreach ($this->sections as $sectionID=>$vals)
|
|
{
|
|
echo "[$sectionID] => Array (\n";
|
|
foreach ($vals as $key=>$val)
|
|
{
|
|
if (is_array ($val))
|
|
{
|
|
echo " [$key] => Array (\n";
|
|
foreach ($val as $k2=>$v2)
|
|
{
|
|
if (is_array ($v2))
|
|
{
|
|
echo " [$k2] => Array (\n";
|
|
foreach ($v2 as $k3=>$v3)
|
|
echo " [$k3] => ".rtrim ($v3, "\r\n")."\n";
|
|
echo " )\n";
|
|
}
|
|
else
|
|
{
|
|
echo " [$k2] => ".rtrim ($v2, "\r\n")."\n";
|
|
}
|
|
}
|
|
echo " )\n";
|
|
}
|
|
else
|
|
{
|
|
echo " [$key] => ".rtrim ($val, "\r\n")."\n";
|
|
}
|
|
}
|
|
echo ")\n\n";
|
|
}
|
|
}
|
|
|
|
/** Add a new section
|
|
* @param array $param The parameters to store
|
|
* @return the sectionID
|
|
*/
|
|
private function sectionAdd ($param)
|
|
{
|
|
$sectionID = md5 (microtime(true).rand());
|
|
$this->sections[$sectionID] = $param;
|
|
return $sectionID;
|
|
}
|
|
|
|
/** Add a new section with the default parameters
|
|
* @return array The sectionID stored with the default parameters
|
|
*/
|
|
private function sectionAddDefault ()
|
|
{
|
|
return $this->sectionAdd (
|
|
array ("_headerBodySeparator"=>$this->headerBodySeparator,
|
|
"_headerCR"=>"\r\n",
|
|
"_headersEML"=>"",
|
|
"_headersArray"=>array (),
|
|
"_contentEML"=>"",
|
|
"_contentUTF"=>""));
|
|
}
|
|
|
|
/** Del an existing section
|
|
* If there is one child, and the section was multiple, remove it and
|
|
* associate the child to a new section
|
|
* @param string $sectionID The section to delete
|
|
*/
|
|
private function sectionDel ($sectionID)
|
|
{
|
|
// TODO !
|
|
}
|
|
|
|
/** Add a newChild to an existing section at the end of the list
|
|
* @param string $sectionIDParent The parent modified by adding a child
|
|
* @param string $sectionIDchild The sectionID of the child to add
|
|
*/
|
|
private function sectionAddChild ($sectionIDParent, $sectionIDchild)
|
|
{
|
|
if (! array_key_exists ($sectionIDParent, $this->sections))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Section parent not found"), 404);
|
|
$this->sections[$sectionIDParent]["_partsIDchild"][] = $sectionIDchild;
|
|
$this->sections[$sectionIDchild]["_parentID"] = $sectionIDParent;
|
|
}
|
|
|
|
/** Add a newChild to an existing section at the beginning of the list
|
|
* @param string $sectionIDParent The parent modified by adding a child
|
|
* @param string $sectionIDchild The sectionID of the child to add
|
|
*/
|
|
private function sectionAddChildFirst ($sectionIDParent, $sectionIDchild)
|
|
{
|
|
if (! array_key_exists ($sectionIDParent, $this->sections))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Section parent not found"), 404);
|
|
array_unshift ($this->sections[$sectionIDParent]["_partsIDchild"],
|
|
$sectionIDchild);
|
|
$this->sections[$sectionIDchild]["_parentID"] = $sectionIDParent;
|
|
}
|
|
|
|
/** Remove all the defined Childs in the section. Do not remove really the
|
|
* childs !
|
|
* @param string $sectionID the section to clean
|
|
*/
|
|
private function sectionDelChilds ($sectionID)
|
|
{
|
|
if (! array_key_exists ($sectionID, $this->sections))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Section not found"), 404);
|
|
unset ($this->sections[$sectionID]["_partsIDchild"]);
|
|
}
|
|
|
|
/** Update the content of an existing section
|
|
* @param string $sectionID The section to modify
|
|
* @param array $param The parameters to update
|
|
*/
|
|
private function sectionUpdate ($sectionID, $param)
|
|
{
|
|
if (! array_key_exists ($sectionID, $this->sections))
|
|
throw new \Exception (dgettext ("domframework", "Section not found"),
|
|
404);
|
|
if (! is_array ($param))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Param provided to sectionUpdate is not array"), 406);
|
|
foreach ($param as $key=>$val)
|
|
{
|
|
$this->sections[$sectionID][$key] = $val;
|
|
}
|
|
}
|
|
|
|
/** Get the list of sections ID
|
|
* @return array the defined sectionsID
|
|
*/
|
|
private function sectionList ()
|
|
{
|
|
return array_keys ($this->sections);
|
|
}
|
|
|
|
/** Get the section ID List with parents ID
|
|
* @return array the defined sectionsID with the parent ID as value
|
|
*/
|
|
private function sectionListParent ()
|
|
{
|
|
$res = array ();
|
|
foreach ($this->sections as $sectionID=>$content)
|
|
{
|
|
if (! array_key_exists ("_parentID", $content))
|
|
$res[$sectionID] = "";
|
|
else
|
|
$res[$sectionID] = $content["_parentID"];
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/** Return the content array of the section
|
|
* @param string $sectionID The section ID to get
|
|
* @return array The content of the section
|
|
*/
|
|
private function sectionGet ($sectionID)
|
|
{
|
|
if (! array_key_exists ($sectionID, $this->sections))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Section not found"), 404);
|
|
return $this->sections[$sectionID];
|
|
}
|
|
|
|
/** Get the section ID of the main part
|
|
* @return bool|string the section ID of the main part or FALSE if not found
|
|
*/
|
|
public function sectionMainID ()
|
|
{
|
|
foreach ($this->sectionListParent () as $sectionID=>$parentID)
|
|
{
|
|
if ($parentID === "")
|
|
return $sectionID;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Refresh the _headersEML from the _headersArray
|
|
* @param string $sectionID the section to refresh
|
|
*/
|
|
private function sectionRefreshHeadersEML ($sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
$headersEML = "";
|
|
foreach ($section["_headersArray"] as $val)
|
|
{
|
|
$head = key ($val);
|
|
$value = $val[$head];
|
|
$headersEML .= "$head: $value";
|
|
}
|
|
$this->sections[$sectionID]["_headersEML"] = $headersEML;
|
|
}
|
|
|
|
/** Read the complete mail to analyze
|
|
* Destroy all the previous definitions of mail
|
|
* @param string $content The complete mail to read
|
|
*/
|
|
public function readMail ($content)
|
|
{
|
|
$this->sections = array ();
|
|
$this->completeEmailEML = $content;
|
|
$partinfo = $this->parseMessagePart ($content);
|
|
if ($partinfo !== null)
|
|
$this->readMailContentRecurse ($partinfo);
|
|
}
|
|
|
|
/** Read the content of the mail and allow the content to be also multipart.
|
|
* Then the method is recursively called to generate the sections
|
|
* @param array $partinfo The partinfo from parseMessagePart to analyze
|
|
* @param string|null $sectionIDParent The parent sectionID to link with
|
|
*/
|
|
private function readMailContentRecurse ($partinfo, $sectionIDParent=false)
|
|
{
|
|
if (key_exists ("_contentType", $partinfo) &&
|
|
substr ($partinfo["_contentType"], 0, 10) === "multipart/")
|
|
{
|
|
// multipart/alternative, multipart/related, multipart/mixed
|
|
// Remove the content, as it is not valuable (will be stored in childs)
|
|
$tmp = $partinfo;
|
|
unset ($tmp["_contentEML"]);
|
|
unset ($tmp["_contentUTF"]);
|
|
if ($sectionIDParent === false)
|
|
$sectionIDParent = $this->sectionAdd ($tmp);
|
|
$boundaryArray = preg_split ("#([\r\n]+)#", $partinfo["_contentEML"],
|
|
null, PREG_SPLIT_DELIM_CAPTURE);
|
|
// Remove the first 2 dashes : the boundary is stored like in the headers
|
|
$boundary = "";
|
|
while (count ($boundaryArray) > 0 && substr ($boundary, 0, 2) !== "--")
|
|
{
|
|
// Skip the lines until the coundary is found. The boundary start by --
|
|
$boundary = array_shift ($boundaryArray);
|
|
$boundaryCR = array_shift ($boundaryArray);
|
|
}
|
|
if ($boundary === false)
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Can't find boundary in multipart/"), 406);
|
|
$boundary = substr ($boundary, 2);
|
|
unset ($boundaryArray);
|
|
$this->sectionUpdate ($sectionIDParent,
|
|
array ("_boundary"=>$boundary,
|
|
"_boundaryCR"=>$boundaryCR));
|
|
$parts = preg_split ("#(--".preg_quote ($boundary)."(--)?)([\r\n]+)#",
|
|
$partinfo["_contentEML"],
|
|
null, PREG_SPLIT_DELIM_CAPTURE);
|
|
for ($i = 0 ; $i < count ($parts) ; $i = $i + 4)
|
|
{
|
|
if ($parts[$i] === "")
|
|
continue;
|
|
$messagePart = $this->parseMessagePart ($parts[$i]);
|
|
if ($messagePart === null ||
|
|
! array_key_exists ("_contentType", $messagePart))
|
|
continue;
|
|
$messagePart["_parentID"] = $sectionIDParent;
|
|
$sectionIDChild = $this->sectionAdd ($messagePart);
|
|
$this->sectionAddChild ($sectionIDParent, $sectionIDChild);
|
|
if (substr ($messagePart["_contentType"], 0, 10) === "multipart/")
|
|
{
|
|
// Recursive part : multipart in multipart
|
|
unset ($this->sections[$sectionIDChild]["_contentEML"]);
|
|
unset ($this->sections[$sectionIDChild]["_contentUTF"]);
|
|
$this->readMailContentRecurse ($messagePart, $sectionIDChild);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$sectionIDParent = $this->sectionAdd ($partinfo);
|
|
}
|
|
}
|
|
|
|
/** Return the data for a part of the mail
|
|
* @param string $content The content of the mail to parse
|
|
* @return array The data content in the mail
|
|
*/
|
|
private function parseMessagePart ($content)
|
|
{
|
|
// Get the HeaderBodySeparator
|
|
$pos = strpos ($content, "\r\n\r\n");
|
|
$headerBodySeparator = "\r\n";
|
|
if ($pos === false)
|
|
{
|
|
$pos = strpos ($content, "\n\n");
|
|
$headerBodySeparator = "\n";
|
|
}
|
|
if ($pos === false)
|
|
{
|
|
$pos = strpos ($content, "\r\r");
|
|
$headerBodySeparator = "\r";
|
|
}
|
|
if ($pos === false)
|
|
//throw new \Exception ("Can't find the header/body separator", 500);
|
|
return array ("_contentEML"=>$content);
|
|
// Get the headers
|
|
$headersEML = ltrim (substr ($content, 0, $pos+
|
|
strlen ($headerBodySeparator)));
|
|
$headersArray = array ();
|
|
if ($headersEML !== "" && $headersEML !== "--")
|
|
{
|
|
$prev = "";
|
|
$prevCR = "";
|
|
$headersSplit = preg_split ("#([\r\n]+)#", $headersEML, null,
|
|
PREG_SPLIT_DELIM_CAPTURE);
|
|
for ($i = 0 ; $i < count ($headersSplit); $i = $i + 2)
|
|
{
|
|
$h = $headersSplit[$i];
|
|
if (! isset ($headersSplit[$i+1]))
|
|
$headersSplit[$i+1] = $headerBodySeparator;
|
|
if ($h === "" || $h === "--")
|
|
continue;
|
|
if ($h{0} === " " || $h{0} === "\t")
|
|
{
|
|
$prev .= $prevCR.$h;
|
|
$prevCR = $headersSplit[$i+1];
|
|
}
|
|
elseif ($prev !== "")
|
|
{
|
|
$exp = explode (": ", $prev, 2);
|
|
if (! array_key_exists (1, $exp))
|
|
{
|
|
//trigger_error ("Malformed mail provided: no section header",
|
|
// E_USER_NOTICE);
|
|
$prev = "";
|
|
continue;
|
|
}
|
|
list ($head, $value) = $exp;
|
|
$value .= $prevCR;
|
|
$headersArray[][$head] = $value;
|
|
$prev = $h;
|
|
$prevCR = $headersSplit[$i+1];
|
|
}
|
|
else
|
|
{
|
|
$prev = $h;
|
|
$prevCR = $headersSplit[$i+1];
|
|
}
|
|
}
|
|
if ($prev !== "")
|
|
{
|
|
$exp = explode (": ", $prev, 2);
|
|
if (array_key_exists (1, $exp))
|
|
{
|
|
list ($head, $value) = $exp;
|
|
$value .= $prevCR;
|
|
$headersArray[][$head] = $value;
|
|
}
|
|
}
|
|
}
|
|
$contentType = $this->getHeaderValue ("Content-Type", $headersArray);
|
|
if ($contentType === false)
|
|
{
|
|
// If the mail doesn't provide a Content-Type header, it is then a raw
|
|
// text mail. Force the Content-Type to continue;
|
|
$contentType = "text/plain; charset=utf-8; format=flowed";
|
|
}
|
|
$contentTypeArray = $this->contentTypeAnalyze ($contentType);
|
|
if (! isset ($contentTypeArray["Content-Type"]))
|
|
{
|
|
//throw new \Exception ("Can't parse the Content-Type", 500);
|
|
return;
|
|
}
|
|
$contentTransferEncoding =
|
|
$this->getHeaderValue ("Content-Transfer-Encoding",
|
|
$headersArray);
|
|
$charset = (isset ($contentTypeArray["charset"])) ?
|
|
$contentTypeArray["charset"] : false;
|
|
// Get the body
|
|
$pos = strpos ($content, $headerBodySeparator.$headerBodySeparator);
|
|
$contentEML = substr ($content, $pos + strlen ($headerBodySeparator) * 2);
|
|
if ($contentTransferEncoding === "quoted-printable")
|
|
$contentUTF = $this->encodingDecode ($contentEML,
|
|
$contentTransferEncoding);
|
|
elseif ($contentTransferEncoding === "base64")
|
|
$contentUTF = $this->encodingDecode ($contentEML,
|
|
$contentTransferEncoding);
|
|
else
|
|
$contentUTF = $contentEML;
|
|
if ($charset !== false)
|
|
$contentUTF = iconv ($charset, "utf-8", $contentUTF);
|
|
$res = array ("_headerBodySeparator"=>$headerBodySeparator,
|
|
"_headerCR"=>$prevCR,
|
|
"_headersEML"=>$headersEML,
|
|
"_headersArray"=>$headersArray,
|
|
"Content-Type"=>$contentType,
|
|
"_contentType"=>$contentTypeArray["Content-Type"],
|
|
"Content-Transfer-Encoding"=>$contentTransferEncoding,
|
|
"_contentEML"=>$contentEML,
|
|
"_contentUTF"=>$contentUTF);
|
|
preg_match ("#; name=['\"](.+)['\"]#", $contentType, $matches);
|
|
if (array_key_exists (1, $matches))
|
|
$res["_name"] = $matches[1];
|
|
if ($charset !== false)
|
|
$res["_charset"] = $charset;
|
|
$contentID = $this->getHeaderValue ("Content-ID", $headersArray);
|
|
if ($contentID !== false)
|
|
$res["Content-ID"] = $contentID;
|
|
return $res;
|
|
}
|
|
|
|
/** The constuctor verify if the external libraries are available
|
|
*/
|
|
public function __construct ()
|
|
{
|
|
if (! function_exists ("finfo_buffer"))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Missing FileInfo PHP Extension"), 500);
|
|
if (! function_exists ("openssl_random_pseudo_bytes"))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Missing OpenSSL PHP Extension"), 500);
|
|
// Define default headers
|
|
$this->addHeader ("Date", date ("r"));
|
|
$this->addHeader ("Message-ID", $this->provideMessageID ());
|
|
$user = posix_getpwuid (posix_geteuid());
|
|
$this->addHeader ("From", $user["name"]."@".php_uname('n'));
|
|
$this->addHeader ("MIME-Version", "1.0");
|
|
}
|
|
|
|
/** Define a HTML body. If the HTML body already exists, overwrite it
|
|
* If there is an text body, manage the boundary in alternative mode
|
|
* @param string $htmlContent in UTF-8
|
|
* @param string $charset to be stored in the mail
|
|
* @param string $encoding the encoding in the mail
|
|
*/
|
|
public function setBodyHTML ($htmlContent, $charset="utf-8",
|
|
$encoding="quoted-printable")
|
|
{
|
|
// Look if there is an existing section with text (main or
|
|
// multipart/alternative)
|
|
$sectionList = $this->sectionList ();
|
|
$sectionIDtoChange = "";
|
|
foreach ($sectionList as $sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
if (array_key_exists ("_contentType", $section) &&
|
|
$section["_contentType"] === "text/html")
|
|
{
|
|
$sectionIDtoChange = $sectionID;
|
|
break;
|
|
}
|
|
}
|
|
if ($sectionIDtoChange === "")
|
|
{
|
|
// No existing HTML section found : need to create one
|
|
// Check if the main section is empty to use it
|
|
$sectionMainID = $this->sectionMainID ();
|
|
if (!array_key_exists ("_contentType",
|
|
$this->sectionGet ($sectionMainID)))
|
|
{
|
|
$sectionIDtoChange = $sectionMainID;
|
|
}
|
|
}
|
|
if ($sectionIDtoChange === "")
|
|
{
|
|
// No existing HTML section found : need to create one
|
|
// Check if there is a multipart/related section without HTML to use
|
|
$sectionList = $this->sectionList ();
|
|
foreach ($sectionList as $sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
if (! array_key_exists ("_contentType", $section) ||
|
|
array_key_exists ("_contentType", $section) &&
|
|
$section["_contentType"] !== "multipart/related")
|
|
continue;
|
|
// Found ! $sectionID is a multipart/related section without HTML
|
|
$sectionIDtoChange = $this->sectionAddDefault ();
|
|
$this->sectionAddChildFirst ($sectionID, $sectionIDtoChange);
|
|
}
|
|
}
|
|
if ($sectionIDtoChange === "")
|
|
{
|
|
// No existing HTML section found : need to create one
|
|
// Check if there is an text part alone (to be pushed in
|
|
// multipart/alternative)
|
|
foreach ($sectionList as $sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
if (! array_key_exists ("_contentType", $section) ||
|
|
$section["_contentType"] !== "text/plain")
|
|
continue;
|
|
// Found ! $sectionID is text/plain section
|
|
// Create a multipart/alternative section
|
|
// Move the text section in the multipart/alternative section
|
|
$boundary = $this->getBoundary ();
|
|
$multiID = $this->sectionAddDefault();
|
|
$this->moveChilds ($multiID);
|
|
$this->addHeader ("Content-Type", "multipart/alternative;\r\n".
|
|
" boundary=$boundary\r\n",
|
|
$multiID);
|
|
$this->sectionUpdate ($multiID,
|
|
array ("_contentType"=>"multipart/alternative",
|
|
"_boundary"=>$boundary,
|
|
"_boundaryCR"=>"\r\n",
|
|
));
|
|
|
|
// Add a HTML section in the multipart/alternative section after the
|
|
// defined text section.
|
|
$sectionIDtoChange = $this->sectionAddDefault ();
|
|
$this->sectionAddChild ($multiID, $sectionIDtoChange);
|
|
}
|
|
}
|
|
if ($sectionIDtoChange === "")
|
|
{
|
|
// No existing section found : need to create one
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Can't find the place to store the HTML"), 500);
|
|
}
|
|
$htmlContent = iconv ("utf-8", $charset, $htmlContent);
|
|
$part["_charset"] = $charset;
|
|
$part["_contentType"] = "text/html";
|
|
$this->setHeader ("Content-Transfer-Encoding", $encoding,
|
|
$sectionIDtoChange);
|
|
// TODO : Add $part["_contentUTF"] ?
|
|
$part["_contentEML"] = $this->encodingEncode ($htmlContent, $encoding)."\n";
|
|
$this->setHeader ("Content-Type", "text/html; charset=$charset",
|
|
$sectionIDtoChange);
|
|
if (isset ($boundary)) $part["_boundary"] = $boundary;
|
|
if (isset ($boundaryCR)) $part["_boundaryCR"] = $boundaryCR;
|
|
$this->sectionUpdate ($sectionIDtoChange, $part);
|
|
$this->createMailEML ();
|
|
}
|
|
|
|
/** Add a Text body. If the text body already exists, overwrite it
|
|
* If there is an HTML body, manage the boundary in alternative mode
|
|
* @param string $textContent in UTF-8
|
|
* @param string $charset to be stored in the mail
|
|
* @param string $encoding the encoding in the mail
|
|
*/
|
|
public function setBodyText ($textContent, $charset="utf-8",
|
|
$encoding="quoted-printable")
|
|
{
|
|
// Look if there is an existing section with text (main or
|
|
// multipart/alternative)
|
|
$sectionList = $this->sectionList ();
|
|
$sectionIDtoChange = "";
|
|
foreach ($sectionList as $sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
if (array_key_exists ("_contentType", $section) &&
|
|
$section["_contentType"] === "text/plain")
|
|
{
|
|
$sectionIDtoChange = $sectionID;
|
|
break;
|
|
}
|
|
}
|
|
if ($sectionIDtoChange === "")
|
|
{
|
|
// No existing section found : need to create one
|
|
// Check if the main section is empty to use it
|
|
$sectionMainID = $this->sectionMainID ();
|
|
if (!array_key_exists ("_contentType",
|
|
$this->sectionGet ($sectionMainID)))
|
|
{
|
|
$sectionIDtoChange = $sectionMainID;
|
|
}
|
|
}
|
|
if ($sectionIDtoChange === "")
|
|
{
|
|
// No existing section found : need to create one
|
|
// Check if there is an html part alone (to be pushed in
|
|
// multipart/alternative)
|
|
foreach ($sectionList as $sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
if (! array_key_exists ("_contentType", $section) ||
|
|
$section["_contentType"] !== "text/html")
|
|
continue;
|
|
$sectionIDhtml = $sectionID;
|
|
// Found ! $sectionID is text/html section
|
|
// Create a multipart/alternative section
|
|
// Move the text section in the multipart/alternative section
|
|
$boundary = $this->getBoundary ();
|
|
$multiID = $this->sectionAddDefault();
|
|
$this->moveChilds ($multiID);
|
|
$this->addHeader ("Content-Type", "multipart/alternative;\r\n".
|
|
" boundary=$boundary\r\n",
|
|
$multiID);
|
|
$this->sectionUpdate ($multiID,
|
|
array ("_contentType"=>"multipart/alternative",
|
|
"_boundary"=>$boundary,
|
|
"_boundaryCR"=>"\r\n",
|
|
));
|
|
// Add a HTML section in the multipart/alternative section
|
|
$sectionIDtoChange = $this->sectionAddDefault ();
|
|
$this->sectionAddChildFirst ($multiID, $sectionIDtoChange);
|
|
}
|
|
}
|
|
if ($sectionIDtoChange === "")
|
|
{
|
|
// No existing section found : need to create one
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Can't find the place to store the TEXT"), 500);
|
|
}
|
|
$textContent = iconv ("utf-8", $charset, $textContent);
|
|
$part["_charset"] = $charset;
|
|
$part["_contentType"] = "text/plain";
|
|
$this->setHeader ("Content-Transfer-Encoding", $encoding,
|
|
$sectionIDtoChange);
|
|
$part["_contentEML"] = $this->encodingEncode ($textContent, $encoding)."\n";
|
|
$part["_contentUTF"] = $textContent;
|
|
$this->setHeader ("Content-Type", "text/plain; charset=$charset",
|
|
$sectionIDtoChange);
|
|
if (isset ($boundary)) $part["_boundary"] = $boundary;
|
|
if (isset ($boundaryCR)) $part["_boundaryCR"] = $boundaryCR;
|
|
$this->sectionUpdate ($sectionIDtoChange, $part);
|
|
$this->createMailEML ();
|
|
}
|
|
|
|
/** Return the HTML body if exists in UTF-8. If the body is not in UTF-8, it
|
|
* is converted
|
|
* Return false if it doesn't exists
|
|
* @return string|false The HTML body converted in UTF-8 or false if there is
|
|
* no HTML part in the mail
|
|
*/
|
|
public function getBodyHTML ()
|
|
{
|
|
$sectionList = $this->sectionList ();
|
|
$sectionIDtoChange = "";
|
|
foreach ($sectionList as $sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
if (array_key_exists ("_contentType", $section) &&
|
|
$section["_contentType"] === "text/html")
|
|
{
|
|
$encoding = $this->getHeaderValue ("Content-Transfer-Encoding",
|
|
$section["_headersArray"]);
|
|
$body = $this->encodingDecode ($section["_contentEML"],
|
|
$encoding);
|
|
return iconv ($section["_charset"], "UTF-8", $body);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Get the text body if exists in UTF-8. If the body is not in UTF-8, it is
|
|
* converted
|
|
* Return false if it doesn't exists
|
|
* @return string|false The Text body converted in UTF-8 or false if there is
|
|
* no Text part in the mail
|
|
*/
|
|
public function getBodyText ()
|
|
{
|
|
$sectionList = $this->sectionList ();
|
|
$sectionIDtoChange = "";
|
|
foreach ($sectionList as $sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
if (array_key_exists ("_contentType", $section) &&
|
|
$section["_contentType"] === "text/plain")
|
|
{
|
|
$encoding = $this->getHeaderValue ("Content-Transfer-Encoding",
|
|
$section["_headersArray"]);
|
|
$body = $this->encodingDecode ($section["_contentEML"],
|
|
$encoding);
|
|
return iconv ($section["_charset"], "UTF-8", $body);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Move the sections where the parent is defined to $oldParentID to the
|
|
* $newParentID.
|
|
* Move the headers from the valid sections to the newParent, except the
|
|
* Content-XX lines
|
|
* @param string $newParentID The Parent which will be modified
|
|
* @param string|null $oldParentID The oldParent to look for
|
|
*/
|
|
private function moveChilds ($newParentID, $oldParentID=false)
|
|
{
|
|
if ($newParentID === $oldParentID)
|
|
throw new \Exception ("moveChilds : old and new ParentID are the same",
|
|
406);
|
|
foreach ($this->sections as $sectionID=>$section)
|
|
{
|
|
if ($newParentID === $sectionID)
|
|
continue;
|
|
if ((! isset ($section["_parentID"]) && $oldParentID === false) ||
|
|
$oldParentID === $section["_parentID"])
|
|
{
|
|
$this->sections[$sectionID]["_parentID"] = $newParentID;
|
|
$this->sections[$newParentID]["_partsIDchild"][] = $sectionID;
|
|
$multipartHeaders = array ();
|
|
$headersEML = "";
|
|
foreach ($section["_headersArray"] as $key=>$val)
|
|
{
|
|
$head = key ($val);
|
|
$value = $val[$head];
|
|
if (substr ($head, 0, 8) !== "Content-")
|
|
{
|
|
$this->addHeader ($head, $value, $newParentID);
|
|
try
|
|
{
|
|
// Removing the Received: headers entries can only be done one
|
|
// time. An exception is raised, but it is not important
|
|
$this->delHeader ($head, $sectionID);
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Section not attached to the wanted parent : skip it
|
|
}
|
|
}
|
|
|
|
// Remove empty sections
|
|
foreach ($this->sections as $sectionID=>$section)
|
|
{
|
|
if (count ($section["_headersArray"]) === 0 &&
|
|
rtrim ($section["_headersEML"]) === "" &&
|
|
rtrim ($section["_contentEML"]) === "")
|
|
{
|
|
if (array_key_exists ("_parentID", $section))
|
|
{
|
|
foreach ($this->sections[$section["_parentID"]]["_partsIDchild"] as
|
|
$key=>$val)
|
|
if ($val === $sectionID)
|
|
unset ($this->sections[$section["_parentID"]]["_partsIDchild"][$key]
|
|
);
|
|
}
|
|
unset ($this->sections[$sectionID]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Add an attachment to the mail.
|
|
* The allowed encodings are "quoted-printable" or "base64"
|
|
* @param string $name The name of the file
|
|
* @param string $fileContent The content of the file in binary
|
|
* @param string|null $encoding The output encoding. Can be
|
|
* base64/quoted-printable
|
|
* @param boolean|null $inline Store the file in inline mode
|
|
* (multipart/related)
|
|
* If false, store the file in attached mode (multipart/mixed)
|
|
*/
|
|
public function addAttachment ($name, $fileContent, $encoding="base64",
|
|
$inline=false)
|
|
{
|
|
if ($this->getBodyHTML() === false && $inline !== false)
|
|
$this->setBodyHTML ("No HTML provided by inline file added");
|
|
// Look if there is a multipart/mixed where adding the new section. If not,
|
|
// create it
|
|
$multipart = ($inline === false) ? "multipart/mixed" : "multipart/related";
|
|
$sectionList = $this->sectionList ();
|
|
$sectionIDMixed = "";
|
|
foreach ($sectionList as $sectionID)
|
|
{
|
|
$section = $this->sectionGet ($sectionID);
|
|
if (!array_key_exists ("_contentType", $section) ||
|
|
array_key_exists ("_contentType", $section) &&
|
|
$section["_contentType"] !== $multipart)
|
|
continue;
|
|
// Found a multipart/mixed. Will add the new attachment section to it
|
|
$sectionIDMixed = $sectionID;
|
|
}
|
|
if ($sectionIDMixed === "")
|
|
{
|
|
// No multipart/mixed. Need to create one
|
|
$headersEML = "";
|
|
$sectionIDMixed = $this->sectionAddDefault ();
|
|
// Move all the existing sections in the new multipart/mixed
|
|
$this->moveChilds ($sectionIDMixed);
|
|
$boundary = $this->getBoundary ();
|
|
$this->addHeader ("Content-Type", "$multipart;\r\n".
|
|
" boundary=$boundary\r\n",
|
|
$sectionIDMixed);
|
|
$this->sectionUpdate ($sectionIDMixed,
|
|
array ("_contentType"=>$multipart,
|
|
"_boundary"=>$boundary,
|
|
"_boundaryCR"=>"\r\n",
|
|
));
|
|
}
|
|
if ($sectionIDMixed === "")
|
|
{
|
|
throw new \Exception (
|
|
"Can't find the multipart/mixed section to add an attachment");
|
|
}
|
|
// Add the new section to the mixed section
|
|
$sectionID = $this->sectionAddDefault ();
|
|
$this->sectionAddChild ($sectionIDMixed, $sectionID);
|
|
$finfo = new \finfo(FILEINFO_MIME);
|
|
$mimetype = $finfo->buffer($fileContent);
|
|
$this->addHeader ("Content-Type", "$mimetype; name=\"".
|
|
str_replace ("\"", "=22",
|
|
$this->encodeHeaderStringWithPosition ($name,
|
|
"quoted-printable",
|
|
strlen ("Content-Type: $mimetype; name=")))."\"\r\n",
|
|
$sectionID);
|
|
if ($inline === false)
|
|
$this->addHeader ("Content-Disposition", "attachment; filename=\"".
|
|
str_replace ("\"", "=22",
|
|
$this->encodeHeaderStringWithPosition ($name,
|
|
"quoted-printable",
|
|
strlen ("Content-Disposition: attachment; ".
|
|
"filename="))).
|
|
"\"\r\n",
|
|
$sectionID);
|
|
else
|
|
{
|
|
$this->addHeader ("Content-Disposition", "inline; filename=".
|
|
str_replace ("\"", "=22",
|
|
$this->encodeHeaderStringWithPosition ($name,
|
|
"quoted-printable",
|
|
strlen ("Content-Disposition: inline; filename=")
|
|
))."\r\n",
|
|
$sectionID);
|
|
$contentID = $this->provideMessageID ();
|
|
$this->addHeader ("Content-ID", "$contentID\r\n", $sectionID);
|
|
}
|
|
$this->addHeader ("Content-Transfer-Encoding", "$encoding\r\n", $sectionID);
|
|
$part["_name"] = $name;
|
|
$part["_contentEML"] = $this->encodingEncode ($fileContent, $encoding);
|
|
$part["_mimetype"] = $mimetype;
|
|
$part["_sizeReal"] = strlen ($fileContent);
|
|
$this->sectionUpdate ($sectionID, $part);
|
|
$this->createMailEML ();
|
|
if ($inline === true)
|
|
return substr ($contentID, 1, -1);
|
|
}
|
|
|
|
/** Add an inline attachment to the mail.
|
|
* The allowed encodings are "quoted-printable" or "base64"
|
|
* Return the Content-ID needed to be used in HTML page like :
|
|
* <img src='cid:XXXXXXXXXX'/>
|
|
* @param string $name The name of the file
|
|
* @param string $fileContent The content of the file in binary
|
|
* @param string|null $encoding The output encoding. Can be
|
|
* base64/quoted-printable
|
|
* @return string The content ID created
|
|
*/
|
|
public function addAttachmentInline ($name, $fileContent, $encoding="base64")
|
|
{
|
|
return $this->addAttachment ($name, $fileContent, $encoding, true);
|
|
}
|
|
|
|
/** Get an attachment of the mail
|
|
* @param integer $number the number of attach to get starting to 0
|
|
* @param boolean|null $inline Return only the attachments Inline if true
|
|
* @return the content of the attachment. Can be binary
|
|
*/
|
|
public function getAttachment ($number, $inline = false)
|
|
{
|
|
$attachmentIDs = $this->getAttachmentID ($inline);
|
|
if (! array_key_exists ($number, $attachmentIDs))
|
|
throw new \Exception (sprintf (dgettext ("domframework",
|
|
"Attachment '%d' not found"), $number), 404);
|
|
$part = $this->sectionGet ($attachmentIDs[$number]);
|
|
$encoding = $this->getHeaderValue ("Content-Transfer-Encoding",
|
|
$part["_headersArray"]);
|
|
return $this->encodingDecode ($part["_contentEML"], $encoding);
|
|
}
|
|
|
|
/** Get the attachment details
|
|
* @param integer $number the number of attach to get starting to 0
|
|
* @param boolean|null $inline Return only the attachments Inline if true
|
|
* @return array containing the information of the attachment
|
|
*/
|
|
public function getAttachmentDetails ($number, $inline = false)
|
|
{
|
|
$attachmentIDs = $this->getAttachmentID ($inline);
|
|
if (! array_key_exists ($number, $attachmentIDs))
|
|
throw new \Exception (sprintf (dgettext ("domframework",
|
|
"Attachment '%d' not found"), $number), 404);
|
|
$part = $this->sectionGet ($attachmentIDs[$number]);
|
|
foreach ($part as $key=>$val)
|
|
{
|
|
if ($key{0} === "_")
|
|
{
|
|
if ($key !== "_contentEML" && $key !== "_contentUTF" &&
|
|
$key !== "_parentID" && $key !== "_headersArray" &&
|
|
$key !== "_headersEML" && $key !== "_headerCR" &&
|
|
$key !== "_headerBodySeparator")
|
|
$res[substr ($key, 1)] = $val;
|
|
}
|
|
else
|
|
$res[$key] = $val;
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/** Return the list of the sectionID containing a attachment. Contains the
|
|
* inline attachments too.
|
|
* @param boolean $inline Return only the sections Inline if true
|
|
* @return array The sectionIDs
|
|
*/
|
|
private function getAttachmentID ($inline = false)
|
|
{
|
|
$res = array ();
|
|
foreach ($this->sections as $sectionID=>$section)
|
|
{
|
|
if (! key_exists ("_headersArray", $section))
|
|
continue;
|
|
$disposition = $this->getHeaderValue ("Content-Disposition",
|
|
$section["_headersArray"]);
|
|
if ($disposition !== false)
|
|
{
|
|
if ($inline === true && substr ($disposition, 0, 6) === "inline")
|
|
$res[] = $sectionID;
|
|
elseif ($inline === false &&
|
|
substr ($disposition, 0, 10) === "attachment")
|
|
$res[] = $sectionID;
|
|
continue;
|
|
}
|
|
// The Mailer-Daemons use Content-Type: message/XXXX
|
|
$contentType = $this->getHeaderValue ("Content-Type",
|
|
$section["_headersArray"]);
|
|
if ($contentType !== false && substr ($contentType, 0, 8) === "message/")
|
|
{
|
|
if ($inline === false)
|
|
$res[] = $sectionID;
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/** Add a To: header. If it already exists, add a new recipient
|
|
* @param string $toMail The mail to add
|
|
* @param string|null $toName The name of the recipient
|
|
*/
|
|
public function addTo ($toMail, $toName = "")
|
|
{
|
|
if (strspn ($toName, "abcdefghijklmnopqrstuvwxyz".
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".
|
|
"0123456789 -_") !== strlen ($toName))
|
|
$toName = $this->encodeHeaders ("To", $toName,
|
|
"quoted-printable");
|
|
if ($toName !== "")
|
|
$toName .= " ";
|
|
$toField = "$toName<$toMail>";
|
|
$oldTo = $this->getTo ();
|
|
if ($oldTo !== false)
|
|
$this->setHeader ("To", rtrim ($oldTo).",\r\n $toField");
|
|
else
|
|
$this->setHeader ("To", $toField);
|
|
}
|
|
|
|
/** Get the To Header as it is written in the mail
|
|
* @return string The To Header defined in the mail
|
|
*/
|
|
public function getTo ()
|
|
{
|
|
return $this->getHeader ("To");
|
|
}
|
|
|
|
/** Add a From: header. If it already exists, overwrite the existing one
|
|
* @param string $fromMail The from Mail to define
|
|
* @param string|null $fromName The from Name to define
|
|
*/
|
|
public function setFrom ($fromMail, $fromName= "")
|
|
{
|
|
if (strspn ($fromName, "abcdefghijklmnopqrstuvwxyz".
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".
|
|
"0123456789 -_") !== strlen ($fromName))
|
|
$fromName = $this->encodeHeaders ("From", $fromName,
|
|
"quoted-printable");
|
|
if ($fromName !== "")
|
|
$fromName .= " ";
|
|
$this->setHeader ("From", "$fromName<$fromMail>");
|
|
}
|
|
|
|
/** Return the From header as it is written in the mail
|
|
* @return string The From Header defined in the mail
|
|
*/
|
|
public function getFrom ()
|
|
{
|
|
return $this->getHeader ("From");
|
|
}
|
|
|
|
/** Return the From header converted to array with mail and name keys
|
|
* @return array The From details
|
|
*/
|
|
public function getFromArray ()
|
|
{
|
|
$from = $this->getHeader ("From");
|
|
$res = array ();
|
|
$from = $this->decodeHeaders ("From", $from);
|
|
$from = $this->convertPeopleToArray ($from);
|
|
return reset ($from);
|
|
}
|
|
|
|
/** Set the subject
|
|
* @param string $subject In UTF8
|
|
*/
|
|
public function setSubject ($subject)
|
|
{
|
|
$this->setHeader ("Subject",
|
|
$this->encodeHeaders ("Subject", $subject,
|
|
"quoted-printable"));
|
|
}
|
|
|
|
/** Set the Date
|
|
* @param string $date In RFC 2822 format
|
|
*/
|
|
public function setDate ($date)
|
|
{
|
|
// TODO : Check if the date format is valid
|
|
$this->setHeader ("Date", $date);
|
|
}
|
|
|
|
/** Set the Date
|
|
* @param string $timestamp In Timestamp format
|
|
*/
|
|
public function setDateTimestamp ($timestamp)
|
|
{
|
|
// TODO : Check if the timestamp is valid
|
|
$this->setHeader ("Date", date ("r", $timestamp));
|
|
}
|
|
|
|
/** Get the Date header if defined.
|
|
* Return false if not defined
|
|
* @return string|bool The date Header if defined or false if not defined
|
|
*/
|
|
public function getDate ()
|
|
{
|
|
return $this->getHeader ("Date");
|
|
}
|
|
|
|
/** Return the Date header (if defined) in timestamp
|
|
* Return false if not defined
|
|
* @return integer|bool The date Header if defined or false if not defined
|
|
*/
|
|
public function getDateTimestamp ()
|
|
{
|
|
$datetimestamp = false;
|
|
$date = rtrim ($this->getDate ());
|
|
if ($date !== false)
|
|
{
|
|
$dateTimestamp = \DateTime::createFromFormat (\DateTime::RFC2822,
|
|
$date);
|
|
if ($dateTimestamp === false)
|
|
$dateTimestamp = \DateTime::createFromFormat (\DateTime::RFC822,
|
|
$date);
|
|
if ($dateTimestamp !== false)
|
|
$dateTimestamp = $dateTimestamp->getTimestamp();
|
|
}
|
|
return $dateTimestamp;
|
|
}
|
|
|
|
/** Set a generic header
|
|
* @param string $header The name of the Header (without colon)
|
|
* @param string $value The value the store. The format must be correct !
|
|
* @param string|null $sectionID The section to modify. If null, use the main
|
|
*/
|
|
public function setHeader ($header, $value, $sectionID=null)
|
|
{
|
|
if (substr ($value, -1) !== "\n" &&
|
|
substr ($value, -1) !== "\r" &&
|
|
substr ($value, -2) !== "\r\n")
|
|
$value .= "\r\n";
|
|
if ($sectionID === null)
|
|
{
|
|
$sectionMainID = $this->sectionMainID ();
|
|
if ($sectionMainID === false)
|
|
$sectionMainID = $this->sectionAddDefault ();
|
|
}
|
|
else
|
|
{
|
|
if (! array_key_exists ($sectionID, $this->sections))
|
|
throw new \Exception ("Wanted section not found in setHeader", 404);
|
|
$sectionMainID = $sectionID;
|
|
}
|
|
|
|
$found = false;
|
|
foreach ($this->sections[$sectionMainID]["_headersArray"] as $key=>$val)
|
|
{
|
|
$head = key ($val);
|
|
if ($head === $header)
|
|
{
|
|
$this->sections[$sectionMainID]["_headersArray"][$key][$header] =
|
|
$value;
|
|
$found = true;
|
|
}
|
|
}
|
|
if ($found === false)
|
|
$this->sections[$sectionMainID]["_headersArray"][][$header] = $value;
|
|
// Re-create the _headersEML for the section
|
|
$_headerEML = "";
|
|
foreach ($this->sections[$sectionMainID]["_headersArray"] as $val)
|
|
{
|
|
$head = key ($val);
|
|
$value = $val[$head];
|
|
$_headerEML .= "$head: $value";
|
|
}
|
|
$this->sections[$sectionMainID]["_headersEML"] = $_headerEML;
|
|
$this->createMailEML ();
|
|
}
|
|
|
|
/** Add a generic header
|
|
* @param string $header The name of the Header (without colon)
|
|
* @param string $value The value the store. The format must be correct !
|
|
* @param string|null $sectionID The section to modify. If null, use the main
|
|
*/
|
|
public function addHeader ($header, $value, $sectionID=null)
|
|
{
|
|
if (substr ($value, -1) !== "\n" &&
|
|
substr ($value, -1) !== "\r" &&
|
|
substr ($value, -2) !== "\r\n")
|
|
$value .= "\r\n";
|
|
if ($sectionID === null)
|
|
{
|
|
$sectionMainID = $this->sectionMainID ();
|
|
if ($sectionMainID === false)
|
|
$sectionMainID = $this->sectionAddDefault ();
|
|
}
|
|
else
|
|
{
|
|
if (! array_key_exists ($sectionID, $this->sections))
|
|
throw new \Exception ("Wanted section not found in addHeader", 404);
|
|
$sectionMainID = $sectionID;
|
|
}
|
|
if ($header === "Return-Path")
|
|
{
|
|
// Must be placed on the first line and must be unique
|
|
reset ($this->sections[$sectionMainID]["_headersArray"]);
|
|
if (key_exists (0, $this->sections[$sectionMainID]["_headersArray"]) &&
|
|
key_exists ("Return-Path",
|
|
$this->sections[$sectionMainID]["_headersArray"][0]))
|
|
{
|
|
// Remove the old one
|
|
array_shift ($this->sections[$sectionMainID]["_headersArray"]);
|
|
$this->sections[$sectionMainID]["_headersEML"] =
|
|
substr ($this->sections[$sectionMainID]["_headersEML"],
|
|
strpos ($this->sections[$sectionMainID]["_headersEML"], "\n") +1);
|
|
}
|
|
array_unshift ($this->sections[$sectionMainID]["_headersArray"],
|
|
array ($header => $value));
|
|
$this->sections[$sectionMainID]["_headersEML"] = "$header: $value".
|
|
$this->sections[$sectionMainID]["_headersEML"];
|
|
}
|
|
else
|
|
{
|
|
// The "normal" headers are places sequentially at the end of the headers
|
|
$this->sections[$sectionMainID]["_headersArray"][][$header] = $value;
|
|
// TODO : Encode ? Strip ?
|
|
$this->sections[$sectionMainID]["_headersEML"] .= "$header: $value";
|
|
}
|
|
$this->createMailEML ();
|
|
}
|
|
|
|
/** Delete a specific header
|
|
* @param string $header The header to remove
|
|
* @param string|null $sectionID The section to modify. If null, use the main
|
|
*/
|
|
public function delHeader ($header, $sectionID=null)
|
|
{
|
|
if ($sectionID === null)
|
|
{
|
|
$sectionMainID = $this->sectionMainID ();
|
|
if ($sectionMainID === false)
|
|
$sectionMainID = $this->sectionAddDefault ();
|
|
}
|
|
else
|
|
{
|
|
if (! array_key_exists ($sectionID, $this->sections))
|
|
throw new \Exception ("Wanted section not found in delHeader", 404);
|
|
$sectionMainID = $sectionID;
|
|
}
|
|
$found = false;
|
|
foreach ($this->sections[$sectionMainID]["_headersArray"] as $key=>$val)
|
|
{
|
|
$head = key ($val);
|
|
if ($head === $header)
|
|
{
|
|
unset ($this->sections[$sectionMainID]["_headersArray"][$key]);
|
|
$found = true;
|
|
}
|
|
}
|
|
if ($found === false)
|
|
throw new \Exception (sprintf ("Header to remove '%s' not found",
|
|
$header), 404);
|
|
// Re-create the _headersEML for the section
|
|
$_headerEML = "";
|
|
foreach ($this->sections[$sectionMainID]["_headersArray"] as $val)
|
|
{
|
|
$head = key ($val);
|
|
$value = $val[$head];
|
|
$_headerEML .= "$head: $value";
|
|
}
|
|
$this->sections[$sectionMainID]["_headersEML"] = $_headerEML;
|
|
$this->createMailEML ();
|
|
}
|
|
|
|
/** Get a generic header
|
|
* If there is multiple headers with the same name, return the first
|
|
* @param string $header The header to get
|
|
* @param array|null $headers Optional headers to examine
|
|
* @return string|bool the literal value or false if it doesn't exist
|
|
*/
|
|
public function getHeader ($header, $headers=null)
|
|
{
|
|
if ($headers === null)
|
|
{
|
|
$sectionMainID = $this->sectionMainID ();
|
|
if ($sectionMainID === false ||
|
|
! key_exists ("_headersArray", $this->sections[$sectionMainID]))
|
|
$headers = array ();
|
|
else
|
|
$headers = $this->sections[$sectionMainID]["_headersArray"];
|
|
}
|
|
foreach ($headers as $key=>$val)
|
|
{
|
|
$head = key ($val);
|
|
$value = $val[$head];
|
|
if ($head === $header)
|
|
return $value;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Get a generic header with removing the carriage return
|
|
* If there is multiple headers with the same name, return the first
|
|
* @param string $header The header to get
|
|
* @param array $headers The _headersArray array
|
|
* @return string|bool the literal value or false if it doesn't exist
|
|
*/
|
|
public function getHeaderValue ($header, $headers=null)
|
|
{
|
|
if ($headers === null)
|
|
{
|
|
$sectionMainID = $this->sectionMainID ();
|
|
if ($sectionMainID === false ||
|
|
! key_exists ("_headersArray", $this->sections[$sectionMainID]))
|
|
$headers = array ();
|
|
else
|
|
$headers = $this->sections[$sectionMainID]["_headersArray"];
|
|
}
|
|
foreach ($headers as $key=>$val)
|
|
{
|
|
$head = key ($val);
|
|
$value = $val[$head];
|
|
if ($head === $header)
|
|
{
|
|
$value = preg_replace ("#[\r\n]+[ \t]+#", " ", $value);
|
|
if (substr ($value, -2) === "\r\n")
|
|
return substr ($value, 0, -2);
|
|
return substr ($value, 0, -1);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Create the complete mail structure
|
|
*/
|
|
public function createMailEML ()
|
|
{
|
|
$complete = "";
|
|
$this->recurse = 0;
|
|
foreach ($this->sectionListParent() as $sectionID=>$sectionParent)
|
|
{
|
|
if ($sectionParent !== "")
|
|
continue;
|
|
$part = $this->sectionGet ($sectionID);
|
|
$complete .= $part["_headersEML"];
|
|
$complete .= $part["_headerBodySeparator"];
|
|
if (array_key_exists ("_contentEML", $part))
|
|
$complete.= $part["_contentEML"];
|
|
$complete .= $this->createMailEMLSub ($part);
|
|
}
|
|
$this->completeEmailEML = $complete;
|
|
}
|
|
|
|
/** Recursive email EML creation for childs
|
|
* @param array $parent The parent array
|
|
*/
|
|
private function createMailEMLSub ($parent)
|
|
{
|
|
$this->recurse++;
|
|
if ($this->recurse > 120)
|
|
throw new \Exception ("Recurse createMailEMLSub > 120", 500);
|
|
if (!array_key_exists ("_partsIDchild", $parent))
|
|
return "";
|
|
$complete = "";
|
|
foreach ($parent["_partsIDchild"] as $childID)
|
|
{
|
|
$child = $this->sectionGet ($childID);
|
|
// The boundary is not defined in the child, but in the parent
|
|
if (array_key_exists ("_boundary", $parent))
|
|
$complete .= "--".$parent["_boundary"].$parent["_boundaryCR"];
|
|
if (array_key_exists ("_headersEML", $child))
|
|
$complete .= $child["_headersEML"];
|
|
if (array_key_exists ("_headerBodySeparator", $child))
|
|
$complete .= $child["_headerBodySeparator"];
|
|
$complete .= $this->createMailEMLSub ($child);
|
|
if (array_key_exists ("_contentEML", $child))
|
|
$complete.= $child["_contentEML"];
|
|
}
|
|
if (array_key_exists ("_boundary", $parent))
|
|
$complete .= "--".$parent["_boundary"]."--".$parent["_boundaryCR"];
|
|
return $complete;
|
|
}
|
|
|
|
/** Return the complete mail
|
|
* @return string The complete mail
|
|
*/
|
|
public function getMail ()
|
|
{
|
|
if (trim ($this->getBodyHTML()) === "No HTML provided by inline file added")
|
|
throw new \Exception ("No HTML provided by inline file added", 500);
|
|
|
|
return $this->completeEmailEML;
|
|
}
|
|
|
|
/** Return an array with the details of the mail :
|
|
* the number of attachments, the from, to, subject in UTF-8, if there is
|
|
* a text and/or html body
|
|
* @return array The details of the mail
|
|
*/
|
|
public function getDetails ()
|
|
{
|
|
$bodyTextExists = false;
|
|
$bodyHTMLExists = false;
|
|
$attachmentNb = count ($this->getAttachmentID ());
|
|
$attachmentInlineNb = count ($this->getAttachmentID (true));
|
|
foreach ($this->sections as $sectionID=>$section)
|
|
{
|
|
if (array_key_exists ("_contentType", $section) &&
|
|
$section["_contentType"] === "text/plain")
|
|
$bodyTextExists = true;
|
|
if (array_key_exists ("_contentType", $section) &&
|
|
$section["_contentType"] === "text/html")
|
|
$bodyHTMLExists = true;
|
|
}
|
|
unset ($sectionID);
|
|
unset ($section);
|
|
$attachmentDetails = array ();
|
|
$attachmentInlineDetails = array ();
|
|
for ($i = 0 ; $i < $attachmentNb ; $i ++)
|
|
$attachmentDetails[$i] = $this->getAttachmentDetails($i);
|
|
for ($i = 0 ; $i < $attachmentInlineNb ; $i ++)
|
|
$attachmentInlineDetails[$i] = $this->getAttachmentDetails($i, true);
|
|
unset ($i);
|
|
$size = strlen ($this->getMail());
|
|
$from = trim ($this->getHeader ("From"));
|
|
if ($from !== false)
|
|
{
|
|
$fromArray = $this->convertPeopleToArray (
|
|
$this->decodeHeaders ("From", $from));
|
|
}
|
|
$to = trim ($this->getHeader ("To"));
|
|
if ($to !== false)
|
|
{
|
|
$toArray = $this->convertPeopleToArray (
|
|
$this->decodeHeaders ("To", $to));
|
|
}
|
|
$date = trim ($this->getDate ());
|
|
$dateTimestamp = $this->getDateTimestamp ();
|
|
$subject = $this->decodeHeaders ("Subject", $this->getHeader ("Subject"));
|
|
return get_defined_vars();
|
|
}
|
|
|
|
/** Create a boundary
|
|
* @return string the textual boundary
|
|
*/
|
|
private function getBoundary ()
|
|
{
|
|
$data = openssl_random_pseudo_bytes (16);
|
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0010
|
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
|
|
return "-----".vsprintf ('%s%s%s%s%s%s%s%s',
|
|
str_split (bin2hex ($data), 4));
|
|
}
|
|
|
|
/** Create a messageID
|
|
* @return string the textual MessageID
|
|
*/
|
|
public function provideMessageID ()
|
|
{
|
|
$data = openssl_random_pseudo_bytes (16);
|
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0010
|
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
|
|
return "<".vsprintf ('%s%s-%s-%s-%s-%s%s%s',
|
|
str_split (bin2hex ($data), 4))."@".
|
|
php_uname('n').">";
|
|
}
|
|
|
|
/** Convert the content to correct encoding.
|
|
* The allowed encodings are "quoted-printable" or "base64" or "flowed"
|
|
* Cut the long lines to 76 chars with the correct separator
|
|
* @param string $content The content to encode
|
|
* @param string $encoding The encoding to use
|
|
* @return string the content encoded by $encoding
|
|
*/
|
|
private function encodingEncode ($content, $encoding)
|
|
{
|
|
if ($encoding === "quoted-printable")
|
|
{
|
|
$tmp = quoted_printable_encode ($content);
|
|
return $tmp;
|
|
}
|
|
elseif ($encoding === "base64")
|
|
{
|
|
$tmp = base64_encode ($content);
|
|
return chunk_split ($tmp);
|
|
}
|
|
elseif ($encoding === "flowed")
|
|
{
|
|
return chunk_split ($content);
|
|
}
|
|
throw new \Exception (sprintf (
|
|
dgettext ("domframework",
|
|
"Invalid encoding provided to encodingEncode : %s"), $encoding), 500);
|
|
}
|
|
|
|
/** Decode the content with correct encoding.
|
|
* The allowed encodings are "quoted-printable" or "base64" or "8bit"
|
|
* @param string $content The content to decode
|
|
* @param string $encoding The encoding to use
|
|
* @return the content decoded by $encoding
|
|
*/
|
|
private function encodingDecode ($content, $encoding)
|
|
{
|
|
if ($encoding === "quoted-printable")
|
|
return quoted_printable_decode ($content);
|
|
elseif ($encoding === "base64")
|
|
return base64_decode ($content);
|
|
elseif ($encoding === "8bit" || $encoding === "7bit" || $encoding === false)
|
|
return $content;
|
|
throw new \Exception (sprintf (
|
|
dgettext ("domframework",
|
|
"Invalid encoding provided to encodingDecode : '%s'"), $encoding), 500);
|
|
}
|
|
|
|
/** Encode a string to be compliant with MIME (used in headers)
|
|
* @param string $header The header to be used. Will not be returned, but the
|
|
* length of the result will be adapted to it
|
|
* @param string $content The content to encode
|
|
* @param string $encoding The encoding to use.
|
|
* The allowed encodings are "quoted-printable" or "base64"
|
|
* @return string the content encoded by $encoding
|
|
*/
|
|
private function encodeHeaders ($header, $content, $encoding)
|
|
{
|
|
$prefs = array ("input-charset" => "utf-8",
|
|
"output-charset" => "utf-8");
|
|
if ($encoding === "quoted-printable")
|
|
$prefs["scheme"] = "Q";
|
|
elseif ($encoding === "base64")
|
|
$prefs["scheme"] = "B";
|
|
else
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Invalid encoding provided to encodeHeaders"), 500);
|
|
return substr (iconv_mime_encode ($header, $content, $prefs),
|
|
strlen ($header)+2);
|
|
}
|
|
|
|
/** Convert the header to text
|
|
* @param string $header The header to decode
|
|
* @param string $content The content of the header to decode
|
|
* @return string the header converted
|
|
*/
|
|
private function decodeHeaders ($header, $content)
|
|
{
|
|
return substr (iconv_mime_decode ("$header: $content", 0, "utf-8"),
|
|
strlen ($header)+2);
|
|
}
|
|
|
|
/** Encode a header string not starting on first column. The number of chars
|
|
* need to be skipped is passed as argument. The function will correctely
|
|
* managethe associated carriage returns
|
|
* @param string $content The content to encode
|
|
* @param string $encoding The The encoding to use.
|
|
* The allowed encodings are "quoted-printable" or "base64"
|
|
* @param integer $blanks Initial blanks to be added on string
|
|
* @return string the content encoded by $encoding
|
|
*/
|
|
private function encodeHeaderStringWithPosition ($content, $encoding, $blanks)
|
|
{
|
|
$prefs = array ("input-charset" => "utf-8",
|
|
"output-charset" => "utf-8");
|
|
if ($encoding === "quoted-printable")
|
|
$prefs["scheme"] = "Q";
|
|
elseif ($encoding === "base64")
|
|
$prefs["scheme"] = "B";
|
|
else
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Invalid encoding provided to encodeHeaderStringWithPosition"), 500);
|
|
return substr (iconv_mime_encode (str_repeat (" ", $blanks), $content,
|
|
$prefs),
|
|
$blanks+2);
|
|
}
|
|
|
|
/** Convert a From/To string to array. Manage multiple recipients
|
|
* Ex. : toto toto <toto@toto.com>, titi <titi@titi.com>
|
|
* array (array ("name"=>"toto toto", "mail"=>"toto@toto.com"),
|
|
* array ("name"=>"titi", "mail"=>"titi@titi.com"))
|
|
* @param string $data The From/To field content
|
|
* @return array The array with the converted data
|
|
*/
|
|
public function convertPeopleToArray ($data)
|
|
{
|
|
$elements = explode (",", $data);
|
|
$res = array ();
|
|
foreach ($elements as $element)
|
|
{
|
|
@list ($name, $mail) = explode ("<", $element);
|
|
if ($mail === null)
|
|
{
|
|
$mail = $name;
|
|
$name = "";
|
|
}
|
|
else
|
|
{
|
|
$name = trim ($name);
|
|
$mail = substr (trim ($mail), 0, -1);
|
|
}
|
|
$array = array ("name"=>$name, "mail"=>$mail);
|
|
$res[] = $array;
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/** Analyze the Content-Type line and return an array with the values
|
|
* @param string $contentType The content Type to analyze
|
|
* @return array The analyzed Content-Type
|
|
*/
|
|
public function contentTypeAnalyze ($contentType)
|
|
{
|
|
$contentType = preg_replace ("#[\r\n]+[ \t]+#", " ", $contentType);
|
|
$elements = explode (";", $contentType);
|
|
$res = array ();
|
|
foreach ($elements as $elem)
|
|
{
|
|
@list ($key, $val) = explode ("=", $elem, 2);
|
|
if ($val === null)
|
|
$res["Content-Type"] = $key;
|
|
else
|
|
{
|
|
if ($val{0} === "'" || $val{0} === "\"")
|
|
$val = substr ($val, 1, -1);
|
|
$res[trim ($key)] = $val;
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
}
|