*/ /** 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 (_("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 * @return 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); 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 (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]); 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 (_("Missing FileInfo PHP Extension"), 500); if (! function_exists ("openssl_random_pseudo_bytes")) throw new \Exception (_("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 (_("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 (_("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 : * * @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 (_("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 (_("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) { $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 ("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 .= "\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) $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) $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); 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->getHeaderValue ("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 ( _("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 ( _("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 (_("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 (_("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 , titi * 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; } }