diff --git a/Tests/mailTest.php b/Tests/mailTest.php index aef8eb7..f402844 100644 --- a/Tests/mailTest.php +++ b/Tests/mailTest.php @@ -11,7 +11,7 @@ class test_mail extends PHPUnit_Framework_TestCase $mail = new mail (); $mail->setBodyText ("TEST"); $res = $mail->getMail (); - $this->assertRegExp ("#TEST\r\n$#", $res); + $this->assertRegExp ("#TEST\n$#", $res); } public function test_setBodyHTML1 () @@ -19,7 +19,7 @@ class test_mail extends PHPUnit_Framework_TestCase $mail = new mail (); $mail->setBodyHTML ("TEST"); $res = $mail->getMail (); - $this->assertRegExp ("#^\r\nTEST\r$#m", $res); + $this->assertRegExp ("#^\r\nTEST\n$#m", $res); } public function test_setGetBodyText1 () @@ -27,7 +27,7 @@ class test_mail extends PHPUnit_Framework_TestCase $mail = new mail (); $mail->setBodyText ("TEST"); $res = $mail->getBodyText (); - $this->assertSame ("TEST", $res); + $this->assertSame ("TEST\n", $res); } public function test_setGetBodyHtml1 () @@ -35,7 +35,7 @@ class test_mail extends PHPUnit_Framework_TestCase $mail = new mail (); $mail->setBodyHtml ("TEST"); $res = $mail->getBodyHtml (); - $this->assertSame ("TEST", $res); + $this->assertSame ("TEST\n", $res); } public function test_setFrom1 () diff --git a/mail.php b/mail.php index 0d836f6..1e2f504 100644 --- a/mail.php +++ b/mail.php @@ -3,44 +3,401 @@ @package domframework @author Dominique Fournier */ -/** Manage the mail content */ class mail { - /** The differents parts of the mail. Will be added to multipart/mixed - */ - private $parts = array (); - /** The inline parts of HTML body. Will be added to multipart/related + /** The complete of the mail */ - private $inlineParts = array (); - - /** The displayed part of the mail (the body) in TEXT and/or HTML. - * This is the alternative part of MIME mails - * Will be added to multipart/alternative - */ - private $bodyContent = array (); - - /** The EML part of the body (with the boundaries if needed) - * Contain the Text and/or HTML part of the body. For HTML, contain the - * inline parts too - */ - private $bodyContentEML = ""; - - /** The Headers of the Body in an array form. Will be appended to the mail - * headers if there is no multipart/alternative, or keep in the mail in case - * of multipart/alternative - */ - private $bodyHeaders = array (); - - /** The main headers of the mail - */ - private $headers = array (); + 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 (_("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 (_("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 (_("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 (_("Section not found"), 404); + if (! is_array ($param)) + throw new \Exception (_("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 + * @param array The content of the section + */ + private function sectionGet ($sectionID) + { + if (! array_key_exists ($sectionID, $this->sections)) + throw new \Exception (_("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); + $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 (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 (_("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]); + $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 !== "") + { + list ($head, $value) = explode (": ", $prev, 2); + $value .= $prevCR; + $headersArray[][$head] = $value; + $prev = $h; + $prevCR = $headersSplit[$i+1]; + } + else + { + $prev = $h; + $prevCR = $headersSplit[$i+1]; + } + } + if ($prev !== "") + { + list ($head, $value) = explode (": ", $prev, 2); + $value .= $prevCR; + $headersArray[][$head] = $value; + } + } + $contentType = $this->getHeaderValue ("Content-Type", $headersArray); + if ($contentType === false) + throw new \Exception ("No Content-Type Header defined", 500); + $contentTypeArray = $this->contentTypeAnalyze ($contentType); + if (! isset ($contentTypeArray["Content-Type"])) + throw new \Exception ("Can't parse the Content-Type", 500); + $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 () @@ -50,19 +407,11 @@ class mail if (! function_exists ("openssl_random_pseudo_bytes")) throw new \Exception (_("Missing OpenSSL PHP Extension"), 500); // Define default headers - $this->setHeader ("Date", date ("r")); - $this->setHeader ("Message-ID", $this->getMessageID ()); + $this->addHeader ("Date", date ("r")); + $this->addHeader ("Message-ID", $this->getMessageID ()); $user = posix_getpwuid (posix_geteuid()); - $this->setHeader ("From", $user["name"]."@".php_uname('n')); - $this->setHeader ("MIME-Version", "1.0"); - } - - /** Get the textual mail provided and use it as default - * @param $textualMail The complete mail to read - */ - public function readMail ($textualMail) - { - // TBD + $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 @@ -74,21 +423,98 @@ class mail public function setBodyHTML ($htmlContent, $charset="utf-8", $encoding="quoted-printable") { - $oldPart = end ($this->bodyContent); - if (count ($oldPart) && - substr ($oldPart["Content-Type"], 0, 9) === "text/html") + // Look if there is an existing section with text (main or + // multipart/alternative) + $sectionList = $this->sectionList (); + $sectionIDtoChange = ""; + foreach ($sectionList as $sectionID) { - // There is already a html body. Remove it to create a new one - array_pop ($this->bodyContent); + $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 (_("Can't find the place to store the HTML"), 500); } $htmlContent = iconv ("utf-8", $charset, $htmlContent); $part["_charset"] = $charset; - $part["_content"] = $this->encodingEncode ($htmlContent, $encoding); - $part["Content-Type"] = "text/html; charset=$charset"; - $part["Content-Transfer-Encoding"] = $encoding; - // Order the HTML after the text if exists - array_push ($this->bodyContent, $part); - $this->createBodyEML (); + $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 @@ -100,35 +526,103 @@ class mail public function setBodyText ($textContent, $charset="utf-8", $encoding="quoted-printable") { - if (isset ($this->bodyContent[0]) && - substr ($this->bodyContent[0]["Content-Type"], 0, 10) === "text/plain") + // Look if there is an existing section with text (main or + // multipart/alternative) + $sectionList = $this->sectionList (); + $sectionIDtoChange = ""; + foreach ($sectionList as $sectionID) { - // There is already a text body. Remove it to create a new one - unset ($this->bodyContent[0]); + $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 (_("Can't find the place to store the TEXT"), 500); } $textContent = iconv ("utf-8", $charset, $textContent); $part["_charset"] = $charset; - $part["Content-Type"] = "text/plain; charset=$charset"; - $part["Content-Transfer-Encoding"] = $encoding; - $part["_content"] = $this->encodingEncode ($textContent, $encoding); - // Order the text before the html if exists - array_unshift ($this->bodyContent, $part); - $this->createBodyEML (); + $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 () { - foreach ($this->bodyContent as $part) + $sectionList = $this->sectionList (); + $sectionIDtoChange = ""; + foreach ($sectionList as $sectionID) { - if (substr ($part["Content-Type"], 0, 9) === "text/html") + $section = $this->sectionGet ($sectionID); + if (array_key_exists ("_contentType", $section) && + $section["_contentType"] === "text/html") { - $body = $this->encodingDecode ($part["_content"], - $part["Content-Transfer-Encoding"]); - return iconv ($part["_charset"], "UTF-8", $body); + $encoding = $this->getHeaderValue ("Content-Transfer-Encoding", + $section["_headersArray"]); + $body = $this->encodingDecode ($section["_contentEML"], + $encoding); + return iconv ($section["_charset"], "UTF-8", $body); } } return false; @@ -137,141 +631,322 @@ class mail /** 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 () { - foreach ($this->bodyContent as $part) + $sectionList = $this->sectionList (); + $sectionIDtoChange = ""; + foreach ($sectionList as $sectionID) { - if (substr ($part["Content-Type"], 0, 10) === "text/plain") + $section = $this->sectionGet ($sectionID); + if (array_key_exists ("_contentType", $section) && + $section["_contentType"] === "text/plain") { - $body = $this->encodingDecode ($part["_content"], - $part["Content-Transfer-Encoding"]); - return iconv ($part["_charset"], "UTF-8", $body); + $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 bool|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") + public function addAttachment ($name, $fileContent, $encoding="base64", + $inline=false) { + // 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); - $part["Content-Type"] = "$mimetype; name=$name\r\n"; - $part["Content-Disposition"] = "attachment; filename=$name\r\n"; - $part["Content-Transfer-Encoding"] = "$encoding\r\n"; + $this->addHeader ("Content-Type", "$mimetype; name=$name\n", $sectionID); + if ($inline === false) + $this->addHeader ("Content-Disposition", "attachment; filename=$name\r\n", + $sectionID); + else + { + $this->addHeader ("Content-Disposition", "inline; filename=$name\r\n", + $sectionID); + $contentID = $this->getMessageID (); + $this->addHeader ("Content-ID", "$contentID\r\n", $sectionID); + } + $this->addHeader ("Content-Transfer-Encoding", "$encoding\r\n", $sectionID); $part["_name"] = $name; - $part["_content"] = $this->encodingEncode ($fileContent, $encoding); + $part["_contentEML"] = $this->encodingEncode ($fileContent, $encoding); $part["_mimetype"] = $mimetype; $part["_sizeReal"] = strlen ($fileContent); - array_push ($this->parts, $part); + $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 : * + * @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") { - $finfo = new \finfo(FILEINFO_MIME); - $mimetype = $finfo->buffer($fileContent); - $part["Content-Type"] = "$mimetype; name=$name\r\n"; - $contentID = $this->getMessageID (); - $part["Content-ID"] = "$contentID\r\n"; - $part["Content-Disposition"] = "inline; filename=$name\r\n"; - $part["Content-Transfer-Encoding"] = "$encoding\r\n"; - $part["_name"] = $name; - $part["_content"] = $this->encodingEncode ($fileContent, $encoding); - $part["_mimetype"] = $mimetype; - $part["_sizeReal"] = strlen ($fileContent); - array_push ($this->inlineParts, $part); - $this->createBodyEML (); - return substr ($contentID, 1, -1); + return $this->addAttachment ($name, $fileContent, $encoding, true); } /** Get an attachment of the mail - * @param $number the number of attach to get starting to 0 + * @param integer $number the number of attach to get starting to 0 + * @param bool|null $inline Return only the attachments Inline if true * @return the content of the attachment. Can be binary */ - public function getAttachment ($number) + public function getAttachment ($number, $inline = false) { - if (! array_key_exists ($number, $this->parts)) - throw new \Exception (_("Attachment not found"), 404); - $part = $this->parts[$number]; - return $this->encodingDecode ($part["_content"], - $part["Content-Transfer-Encoding"]); + $attachmentIDs = $this->getAttachmentID ($inline); + if (! array_key_exists ($number, $attachmentIDs)) + throw new \Exception (sprintf (_("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 $number the number of attach to get starting to 0 + * @param integer $number the number of attach to get starting to 0 + * @param bool|null $inline Return only the attachments Inline if true * @return array containing the information of the attachment */ - public function getAttachmentDetails ($number) + public function getAttachmentDetails ($number, $inline = false) { - if (! array_key_exists ($number, $this->parts)) + $attachmentIDs = $this->getAttachmentID ($inline); + if (! array_key_exists ($number, $attachmentIDs)) throw new \Exception (sprintf (_("Attachment '%d' not found"), $number), 404); - $part = $this->parts[$number]; - $res = array (); + $part = $this->sectionGet ($attachmentIDs[$number]); foreach ($part as $key=>$val) { - if ($key{0} === "_" && $key !== "_content") - $res[substr ($key, 1)] = $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; } - /** Add a To: header. If it already exists, add a new recipient*/ - public function addTo ($toMail, $toName) + /** Return the list of the sectionID containing a attachment. Contains the + * inline attachments too. + * @param bool $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) + { + $disposition = $this->getHeaderValue ("Content-Disposition", + $section["_headersArray"]); + if ($disposition === false) + continue; + if ($inline === true && substr ($disposition, 0, 6) === "inline") + $res[] = $sectionID; + elseif ($inline === false && substr ($disposition, 0, 10) === "attachment") + $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 ("From", $toName, + $toName = $this->encodeHeaders ("To", $toName, "quoted-printable"); - $toField = "$toName <$toMail>"; - if (array_key_exists ("To", $this->headers)) - $this->setHeader ("To", $this->getHeaderValue ("To").",\r\n $toField"); + 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 */ + /** 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->headers["To"]; + return $this->getHeader ("To"); } - /** Add a From: header. If it already exists, overwrite the existing one*/ - public function setFrom ($fromMail, $fromName) + /** 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"); - $this->setHeader ("From", "$fromName <$fromMail>"); + if ($fromName !== "") + $fromName .= " "; + $this->setHeader ("From", "$fromName<$fromMail>"); } - /** Return the From header as it is written in the mail */ + /** 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->headers["From"]; + return $this->getHeader ("From"); } - /** Return the From header converted to array with mail and name keys */ + /** 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", $this->headers["From"]); - return reset ($this->convertPeopleToArray ($from)); + $from = $this->decodeHeaders ("From", $from); + $from = $this->convertPeopleToArray ($from); + return reset ($from); } /** Set the Date - * @param $date In RFC 2822 format + * @param string $date In RFC 2822 format */ public function setDate ($date) { @@ -280,7 +955,7 @@ class mail } /** Set the Date - * @param $date In Timestamp format + * @param string $timestamp In Timestamp format */ public function setDateTimestamp ($timestamp) { @@ -288,15 +963,19 @@ class mail $this->setHeader ("Date", date ("r", $timestamp)); } - /** Return the Date header if defined. - Return false if not defined */ + /** 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 false if not defined + * @return integer|bool The date Header if defined or false if not defined + */ public function getDateTimestamp () { $datetimestamp = false; @@ -315,231 +994,267 @@ class mail } /** Set a generic header - * @param $header The name of the Header (without colon) - * @param $value The value the store. The format must be correct ! + * @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) + public function setHeader ($header, $value, $sectionID=null) { if (substr ($value, -1) !== "\n" && substr ($value, -1) !== "\r" && substr ($value, -2) !== "\r\n") $value .= "\r\n"; - $this->headers[$header] = $value; + 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 .= "\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; + } + $this->sections[$sectionMainID]["_headersArray"][][$header] = $value; + // TODO : Encode ? Strip ? + $this->sections[$sectionMainID]["_headersEML"] .= "$header: $value"; + $this->createMailEML (); } /** Delete a specific header - * @param $header The header to remove + * @param string $header The header to remove + * @param string|null $sectionID The section to modify. If null, use the main */ - public function delHeader ($header) + public function delHeader ($header, $sectionID=null) { - unset ($this->headers[$header]); + 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; + } + $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 - * @param $header The header to get - * @return the literal value or false if it doesn't exist + * If there is multiple headers with the same name, return the first + * @param string $header The header to get + * @return string|bool the literal value or false if it doesn't exist */ - public function getHeader ($header) + public function getHeader ($header, $headers=null) { - if (! isset ($this->headers[$header])) - return false; - return $this->headers[$header]; - } - - /** Get a generic header with removing the carraige return - *@param $header The header to get - * @return the literal value or false if it doesn't exist - */ - public function getHeaderValue ($header) - { - if (! isset ($this->headers[$header])) - return false; - if (substr ($this->headers[$header], -1) !== "\n" && - substr ($this->headers[$header], -1) !== "\r") - return substr ($this->headers[$header], 0, -1); - return substr ($this->headers[$header], 0, -2); - } - - /** Create the Body EML part for HTML - * @param $bodyContentHTML the $this->part corresponding to HTML Body - * @return the EML part for HTML with multipart/related if needed - */ - public function createBodyEMLHTML ($bodyContentHTML) - { - $html = ""; - if (count ($this->inlineParts)) + if ($headers === null) { - $inlineBoundary = $this->getBoundary (); - $html .= "Content-Type: multipart/related;\r\n". - " boundary=\"$inlineBoundary\"\r\n\r\n"; - $html .= "--$inlineBoundary\r\n"; - } - foreach ($bodyContentHTML as $key=>$val) - { - if ($key{0} === "_") continue; - $html .= "$key: $val\r\n"; - } - $html .= "\r\n".$bodyContentHTML["_content"]."\r\n"; - foreach ($this->inlineParts as $part) - { - $html .= "--$inlineBoundary\r\n"; - foreach ($part as $key=>$val) - { - if ($key{0} === "_") continue; - $html .= "$key: $val"; - } - $html .= "\r\n".$part["_content"]."\r\n"; - } - if (count ($this->inlineParts)) - { - $html .= "--$inlineBoundary--\r\n"; - } - return $html; - } - - /** Create the Body EML part - */ - public function createBodyEML () - { - // Create the alternative/part body for TEXT/HTML part - $bodyContent = ""; - $bodyHeaders = array (); - $body = ""; - // TODO : Check if the mail contain only attached file - if (count ($this->bodyContent) === 0) - throw new \Exception (_("No content in the mail"), 406); - elseif (count ($this->bodyContent) === 2) - { - // Text + HTML available : manage the alternative part - $boundary = $this->getBoundary (); - $bodyHeaders["Content-Type"] = "multipart/alternative;\r\n". - " boundary=\"$boundary\"\r\n"; - // Store the Text part - $bodyContent .= "--$boundary\r\n"; - foreach ($this->bodyContent[0] as $key=>$val) - { - if ($key{0} === "_") continue; - $bodyContent .= "$key: $val\r\n"; - } - $bodyContent .= "\r\n".$this->bodyContent[0]["_content"]."\r\n"; - - // Store the HTML part - $bodyContent .= "--$boundary\r\n"; - $bodyContent .= $this->createBodyEMLHTML ($this->bodyContent[1]); - $bodyContent .= "--$boundary--\r\n"; - } - else - { - // Text or HTML but not both - $part = reset ($this->bodyContent); - if (substr ($part["Content-Type"], 0, 10) === "text/plain") - { - // Only TEXT part - foreach ($part as $key=>$val) - { - if ($key{0} === "_") - $bodyContent = "$val\r\n"; - else - $bodyHeaders[$key] = "$val\r\n"; - } - $body .= $bodyContent; - } - elseif (substr ($part["Content-Type"], 0, 9) === "text/html") - { - // Only HTML part - foreach ($part as $key=>$val) - { - if ($key{0} === "_") - $bodyContent = "$val\r\n"; - else - $bodyHeaders[$key] = "$val\r\n"; - } - $body .= $this->createBodyEMLHTML ($part); - } + $sectionMainID = $this->sectionMainID (); + if ($sectionMainID === false) + $headers = array (); else - throw new \Exception (_("No text/plain nor text/html available"), - 500); + $headers = $this->sections[$sectionMainID]["_headersArray"]; } + foreach ($headers as $key=>$val) + { + $head = key ($val); + $value = $val[$head]; + if ($head === $header) + return $value; + } + return false; + } - $this->bodyContentEML = $bodyContent; - $this->bodyHeaders = $bodyHeaders; + /** 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) + $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 > 10) + throw new \Exception ("Recurse createMailEMLSub > 10", 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 () { - $complete = ""; - if (count ($this->parts) === 0) - { - // No attached files available : manage the body in text and/or html - foreach ($this->headers as $key=>$val) - { - $complete .= "$key: $val"; - } - foreach ($this->bodyHeaders as $key=>$val) - { - $complete .= "$key: $val"; - } - $complete .= $this->headerBodySeparator; - $complete .= $this->bodyContentEML; - } - else - { - // Attached files available : the main Content-Type is multipart/mixed; - $boundary = $this->getBoundary (); - $this->headers["Content-Type"] = "multipart/mixed;\r\n". - " boundary=\"$boundary\"\r\n"; - foreach ($this->headers as $key=>$val) - { - $complete .= "$key: $val"; - } - $complete .= $this->headerBodySeparator; - if ($this->bodyContentEML !== "") - { - $complete .= "--$boundary\r\n"; - foreach ($this->bodyHeaders as $key=>$val) - { - $complete .= "$key: $val"; - } - $complete .= $this->headerBodySeparator; - $complete .= $this->bodyContentEML; - } - foreach ($this->parts as $part) - { - $complete .= "--$boundary\r\n"; - foreach ($part as $key=>$val) - { - if ($key{0} === "_") continue; - $complete .= "$key: $val"; - } - $complete .= $this->headerBodySeparator; - $complete .= $part["_content"]; - } - $complete .= "--$boundary--\r\n"; - } - return $complete; + 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 + * @return array The details of the mail */ public function getDetails () { - $attachmentNb = count ($this->parts); - for ($i = 0 ; $i < count ($this->parts) ; $i ++) + $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); + 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); - $bodyTextExists = (isset ($this->bodyContent[0]) && - substr ($this->bodyContent[0]["Content-Type"], 0, 10) === "text/plain")? - true: false; - $oldPart = end ($this->bodyContent); - $bodyHtmlExists = (count ($oldPart) && - substr ($oldPart["Content-Type"], 0, 9) === "text/html")? - true:false; - unset ($oldPart); $size = strlen ($this->getMail()); $from = $this->getHeader ("From"); if ($from !== false) @@ -555,11 +1270,12 @@ class mail } $date = $this->getDate (); $dateTimestamp = $this->getDateTimestamp (); + $subject = $this->getHeaderValue ("Subject"); return get_defined_vars(); } /** Create a boundary - * @return the textual boundary + * @return string the textual boundary */ private function getBoundary () { @@ -571,7 +1287,7 @@ class mail } /** Create a messageID - * @return the textual MessageID + * @return string the textual MessageID */ private function getMessageID () { @@ -584,9 +1300,11 @@ class mail } /** Convert the content to correct encoding. - * The allowed encodings are "quoted-printable" or "base64" + * The allowed encodings are "quoted-printable" or "base64" or "flowed" * Cut the long lines to 76 chars with the correct separator - * @return the content encoded by $encoding + * @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) { @@ -600,12 +1318,20 @@ class mail $tmp = base64_encode ($content); return chunk_split ($tmp); } - throw new \Exception (_("Invalid encoding provided to encodingEncode"), + elseif ($encoding === "flowed") + { + return chunk_split ($content); + } + throw new \Exception (sprintf ( + _("Invalid encoding provided to encodingEncode : %s"), + $encoding), 500); } /** Decode the content with correct encoding. - * The allowed encodings are "quoted-printable" or "base64" + * 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) @@ -614,17 +1340,21 @@ class mail return quoted_printable_decode ($content); elseif ($encoding === "base64") return base64_decode ($content); - throw new \Exception (_("Invalid encoding provided to encodingDecode"), + elseif ($encoding === "8bit") + return ($content); + throw new \Exception (sprintf ( + _("Invalid encoding provided to encodingDecode : '%s'"), + $encoding), 500); } /** Encode a string to be compliant with MIME (used in headers) - * @param $header The header to be used. Will not be returned, but the length - * of the result will be adapted to it - * @param $content The content to encode - * @param $encoding The encoding to use. + * @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 the content encoded by $encoding + * @return string the content encoded by $encoding */ private function encodeHeaders ($header, $content, $encoding) { @@ -641,7 +1371,11 @@ class mail strlen ($header)+2); } - /** Convert the header to text */ + /** 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"), @@ -650,8 +1384,10 @@ class mail /** Convert a From/To string to array. Manage multiple recipients * Ex. : toto toto , titi - array (array ("name"=>"toto toto", "mail"=>"toto@toto.com"), - array ("name"=>"titi", "mail"=>"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) { @@ -675,4 +1411,28 @@ class mail } 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; + } }