*/
/** 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
* @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 ()
{
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->getMessageID ());
$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 $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")
{
// 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 $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")
{
// 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 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",
$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->getMessageID ();
$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 bool|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 bool|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 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 ("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 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;
}
$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
* @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 > 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 ()
{
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 = $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 ();
$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
*/
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" 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")
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"),
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;
}
}