Files
DomFramework/mail.php
Dominique Fournier 23516c7b57 * mail : the mail creator. Allow to create complete mails easily
$mail = new mail ();
    $mail->setFrom ("sender@example.com","Sender Example Com");
    $mail->addTo ("recipient1@example.com","Recipient1 Example Com");
    $mail->addTo ("recipient2@example.com","Recipient2 Example Com");
    $mail->setBodyText ("Content of TextBody part");
    $mail->addAttachment ("file0.text", "File content");
    $contentID1 = $mail->addAttachmentInline ("file2.jpg", "qcqscqs");
    $mail->setBodyHTML ("<p>Content of HTMLBody part with inline
                         <img src='cid:$contentID1'></p>");
    echo $mail->getMail ();



git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@2709 bf3deb0d-5f1a-0410-827f-c0cc1f45334c
2016-04-29 14:47:12 +00:00

679 lines
22 KiB
PHP

<?php
/** DomFramework
@package domframework
@author Dominique Fournier <dominique@fournier38.fr> */
/** 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 :
* <img src='cid:XXXXXXXXXX'/>
*/
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 <toto@toto.com>, titi <titi@titi.com>
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;
}
}