Skip to content

Commit

Permalink
BackwardsCompatibilityBreak - the $contents parameter of fEmail::ad…
Browse files Browse the repository at this point in the history
…dAttachment() is now first instead of third.

Completed ticket #331 - fEmail::addAttachment() will now accept fFile objects for the `$contents` parameter.

Completed ticket #417 - Added the method fEmail::addRelatedFile() to allow for embedded images inside of the HTML body.
  • Loading branch information
wbond committed Apr 12, 2012
1 parent fd0e0ea commit a1ae8dc
Showing 1 changed file with 179 additions and 68 deletions.
247 changes: 179 additions & 68 deletions classes/fEmail.php
Expand Up @@ -17,7 +17,8 @@
* @package Flourish
* @link http://flourishlib.com/fEmail
*
* @version 1.0.0b23
* @version 1.0.0b24
* @changes 1.0.0b24 Backwards Compatibility Break - the `$contents` parameter of ::addAttachment() is now first instead of third, ::addAttachment() will now accept fFile objects for the `$contents` parameter, added ::addRelatedFile() [wb, 2010-12-01]
* @changes 1.0.0b23 Fixed a bug on Windows where emails starting with a `.` would have the `.` removed [wb, 2010-09-11]
* @changes 1.0.0b22 Revamped the FQDN code and added ::getFQDN() [wb, 2010-09-07]
* @changes 1.0.0b21 Added a check to prevent permissions warnings when getting the FQDN on Windows machines [wb, 2010-09-02]
Expand Down Expand Up @@ -274,7 +275,7 @@ static public function getFQDN()
} catch (com_exception $e) { }
}
}
if ($domain) {
if (!empty($domain)) {
self::$fqdn = '.' . $domain;
}

Expand Down Expand Up @@ -387,6 +388,13 @@ static public function unindentExpand($text)
*/
private $html_body = NULL;

/**
* The Message-ID header for the email
*
* @var string
*/
private $message_id = NULL;

/**
* The plaintext body of the email
*
Expand All @@ -401,6 +409,13 @@ static public function unindentExpand($text)
*/
private $recipients_smime_cert_file = NULL;

/**
* The files to include as multipart/related
*
* @var array
*/
private $related_files = array();

/**
* The email address to reply to
*
Expand Down Expand Up @@ -472,7 +487,7 @@ static public function unindentExpand($text)
*/
public function __construct()
{

$this->message_id = '<' . fCryptography::randomString(10, 'hexadecimal') . '.' . time() . '@' . self::getFQDN() . '>';
}


Expand All @@ -495,28 +510,17 @@ public function __get($method)
*
* If a duplicate filename is detected, it will be changed to be unique.
*
* @param string $filename The name of the file to attach
* @param string $mime_type The mime type of the file
* @param string $contents The contents of the file
* @param string|fFile $contents The contents of the file
* @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
* @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
* @return void
*/
public function addAttachment($filename, $mime_type, $contents)
public function addAttachment($contents, $filename=NULL, $mime_type=NULL)
{
if (!self::stringlike($filename)) {
throw new fProgrammerException(
'The filename specified, %s, does not appear to be a valid filename',
$filename
);
}

$filename = (string) $filename;
$this->extrapolateFileInfo($contents, $filename, $mime_type);

$i = 1;
while (isset($this->attachments[$filename])) {
$filename_info = fFilesystem::getPathInfo($filename);
$extension = ($filename_info['extension']) ? '.' . $filename_info['extension'] : '';
$filename = preg_replace('#_copy\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension;
$i++;
$this->generateNewFilename($filename);
}

$this->attachments[$filename] = array(
Expand All @@ -526,6 +530,40 @@ public function addAttachment($filename, $mime_type, $contents)
}


/**
* Adds a “related” file to the email, returning the `Content-ID` for use in HTML
*
* The purpose of a related file is to be able to reference it in part of
* the HTML body. Image `src` URLs can reference a related file by starting
* the URL with `cid:` and then inserting the `Content-ID`.
*
* If a duplicate filename is detected, it will be changed to be unique.
*
* @param string|fFile $contents The contents of the file
* @param string $filename The name to give the attachement - optional if `$contents` is an fFile object
* @param string $mime_type The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
* @return string The fully-formed `cid:` URL for use in HTML `src` attributes
*/
public function addRelatedFile($contents, $filename=NULL, $mime_type=NULL)
{
$this->extrapolateFileInfo($contents, $filename, $mime_type);

while (isset($this->related_files[$filename])) {
$this->generateNewFilename($filename);
}

$cid = count($this->related_files) . '.' . substr($this->message_id, 1, -1);

$this->related_files[$filename] = array(
'mime-type' => $mime_type,
'contents' => $contents,
'content-id' => '<' . $cid . '>'
);

return 'cid:' . $cid;
}


/**
* Adds a blind carbon copy (BCC) email recipient
*
Expand Down Expand Up @@ -680,65 +718,77 @@ private function combineNameEmail($name, $email)
*/
private function createBody($boundary)
{
$boundary_stack = array($boundary);

$mime_notice = self::compose(
"This message has been formatted using MIME. It does not appear that your\r\nemail client supports MIME."
);

$body = '';

// Build the multi-part/alternative section for the plaintext/HTML combo
if ($this->html_body) {

$alternative_boundary = $boundary;

// Depending on the other content, we may need to create a new boundary
if ($this->attachments) {
$alternative_boundary = $this->createBoundary();
$body .= 'Content-Type: multipart/alternative; boundary="' . $alternative_boundary . "\"\r\n\r\n";
} else {
$body .= $mime_notice . "\r\n\r\n";
}

$body .= '--' . $alternative_boundary . "\r\n";
if ($this->html_body || $this->attachments) {
$body .= $mime_notice . "\r\n\r\n";
}

if ($this->html_body && $this->related_files && $this->attachments) {
$body .= '--' . end($boundary_stack) . "\r\n";
$boundary_stack[] = $this->createBoundary();
$body .= 'Content-Type: multipart/related; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
}

if ($this->html_body && ($this->attachments || $this->related_files)) {
$body .= '--' . end($boundary_stack) . "\r\n";
$boundary_stack[] = $this->createBoundary();
$body .= 'Content-Type: multipart/alternative; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
}

if ($this->html_body || $this->attachments) {
$body .= '--' . end($boundary_stack) . "\r\n";
$body .= "Content-Type: text/plain; charset=utf-8\r\n";
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
$body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
$body .= '--' . $alternative_boundary . "\r\n";
}

$body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";

if ($this->html_body) {
$body .= '--' . end($boundary_stack) . "\r\n";
$body .= "Content-Type: text/html; charset=utf-8\r\n";
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
$body .= $this->makeQuotedPrintable($this->html_body) . "\r\n";
$body .= '--' . $alternative_boundary . "--\r\n";
}

// If there is no HTML, just encode the body
} else {
if ($this->related_files) {
$body .= '--' . end($boundary_stack) . "--\r\n";
array_pop($boundary_stack);

// Depending on the other content, these headers may be inline or in the real headers
if ($this->attachments) {
$body .= "Content-Type: text/plain; charset=utf-8\r\n";
$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
foreach ($this->related_files as $filename => $file_info) {
$body .= '--' . end($boundary_stack) . "\r\n";
$body .= 'Content-Type: ' . $file_info['mime-type'] . '; name="' . $filename . "\"\r\n";
$body .= "Content-Transfer-Encoding: base64\r\n";
$body .= 'Content-ID: ' . $file_info['content-id'] . "\r\n\r\n";
$body .= $this->makeBase64($file_info['contents']) . "\r\n";
}

$body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
}

// If we have attachments, we need to wrap a multipart/mixed around the current body
if ($this->attachments) {

$multipart_body = $mime_notice . "\r\n\r\n";
$multipart_body .= '--' . $boundary . "\r\n";
$multipart_body .= $body;
if ($this->html_body) {
$body .= '--' . end($boundary_stack) . "--\r\n";
array_pop($boundary_stack);
}

foreach ($this->attachments as $filename => $file_info) {
$multipart_body .= '--' . $boundary . "\r\n";
$multipart_body .= 'Content-Type: ' . $file_info['mime-type'] . "\r\n";
$multipart_body .= "Content-Transfer-Encoding: base64\r\n";
$multipart_body .= 'Content-Disposition: attachment; filename="' . $filename . "\";\r\n\r\n";
$multipart_body .= $this->makeBase64($file_info['contents']) . "\r\n";
$body .= '--' . end($boundary_stack) . "\r\n";
$body .= 'Content-Type: ' . $file_info['mime-type'] . "\r\n";
$body .= "Content-Transfer-Encoding: base64\r\n";
$body .= 'Content-Disposition: attachment; filename="' . $filename . "\";\r\n\r\n";
$body .= $this->makeBase64($file_info['contents']) . "\r\n";
}

$multipart_body .= '--' . $boundary . "--\r\n";

$body = $multipart_body;
}

if ($this->html_body || $this->attachments) {
$body .= '--' . end($boundary_stack) . "--\r\n";
array_pop($boundary_stack);
}

return $body;
Expand Down Expand Up @@ -777,16 +827,18 @@ private function createHeaders($boundary, $message_id)
$headers .= "Message-ID: " . $message_id . "\r\n";
$headers .= "MIME-Version: 1.0\r\n";

if ($this->html_body && !$this->attachments) {
$headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n";
}

if (!$this->html_body && !$this->attachments) {
$headers .= "Content-Type: text/plain; charset=utf-8\r\n";
$headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
}

if ($this->attachments) {
} elseif ($this->html_body && !$this->attachments) {
if ($this->related_files) {
$headers .= 'Content-Type: multipart/related; boundary="' . $boundary . "\"\r\n";
} else {
$headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n";
}

} elseif ($this->attachments) {
$headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . "\"\r\n";
}

Expand Down Expand Up @@ -944,6 +996,61 @@ private function extractEmails($list)
}


/**
* Extracts the filename and mime-type from an fFile object
*
* @param string|fFile &$contents The file to extrapolate the info from
* @param string &$filename The filename to use for the file
* @param string &$mime_type The mime type of the file
* @return void
*/
private function extrapolateFileInfo(&$contents, &$filename, &$mime_type)
{
if ($contents instanceof fFile) {
if ($filename === NULL) {
$filename = $contents->getName();
}
if ($mime_type === NULL) {
$mime_type = $contents->getMimeType();
}
$contents = $contents->read();

} else {
if (!self::stringlike($filename)) {
throw new fProgrammerException(
'The filename specified, %s, does not appear to be a valid filename',
$filename
);
}

$filename = (string) $filename;

if ($mime_type === NULL) {
$mime_type = fFile::determineMimeType($filename, $contents);
}
}
}


/**
* Generates a new filename in an attempt to create a unique name
*
* @param string $filename The filename to generate another name for
* @return string The newly generated filename
*/
private function generateNewFilename($filename)
{
$filename_info = fFilesystem::getPathInfo($filename);
if (preg_match('#_copy(\d+)($|\.)#D', $filename_info['filename'], $match)) {
$i = $match[1] + 1;
} else {
$i = 1;
}
$extension = ($filename_info['extension']) ? '.' . $filename_info['extension'] : '';
return preg_replace('#_copy\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension;
}


/**
* Loads the plaintext version of the email body from a file and applies replacements
*
Expand Down Expand Up @@ -1023,6 +1130,11 @@ private function makeEncodedWord($content)
$content = str_replace("\r", "\n", $content);
$content = str_replace("\n", "\r\n", $content);

// Encoded word is not required if all characters are ascii
if (!preg_match('#[\x80-\xFF]#', $content)) {
return $content;
}

// A quick a dirty hex encoding
$content = rawurlencode($content);
$content = str_replace('=', '%3D', $content);
Expand Down Expand Up @@ -1210,9 +1322,8 @@ public function send($connection=NULL)

$to = trim($this->buildMultiAddressHeader("", $this->to_emails));

$message_id = '<' . fCryptography::randomString(32, 'hexadecimal') . '@' . self::getFQDN() . '>';
$top_level_boundary = $this->createBoundary();
$headers = $this->createHeaders($top_level_boundary, $message_id);
$headers = $this->createHeaders($top_level_boundary, $this->message_id);

$subject = str_replace(array("\r", "\n"), '', $this->subject);
$subject = $this->makeEncodedWord($subject);
Expand All @@ -1233,7 +1344,7 @@ public function send($connection=NULL)
$to_emails = array_merge($to_emails, $this->extractEmails($this->bcc_emails));
$from = $this->bounce_to_email ? $this->bounce_to_email : current($this->extractEmails(array($this->from_email)));
$connection->send($from, $to_emails, "To: " . $to . "\r\nSubject: " . $subject . "\r\n" . $headers, $body);
return $message_id;
return $this->message_id;
}

// Sendmail when not in safe mode will allow you to set the envelope from address via the -f parameter
Expand Down Expand Up @@ -1299,7 +1410,7 @@ public function send($connection=NULL)
);
}

return $message_id;
return $this->message_id;
}


Expand Down

0 comments on commit a1ae8dc

Please sign in to comment.