Version: current

      1 <?php
      2 /**
      3  * Mailer objects are responsible for actually sending emails.
      4  * The default Mailer class will use PHP's mail() function.
      5  *
      6  * @package sapphire
      7  * @subpackage email
      8  */
      9 class Mailer extends Object {
     10
     11     /**
     12      * Отправитель для тех случаев когда пиьсмо не принимается
     13      */
     14     protected static $fallback_sender = 'trash@mediaweb.ru';
     15
     16     static function set_fallback_sender($value) {
     17         self::$fallback_sender = validEmailAddr($value);
     18     }
     19
     20     static function get_fallback_sender() {
     21         return self::$fallback_sender;
     22     }
     23
     24     /**
     25      * Mail encoding
     26      */
     27     protected static $encoding ='base64';
     28
     29     static function set_encoding($val) {
     30         if ($val == 'base64' || $val == '8bit' || $val == '') {
     31             self::$encoding = $val;
     32         }
     33         else {
     34             die("Unsupported mail encoding: '$val'");
     35         }
     36     }
     37
     38     static function get_encoding() {
     39         return self::$encoding;
     40     }
     41
     42     /**
     43      * Send a plain-text email.
     44      *
     45      * @param string $to
     46      * @param string $from
     47      * @param string §subject
     48      * @param string $plainContent
     49      * @param bool $attachedFiles
     50      * @param array $customheaders
     51      * @return bool
     52      */
     53     function sendPlain($to, $from, $subject, $plainContent, $attachedFiles = false, $customheaders = false) {
     54         return plaintextEmail($to, $from, $subject, $plainContent, $attachedFiles, $customheaders);
     55     }
     56
     57     /**
     58      * Send a multi-part HTML email.
     59      *
     60      * @return bool
     61      */
     62     function sendHTML($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false, $plainContent = false, $inlineImages = false) {
     63         return htmlEmail($to, $from, $subject, $htmlContent, $attachedFiles, $customheaders, $plainContent, $inlineImages);
     64     }
     65
     66     /**
     67      * Заменяет отправителя в заголовках письма
     68      *
     69      * @param mixed $headers    строка или массив с заголовками письма
     70      * @param string $sender    новый отправитель (по-умолчанию @link Mailer::get_fallback_sender())
     71      *
     72      * @return mixed    обновленные заголовки
     73      */
     74     static function replace_sender($headers, $sender = false) {
     75         if (!$sender)
     76             $sender = self::get_fallback_sender();
     77
     78         // $sender may be a just email like 'user@domain.com' or have human name like '"User Name" <user@domain.com>'
     79         // Add hostname if not human name in $sender
     80         if (isset($_SERVER['HTTP_HOST']) && strpos($sender, '<') === false)
     81             $sender = sprintf('"%s" <%s>', $_SERVER['HTTP_HOST'], $sender);
     82
     83         return preg_replace('/(From|Sender): .*/i', '$1: ' . validEmailAddr($sender), $headers);
     84     }
     85 }
     86
     87 // TO DO: Clean this code up, make it more OO.
     88 // For now, we've just put a clean interface around this dirty code :)
     89
     90 /*
     91  * Sends an email as a both HTML and plaintext
     92  *   $attachedFiles should be an array of file names
     93  *    - if you pass the entire $_FILES entry, the user-uploaded filename will be preserved
     94  *   use $plainContent to override default plain-content generation
     95  *
     96  * @return bool
     97  */
     98 function htmlEmail($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false, $plainContent = false, $inlineImages = false) {
     99     if ($customheaders && is_array($customheaders) == false) {
    100         echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
    101         dieprintr($headers);
    102     }
    103
    104     $from = validEmailAddr($from);
    105     $to = validEmailAddr($to);
    106
    107     $subjectIsUnicode = true;
    108     $bodyIsUnicode = true;
    109     $plainEncoding = Mailer::get_encoding();
    110
    111     // We generate plaintext content by default, but you can pass custom stuff
    112     if(!$plainContent) {
    113         $plainContent = Convert::xml2raw($htmlContent);
    114     }
    115
    116
    117     // If the subject line contains extended characters, we must encode the
    118     $subject = Convert::xml2raw($subject);
    119     if(isset($subjectIsUnicode) && $subjectIsUnicode)
    120         $subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
    121
    122
    123     // Make the plain text part
    124     $headers["Content-Type"] = "text/plain; charset=\"utf-8\"";
    125     $headers["Content-Transfer-Encoding"] = $plainEncoding ? $plainEncoding : "8bit";
    126
    127     $plainPart = processHeaders($headers, ($plainEncoding == "base64") ? chunk_split(base64_encode($plainContent),60) : wordwrap($plainContent,120));
    128
    129     // Make the HTML part
    130     $headers["Content-Type"] = "text/html; charset=\"utf-8\"";
    131
    132
    133     // Add basic wrapper tags if the body tag hasn't been given
    134     if(stripos($htmlContent, '<body') === false) {
    135         $htmlContent =
    136             "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" .
    137             "<HTML><HEAD>\n" .
    138             "<META http-equiv=Content-Type content=\"text/html; charset=utf-8\">\n" .
    139             "<STYLE type=3Dtext/css></STYLE>\n\n".
    140             "</HEAD>\n" .
    141             "<BODY bgColor=#ffffff>\n" .
    142                 $htmlContent .
    143             "\n</BODY>\n" .
    144             "</HTML>";
    145     }
    146
    147     if($inlineImages) {
    148         $htmlPart = wrapImagesInline($htmlContent);
    149     } else {
    150         $headers["Content-Transfer-Encoding"] = $plainEncoding ? $plainEncoding : "8bit";
    151         $htmlPart = processHeaders($headers, ($plainEncoding == "base64") ? chunk_split(base64_encode($htmlContent),120) : wordwrap($htmlContent,120));
    152 //      $headers["Content-Transfer-Encoding"] = "base64";
    153 //      $htmlPart = processHeaders($headers, chunk_split(base64_encode($htmlContent),120));
    154     }
    155
    156     list($messageBody, $messageHeaders) = encodeMultipart(array($plainPart,$htmlPart), "multipart/alternative");
    157
    158     // Messages with attachments are handled differently
    159     if($attachedFiles && is_array($attachedFiles)) {
    160
    161         // The first part is the message itself
    162         $fullMessage = processHeaders($messageHeaders, $messageBody);
    163         $messageParts = array($fullMessage);
    164
    165         // Include any specified attachments as additional parts
    166         foreach($attachedFiles as $file) {
    167             if(isset($file['tmp_name']) && isset($file['name'])) {
    168                 $messageParts[] = encodeFileForEmail($file['tmp_name'], $file['name']);
    169             } else {
    170                 $messageParts[] = encodeFileForEmail($file);
    171             }
    172         }
    173
    174         // We further wrap all of this into another multipart block
    175         list($fullBody, $headers) = encodeMultipart($messageParts, "multipart/mixed");
    176
    177     // Messages without attachments do not require such treatment
    178     } else {
    179         $headers = $messageHeaders;
    180         $fullBody = $messageBody;
    181     }
    182
    183     // Email headers
    184     $headers["From"] = $from;
    185     $headers["Reply-To"] = $from;
    186
    187     // Messages with the X-SilverStripeMessageID header can be tracked
    188     if(isset($customheaders["X-SilverStripeMessageID"]) && defined('BOUNCE_EMAIL')) {
    189             $bounceAddress = BOUNCE_EMAIL;
    190     } else {
    191             $bounceAddress = $from;
    192     }
    193
    194     // Strip the human name from the bounce address
    195     if(ereg('^([^<>]*)<([^<>]+)> *$', $bounceAddress, $parts)) $bounceAddress = $parts[2];
    196
    197     // $headers["Sender"]       = $from;
    198     $headers["X-Mailer"]    = X_MAILER;
    199     if (!isset($customheaders["X-Priority"])) $headers["X-Priority"]    = 3;
    200
    201     $headers = array_merge((array)$headers, (array)$customheaders);
    202
    203     // the carbon copy header has to be 'Cc', not 'CC' or 'cc' -- ensure this.
    204     if (isset($headers['CC'])) { $headers['Cc'] = $headers['CC']; unset($headers['CC']); }
    205     if (isset($headers['cc'])) { $headers['Cc'] = $headers['cc']; unset($headers['cc']); }
    206
    207     // the carbon copy header has to be 'Bcc', not 'BCC' or 'bcc' -- ensure this.
    208     if (isset($headers['BCC'])) {$headers['Bcc']=$headers['BCC']; unset($headers['BCC']); }
    209     if (isset($headers['bcc'])) {$headers['Bcc']=$headers['bcc']; unset($headers['bcc']); }
    210
    211     // Send the email
    212     $headers = processHeaders($headers);
    213
    214     // Try it without the -f option if it fails
    215     if (!($result = @mail($to, $subject, $fullBody, $headers, "-f$bounceAddress"))) {
    216         $result = mail($to, $subject, $fullBody, Mailer::replace_sender($headers));
    217     }
    218
    219     return $result;
    220 }
    221
    222 /*
    223  * Send a plain text e-mail
    224  */
    225 function plaintextEmail($to, $from, $subject, $plainContent, $attachedFiles, $customheaders = false) {
    226     $subjectIsUnicode = false;
    227     $plainEncoding = Mailer::get_encoding();
    228
    229     if ($customheaders && is_array($customheaders) == false) {
    230         echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
    231         dieprintr($headers);
    232     }
    233
    234     $from = validEmailAddr($from);
    235     $to = validEmailAddr($to);
    236
    237     if(strpos($subject,"&#") !== false) $subjectIsUnicode = true;
    238
    239     // If the subject line contains extended characters, we must encode it
    240     $subject = Convert::xml2raw($subject);
    241     if($subjectIsUnicode)
    242         $subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
    243
    244
    245     // Make the plain text part
    246     $headers["Content-Type"] = "text/plain; charset=\"utf-8\"";
    247     $headers["Content-Transfer-Encoding"] = $plainEncoding ? $plainEncoding : '8bit';
    248
    249     $plainContent = ($plainEncoding == "base64") ? chunk_split(base64_encode($plainContent),60) : wordwrap($plainContent, 120);
    250
    251     // Messages with attachments are handled differently
    252     if(is_array($attachedFiles)) {
    253         // The first part is the message itself
    254         $fullMessage = processHeaders($headers, $plainContent);
    255         $messageParts = array($fullMessage);
    256
    257         // Include any specified attachments as additional parts
    258         foreach($attachedFiles as $file) {
    259             if(isset($file['tmp_name']) && isset($file['name'])) {
    260                 $messageParts[] = encodeFileForEmail($file['tmp_name'], $file['name']);
    261             } else {
    262                 $messageParts[] = encodeFileForEmail($file);
    263             }
    264         }
    265
    266
    267         // We further wrap all of this into another multipart block
    268         list($fullBody, $headers) = encodeMultipart($messageParts, "multipart/mixed");
    269
    270     // Messages without attachments do not require such treatment
    271     } else {
    272         $fullBody = $plainContent;
    273     }
    274
    275     // Email headers
    276     $headers["From"] = $from;
    277     $headers["Reply-To"] = $from;
    278
    279     // Messages with the X-SilverStripeMessageID header can be tracked
    280     if(isset($customheaders["X-SilverStripeMessageID"]) && defined('BOUNCE_EMAIL')) {
    281         $bounceAddress = BOUNCE_EMAIL;
    282         // Get the human name from the from address, if there is one
    283         if(ereg('^([^<>]+)<([^<>])> *$', $from, $parts))
    284             $bounceAddress = "$parts[1]<$bounceAddress>";
    285     } else {
    286         $bounceAddress = $from;
    287     }
    288
    289     // $headers["Sender"]       = $from;
    290     $headers["X-Mailer"]    = X_MAILER;
    291     if(!isset($customheaders["X-Priority"])) {
    292         $headers["X-Priority"]  = 3;
    293     }
    294
    295     $headers = array_merge((array)$headers, (array)$customheaders);
    296
    297     // the carbon copy header has to be 'Cc', not 'CC' or 'cc' -- ensure this.
    298     if (isset($headers['CC'])) { $headers['Cc'] = $headers['CC']; unset($headers['CC']); }
    299     if (isset($headers['cc'])) { $headers['Cc'] = $headers['cc']; unset($headers['cc']); }
    300
    301     // Send the email
    302     $headers = processHeaders($headers);
    303
    304     // Try it without the -f option if it fails
    305     if(!$result = @mail($to, $subject, $fullBody, $headers, "-f$bounceAddress")) {
    306         $result = mail($to, $subject, $fullBody, Mailer::replace_sender($headers));
    307     }
    308
    309     return $result;
    310 }
    311
    312
    313 function encodeMultipart($parts, $contentType, $headers = false) {
    314     $separator = "----=_NextPart_" . ereg_replace('[^0-9]','',rand() * 10000000000);
    315
    316
    317     $headers["MIME-Version"] = "1.0";
    318     $headers["Content-Type"] = "$contentType; boundary=\"$separator\"";
    319     $headers["Content-Transfer-Encoding"] = "8bit";
    320
    321     if($contentType == "multipart/alternative") {
    322         //$baseMessage = "This is an encoded HTML message.  There are two parts: a plain text and an HTML message, open whatever suits you better.";
    323         $baseMessage = "\nThis is a multi-part message in MIME format.";
    324     } else {
    325         //$baseMessage = "This is a message containing attachments.  The e-mail body is contained in the first attachment";
    326         $baseMessage = "\nThis is a multi-part message in MIME format.";
    327     }
    328
    329
    330     $separator = "\n--$separator\n";
    331     $body = "$baseMessage\n" .
    332         $separator . implode("\n".$separator, $parts) . "\n" . trim($separator) . "--";
    333
    334     return array($body, $headers);
    335 }
    336
    337 /*
    338  * Return a multipart/related e-mail chunk for the given HTML message and its linked images
    339  * Decodes absolute URLs, accessing the appropriate local images
    340  */
    341 function wrapImagesInline($htmlContent) {
    342     global $_INLINED_IMAGES;
    343     $_INLINED_IMAGES = null;
    344
    345     $replacedContent = imageRewriter($htmlContent, 'wrapImagesInline_rewriter($URL)');
    346
    347
    348     // Make the HTML part
    349     $headers["Content-Type"] = "text/html; charset=\"utf-8\"";
    350     $headers["Content-Transfer-Encoding"] = "quoted-printable";
    351     $multiparts[] = processHeaders($headers, QuotedPrintable_encode($replacedContent));
    352
    353     // Make all the image parts
    354     global $_INLINED_IMAGES;
    355     foreach($_INLINED_IMAGES as $url => $cid) {
    356         $multiparts[] = encodeFileForEmail($url, false, "inline", "Content-ID: <$cid>\n");
    357     }
    358
    359     // Merge together in a multipart
    360     list($body, $headers) = encodeMultipart($multiparts, "multipart/related");
    361     return processHeaders($headers, $body);
    362 }
    363 function wrapImagesInline_rewriter($url) {
    364     $url = relativiseURL($url);
    365
    366     global $_INLINED_IMAGES;
    367     if(!$_INLINED_IMAGES[$url]) {
    368         $identifier = "automatedmessage." . rand(1000,1000000000) . "@silverstripe.com";
    369         $_INLINED_IMAGES[$url] = $identifier;
    370     }
    371     return "cid:" . $_INLINED_IMAGES[$url];
    372
    373 }
    374
    375 /*
    376  * Combine headers w/ the body into a single string.
    377  */
    378 function processHeaders($headers, $body = false) {
    379     $res = '';
    380     if(is_array($headers)) while(list($k, $v) = each($headers))
    381         $res .= "$k: $v\n";
    382     if($body) $res .= "\n$body";
    383     return $res;
    384 }
    385
    386 /**
    387  * Encode the contents of a file for emailing, including headers
    388  *
    389  * $file can be an array, in which case it expects these members:
    390  *   'filename'        - the filename of the file
    391  *   'contents'        - the raw binary contents of the file as a string
    392  *  and can optionally include these members:
    393  *   'mimetype'        - the mimetype of the file (calculated from filename if missing)
    394  *   'contentLocation' - the 'Content-Location' header value for the file
    395  *
    396  * $file can also be a string, in which case it is assumed to be the filename
    397  *
    398  * h5. contentLocation
    399  *
    400  * Content Location is one of the two methods allowed for embedding images into an html email. It's also the simplest, and best supported
    401  *
    402  * Assume we have an email with this in the body:
    403  *
    404  *   <img src="http://example.com/image.gif" />
    405  *
    406  * To display the image, an email viewer would have to download the image from the web every time it is displayed. Due to privacy issues, most
    407  * viewers will not display any images unless the user clicks 'Show images in this email'. Not optimal.
    408  *
    409  * However, we can also include a copy of this image as an attached file in the email. By giving it a contentLocation of "http://example.com/image.gif"
    410  * most email viewers will use this attached copy instead of downloading it. Better, most viewers will show it without a 'Show images in this email'
    411  * conformation.
    412  *
    413  * Here is an example of passing this information through Email.php:
    414  *
    415  *   $email = new Email();
    416  *   $email->attachments[] = array(
    417  *     'filename' => BASE_PATH . "/themes/mytheme/images/header.gif",
    418  *     'contents' => file_get_contents(BASE_PATH . "/themes/mytheme/images/header.gif"),
    419  *     'mimetype' => 'image/gif',
    420  *     'contentLocation' => Director::absoluteBaseURL() . "/themes/mytheme/images/header.gif"
    421  *   );
    422  *
    423  */
    424 function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") {
    425     if(!$file) {
    426         user_error("encodeFileForEmail: not passed a filename and/or data", E_USER_WARNING);
    427         return;
    428     }
    429
    430     if (is_string($file)) {
    431         $file = array('filename' => $file);
    432         $fh = fopen($file['filename'], "rb");
    433         if ($fh) {
    434             while(!feof($fh)) $file['contents'] .= fread($fh, 10000);
    435             fclose($fh);
    436         }
    437     }
    438
    439     // Build headers, including content type
    440     if(!$destFileName) $base = basename($file['filename']);
    441     else $base = $destFileName;
    442
    443     $mimeType = $file['mimetype'] ? $file['mimetype'] : getMimeType($file['filename']);
    444     if(!$mimeType) $mimeType = "application/unknown";
    445
    446     if (empty($disposition)) $disposition = isset($file['contentLocation']) ? 'inline' : 'attachment';
    447
    448     // Encode for emailing
    449     if (substr($file['mimetype'], 0, 4) != 'text') {
    450         $encoding = "base64";
    451         $file['contents'] = chunk_split(base64_encode($file['contents']));
    452     } else {
    453         // This mime type is needed, otherwise some clients will show it as an inline attachment
    454         $mimeType = 'application/octet-stream';
    455         $encoding = "quoted-printable";
    456         $file['contents'] = QuotedPrintable_encode($file['contents']);
    457     }
    458
    459     $headers = "Content-type: $mimeType;\n\tname=\"$base\"\n".
    460                "Content-Transfer-Encoding: $encoding\n".
    461                "Content-Disposition: $disposition;\n\tfilename=\"$base\"\n" ;
    462
    463     if ( isset($file['contentLocation']) ) $headers .= 'Content-Location: ' . $file['contentLocation'] . "\n" ;
    464
    465     $headers .= $extraHeaders . "\n";
    466
    467     // Return completed packet
    468     return $headers . $file['contents'];
    469 }
    470
    471 function QuotedPrintable_encode($quotprint) {
    472         $quotprint = (string) str_replace('\r\n',chr(13).chr(10),$quotprint);
    473         $quotprint = (string) str_replace('\n',  chr(13).chr(10),$quotprint);
    474         $quotprint = (string) preg_replace("~([\x01-\x1F\x3D\x7F-\xFF])~e", "sprintf('=%02X', ord('\\1'))", $quotprint);
    475         //$quotprint = (string) str_replace('\=0D=0A',"=0D=0A",$quotprint);
    476         $quotprint = (string) str_replace('=0D=0A',"\n",$quotprint);
    477         $quotprint = (string) str_replace('=0A=0D',"\n",$quotprint);
    478         $quotprint = (string) str_replace('=0D',"\n",$quotprint);
    479         $quotprint = (string) str_replace('=0A',"\n",$quotprint);
    480         return (string) $quotprint;
    481 }
    482
    483 function validEmailAddr($emailAddress) {
    484     $emailAddress = trim($emailAddress);
    485     $angBrack = strpos($emailAddress, '<');
    486
    487     if($angBrack === 0) {
    488         $emailAddress = substr($emailAddress, 1, strpos($emailAddress,'>')-1);
    489
    490     } else if($angBrack) {
    491         $emailAddress = str_replace('@', '', substr($emailAddress, 0, $angBrack))
    492                             .substr($emailAddress, $angBrack);
    493     }
    494
    495     return $emailAddress;
    496 }
    497
    498 /*
    499  * Get mime type based on extension
    500  */
    501 function getMimeType($filename) {
    502     global $global_mimetypes;
    503     if(!$global_mimetypes) loadMimeTypes();
    504     $ext = strtolower(substr($filename,strrpos($filename,'.')+1));
    505     return isset($global_mimetypes[$ext]) ? $global_mimetypes[$ext] : 'application/octet-stream';
    506 }
    507
    508 /*
    509  * Load the mime-type data from the system file
    510  */
    511 function loadMimeTypes() {
    512     $mimetypePathCustom = '/etc/mime.types';
    513     $mimetypePathGeneric = Director::baseFolder() . '/sapphire/email/mime.types';
    514     $mimeTypes = file_exists($mimetypePathGeneric) ?  file($mimetypePathGeneric) : file($mimetypePathCustom);
    515     foreach($mimeTypes as $typeSpec) {
    516         if(($typeSpec = trim($typeSpec)) && substr($typeSpec,0,1) != "#") {
    517             $parts = preg_split("/[ \t\r\n]+/", $typeSpec);
    518             if(sizeof($parts) > 1) {
    519                 $mimeType = array_shift($parts);
    520                 foreach($parts as $ext) {
    521                     $ext = strtolower($ext);
    522                     $mimeData[$ext] = $mimeType;
    523                 }
    524             }
    525         }
    526     }
    527
    528     global $global_mimetypes;
    529     $global_mimetypes = $mimeData;
    530     return $mimeData;
    531 }
    532
    533

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -