*/ /** 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 */ 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 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"; /** 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->setHeader ("Date", date ("r")); $this->setHeader ("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 } /** 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 $htmlContent in UTF-8 * @param $charset to be stored in the mail * @param $encoding the encoding in the 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") { // There is already a html body. Remove it to create a new one array_pop ($this->bodyContent); } $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 (); } /** 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 $textContent in UTF-8 * @param $charset to be stored in the mail * @param $encoding the encoding in the 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") { // There is already a text body. Remove it to create a new one unset ($this->bodyContent[0]); } $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 (); } /** 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 */ public function getBodyHTML () { foreach ($this->bodyContent as $part) { if (substr ($part["Content-Type"], 0, 9) === "text/html") { $body = $this->encodingDecode ($part["_content"], $part["Content-Transfer-Encoding"]); return iconv ($part["_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 */ public function getBodyText () { foreach ($this->bodyContent as $part) { if (substr ($part["Content-Type"], 0, 10) === "text/plain") { $body = $this->encodingDecode ($part["_content"], $part["Content-Transfer-Encoding"]); return iconv ($part["_charset"], "UTF-8", $body); } } return false; } /** Add an attachment to the mail. * The allowed encodings are "quoted-printable" or "base64" */ public function addAttachment ($name, $fileContent, $encoding="base64") { $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"; $part["_name"] = $name; $part["_content"] = $this->encodingEncode ($fileContent, $encoding); $part["_mimetype"] = $mimetype; $part["_sizeReal"] = strlen ($fileContent); array_push ($this->parts, $part); } /** 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 : * */ 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); } /** Get an attachment of the mail * @param $number the number of attach to get starting to 0 * @return the content of the attachment. Can be binary */ public function getAttachment ($number) { 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"]); } /** Get the attachment details * @param $number the number of attach to get starting to 0 * @return array containing the information of the attachment */ public function getAttachmentDetails ($number) { if (! array_key_exists ($number, $this->parts)) throw new \Exception (sprintf (_("Attachment '%d' not found"), $number), 404); $part = $this->parts[$number]; $res = array (); foreach ($part as $key=>$val) { if ($key{0} === "_" && $key !== "_content") $res[substr ($key, 1)] = $val; } return $res; } /** Add a To: header. If it already exists, add a new recipient*/ public function addTo ($toMail, $toName) { if (strspn ($toName, "abcdefghijklmnopqrstuvwxyz". "ABCDEFGHIJKLMNOPQRSTUVWXYZ". "0123456789 -_") !== strlen ($toName)) $toName = $this->encodeHeaders ("From", $toName, "quoted-printable"); $toField = "$toName <$toMail>"; if (array_key_exists ("To", $this->headers)) $this->setHeader ("To", $this->getHeaderValue ("To").",\r\n $toField"); else $this->setHeader ("To", $toField); } /** Get the To Header as it is written in the mail */ public function getTo () { return $this->headers["To"]; } /** Add a From: header. If it already exists, overwrite the existing one*/ 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>"); } /** Return the From header as it is written in the mail */ public function getFrom () { return $this->headers["From"]; } /** Return the From header converted to array with mail and name keys */ public function getFromArray () { $res = array (); $from = $this->decodeHeaders ("From", $this->headers["From"]); return reset ($this->convertPeopleToArray ($from)); } /** Set the Date * @param $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 $date In Timestamp format */ public function setDateTimestamp ($timestamp) { // TODO : Check if the timestamp is valid $this->setHeader ("Date", date ("r", $timestamp)); } /** Return the Date header if defined. Return false if not defined */ public function getDate () { return $this->getHeader ("Date"); } /** Return the Date header (if defined) in timestamp * Return 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 $header The name of the Header (without colon) * @param $value The value the store. The format must be correct ! */ public function setHeader ($header, $value) { if (substr ($value, -1) !== "\n" && substr ($value, -1) !== "\r" && substr ($value, -2) !== "\r\n") $value .= "\r\n"; $this->headers[$header] = $value; } /** Delete a specific header * @param $header The header to remove */ public function delHeader ($header) { unset ($this->headers[$header]); } /** Get a generic header * @param $header The header to get * @return the literal value or false if it doesn't exist */ public function getHeader ($header) { 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)) { $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); } else throw new \Exception (_("No text/plain nor text/html available"), 500); } $this->bodyContentEML = $bodyContent; $this->bodyHeaders = $bodyHeaders; } /** Return 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 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 */ public function getDetails () { $attachmentNb = count ($this->parts); for ($i = 0 ; $i < count ($this->parts) ; $i ++) $attachmentDetails[$i] = $this->getAttachmentDetails($i); 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) { $fromArray = $this->convertPeopleToArray ( $this->decodeHeaders ("From", $from)); } $to = $this->getHeader ("To"); if ($to !== false) { $toArray = $this->convertPeopleToArray ( $this->decodeHeaders ("To", $to)); } $date = $this->getDate (); $dateTimestamp = $this->getDateTimestamp (); return get_defined_vars(); } /** Create a boundary * @return 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 the textual MessageID */ private function getMessageID () { $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" * Cut the long lines to 76 chars with the correct separator * @return 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); } throw new \Exception (_("Invalid encoding provided to encodingEncode"), 500); } /** Decode the content with correct encoding. * The allowed encodings are "quoted-printable" or "base64" * @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); throw new \Exception (_("Invalid encoding provided to encodingDecode"), 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. * The allowed encodings are "quoted-printable" or "base64" * @return 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 */ private function decodeHeaders ($header, $content) { return substr (iconv_mime_decode ("$header: $content"), strlen ($header)+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")) */ 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; } }