root/fEmail.php

Revision 899, 48.4 kB (checked in by wbond, 17 hours ago)

Added a check to fEmail to prevent permissions warnings when getting the FQDN on Windows machines

LineHide Line Numbers
1 <?php
2 /**
3  * Allows creating and sending a single email containing plaintext, HTML, attachments and S/MIME encryption
4  *
5  * Please note that this class uses the [http://php.net/function.mail mail()]
6  * function by default. Developers that are sending multiple emails, or need
7  * SMTP support, should use fSMTP with this class.
8  *
9  * This class is implemented to use the UTF-8 character encoding. Please see
10  * http://flourishlib.com/docs/UTF-8 for more information.
11  *
12  * @copyright  Copyright (c) 2008-2010 Will Bond, others
13  * @author     Will Bond [wb] <will@flourishlib.com>
14  * @author     Bill Bushee, iMarc LLC [bb-imarc] <bill@imarc.net>
15  * @license    http://flourishlib.com/license
16  *
17  * @package    Flourish
18  * @link       http://flourishlib.com/fEmail
19  *
20  * @version    1.0.0b21
21  * @changes    1.0.0b21  Added a check to prevent permissions warnings when getting the FQDN on Windows machines [wb, 2010-09-02]
22  * @changes    1.0.0b20  Fixed ::send() to only remove the name of a recipient when dealing with the `mail()` function on Windows and to leave it when using fSMTP [wb, 2010-06-22]
23  * @changes    1.0.0b19  Changed ::send() to return the message id for the email, fixed the email regexes to require [] around IPs [wb, 2010-05-05]
24  * @changes    1.0.0b18  Fixed the name of the static method ::unindentExpand() [wb, 2010-04-28]
25  * @changes    1.0.0b17  Added the static method ::unindentExpand() [wb, 2010-04-26]
26  * @changes    1.0.0b16  Added support for sending emails via fSMTP [wb, 2010-04-20]
27  * @changes    1.0.0b15  Added the `$unindent_expand_constants` parameter to ::setBody(), added ::loadBody() and ::loadHTMLBody(), fixed HTML emails with attachments [wb, 2010-03-14]
28  * @changes    1.0.0b14  Changed ::send() to not double `.`s at the beginning of lines on Windows since it seemed to break things rather than fix them [wb, 2010-03-05]
29  * @changes    1.0.0b13  Fixed the class to work when safe mode is turned on [wb, 2009-10-23]
30  * @changes    1.0.0b12  Removed duplicate MIME-Version headers that were being included in S/MIME encrypted emails [wb, 2009-10-05]
31  * @changes    1.0.0b11  Updated to use the new fValidationException API [wb, 2009-09-17]
32  * @changes    1.0.0b10  Fixed a bug with sending both an HTML and a plaintext body [bb-imarc, 2009-06-18]
33  * @changes    1.0.0b9   Fixed a bug where the MIME headers were not being set for all emails [wb, 2009-06-12]
34  * @changes    1.0.0b8   Added the method ::clearRecipients() [wb, 2009-05-29]
35  * @changes    1.0.0b7   Email names with UTF-8 characters are now properly encoded [wb, 2009-05-08]
36  * @changes    1.0.0b6   Fixed a bug where <> quoted email addresses in validation messages were not showing [wb, 2009-03-27]
37  * @changes    1.0.0b5   Updated for new fCore API [wb, 2009-02-16]
38  * @changes    1.0.0b4   The recipient error message in ::validate() no longer contains a typo [wb, 2009-02-09]
39  * @changes    1.0.0b3   Fixed a bug with missing content in the fValidationException thrown by ::validate() [wb, 2009-01-14]
40  * @changes    1.0.0b2   Fixed a few bugs with sending S/MIME encrypted/signed emails [wb, 2009-01-10]
41  * @changes    1.0.0b    The initial implementation [wb, 2008-06-23]
42  */
43 class fEmail
44 {
45     // The following constants allow for nice looking callbacks to static methods
46     const fixQmail       = 'fEmail::fixQmail';
47     const reset          = 'fEmail::reset';
48     const unindentExpand = 'fEmail::unindentExpand';
49    
50    
51     /**
52     * A regular expression to match an email address, exluding those with comments and folding whitespace
53     *
54     * The matches will be:
55    
56     *  - `[0]`: The whole email address
57     *  - `[1]`: The name before the `@`
58     *  - `[2]`: The domain/ip after the `@`
59     *
60     * @var string
61     */
62     const EMAIL_REGEX = '~^[ \t]*(                                                                      # Allow leading whitespace
63                            (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")                       # An "atom" or a quoted string
64                            (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))*    # A . plus another "atom" or a quoted string, any number of times
65                          )@(                                                                            # The @ symbol
66                            (?:[a-z0-9\\-]+\.)+[a-z]{2,}|                                                # Domain name
67                            \[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\]  # (or) IP addresses
68                          )[ \t]*$~ixD';                                                                 # Allow Trailing whitespace
69    
70     /**
71     * A regular expression to match a `name <email>` string, exluding those with comments and folding whitespace
72     *
73     * The matches will be:
74     *
75     *  - `[0]`: The whole name and email address
76     *  - `[1]`: The name
77     *  - `[2]`: The whole email address
78     *  - `[3]`: The email username before the `@`
79     *  - `[4]`: The email domain/ip after the `@`
80     *
81     * @var string
82     */
83     const NAME_EMAIL_REGEX = '~^[ \t]*(                                                                            # Allow leading whitespace
84                                 (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*)                 # An "atom" or a quoted string
85                                 (?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*))*)  # Another "atom" or a quoted string or a . followed by one of those, any number of times
86                               [ \t]*<[ \t]*((                                                                      # The < encapsulating the email address
87                                 (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")                             # An "atom" or a quoted string
88                                 (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))*          # A . plus another "atom" or a quoted string, any number of times
89                               )@(                                                                                  # The @ symbol
90                                 (?:[a-z0-9\\-]+\.)+[a-z]{2,}|                                                      # Domain nam
91                                 \[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\]        # (or) IP addresses
92                               ))[ \t]*>[ \t]*$~ixD';                                                               # Closing > and trailing whitespace
93    
94    
95     /**
96     * Flags if the class should use [http://php.net/popen popen()] to send mail via sendmail
97     *
98     * @var boolean
99     */
100     static private $popen_sendmail = FALSE;
101    
102     /**
103     * Flags if the class should convert `\r\n` to `\n` for qmail. This makes invalid email headers that may work.
104     *
105     * @var boolean
106     */
107     static private $convert_crlf  = FALSE;
108    
109     /**
110     * The local hostname, used for message ids
111     */
112     static private $local_hostname;
113    
114    
115     /**
116     * Composes text using fText if loaded
117     *
118     * @param  string  $message    The message to compose
119     * @param  mixed   $component  A string or number to insert into the message
120     * @param  mixed   ...
121     * @return string  The composed and possible translated message
122     */
123     static protected function compose($message)
124     {
125         $args = array_slice(func_get_args(), 1);
126        
127         if (class_exists('fText', FALSE)) {
128             return call_user_func_array(
129                 array('fText', 'compose'),
130                 array($message, $args)
131             );
132         } else {
133             return vsprintf($message, $args);
134         }
135     }
136    
137    
138     /**
139     * Sets the class to try and fix broken qmail implementations that add `\r` to `\r\n`
140     *
141     * Before trying to fix qmail with this method, please try using fSMTP
142     * to connect to `localhost` and pass the fSMTP object to ::send().
143     *
144     * @return void
145     */
146     static public function fixQmail()
147     {
148         if (fCore::checkOS('windows')) {
149             return;
150         }
151        
152         $sendmail_command = ini_get('sendmail_path');
153        
154         if (!$sendmail_command) {
155             self::$convert_crlf = TRUE;
156             trigger_error(
157                 self::compose('The proper fix for sending through qmail is not possible since the sendmail path is not set'),
158                 E_USER_WARNING
159             );
160             trigger_error(
161                 self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
162                 E_USER_WARNING
163             );
164         }
165        
166         $sendmail_command_parts = explode(' ', $sendmail_command, 2);
167        
168         $sendmail_path   = $sendmail_command_parts[0];
169         $sendmail_dir    = pathinfo($sendmail_path, PATHINFO_DIRNAME);
170         $sendmail_params = (isset($sendmail_command_parts[1])) ? $sendmail_command_parts[1] : '';
171        
172         // Check to see if we can run sendmail via popen
173         $executable = FALSE;
174         $safe_mode  = FALSE;
175        
176         if (!in_array(strtolower(ini_get('safe_mode')), array('0', '', 'off'))) {
177             $safe_mode = TRUE;
178             $exec_dirs = explode(';', ini_get('safe_mode_exec_dir'));
179             foreach ($exec_dirs as $exec_dir) {
180                 if (stripos($sendmail_dir, $exec_dir) !== 0) {
181                     continue;
182                 }
183                 if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
184                     $executable = TRUE;
185                 }
186             }
187            
188         } else {
189             if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
190                 $executable = TRUE;
191             }
192         }
193        
194         if ($executable) {
195             self::$popen_sendmail = TRUE;
196         } else {
197             self::$convert_crlf   = TRUE;
198             if ($safe_mode) {
199                 trigger_error(
200                     self::compose('The proper fix for sending through qmail is not possible since safe mode is turned on and the sendmail binary is not in one of the paths defined by the safe_mode_exec_dir ini setting'),
201                     E_USER_WARNING
202                 );
203                 trigger_error(
204                     self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
205                     E_USER_WARNING
206                 );
207             } else {
208                 trigger_error(
209                     self::compose('The proper fix for sending through qmail is not possible since the sendmail binary could not be found or is not executable'),
210                     E_USER_WARNING
211                 );
212                 trigger_error(
213                     self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
214                     E_USER_WARNING
215                 );
216             }
217         }
218     }
219    
220    
221     /**
222     * Resets the configuration of the class
223     *
224     * @internal
225      *
226     * @return void
227     */
228     static public function reset()
229     {
230         self::$popen_sendmail = FALSE;
231         self::$convert_crlf   = FALSE;
232     }
233    
234    
235     /**
236     * Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
237     *
238     * @param  mixed $value  The value to check
239     * @return boolean  If the value is string-like
240     */
241     static protected function stringlike($value)
242     {
243         if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
244             return FALSE;   
245         }
246        
247         return TRUE;
248     }
249    
250    
251     /**
252     * Takes a block of text, unindents it and replaces {CONSTANT} tokens with the constant's value
253     *
254     * @param string $text  The text to unindent and replace constants in
255     * @return string  The unindented text
256     */
257     static public function unindentExpand($text)
258     {
259         $text = preg_replace('#^[ \t]*\n|\n[ \t]*$#D', '', $text);
260            
261         if (preg_match('#^[ \t]+(?=\S)#m', $text, $match)) {
262             $text = preg_replace('#^' . preg_quote($match[0]) . '#m', '', $text);
263         }
264        
265         preg_match_all('#\{([a-z][a-z0-9_]*)\}#i', $text, $constants, PREG_SET_ORDER);
266         foreach ($constants as $constant) {
267             if (!defined($constant[1])) { continue; }
268             $text = preg_replace('#' . preg_quote($constant[0], '#') . '#', constant($constant[1]), $text, 1);
269         }
270        
271         return $text;
272     }
273    
274    
275     /**
276     * The file contents to attach
277     *
278     * @var array
279     */
280     private $attachments = array();
281    
282     /**
283     * The email address(es) to BCC to
284     *
285     * @var array
286     */
287     private $bcc_emails = array();
288    
289     /**
290     * The email address to bounce to
291     *
292     * @var string
293     */
294     private $bounce_to_email = NULL;
295    
296     /**
297     * The email address(es) to CC to
298     *
299     * @var array
300     */
301     private $cc_emails = array();
302    
303     /**
304     * The email address being sent from
305     *
306     * @var string
307     */
308     private $from_email = NULL;
309    
310     /**
311     * The HTML body of the email
312     *
313     * @var string
314     */
315     private $html_body = NULL;
316    
317     /**
318     * The plaintext body of the email
319     *
320     * @var string
321     */
322     private $plaintext_body = NULL;
323    
324     /**
325     * The recipient's S/MIME PEM certificate filename, used for encryption of the message
326     *
327     * @var string
328     */
329     private $recipients_smime_cert_file = NULL;
330    
331     /**
332     * The email address to reply to
333     *
334     * @var string
335     */
336     private $reply_to_email = NULL;
337    
338     /**
339     * The email address actually sending the email
340     *
341     * @var string
342     */
343     private $sender_email = NULL;
344    
345     /**
346     * The senders's S/MIME PEM certificate filename, used for singing the message
347     *
348     * @var string
349     */
350     private $senders_smime_cert_file = NULL;
351    
352     /**
353     * The senders's S/MIME private key filename, used for singing the message
354     *
355     * @var string
356     */
357     private $senders_smime_pk_file = NULL;
358    
359     /**
360     * The senders's S/MIME private key password, used for singing the message
361     *
362     * @var string
363     */
364     private $senders_smime_pk_password = NULL;
365    
366     /**
367     * If the message should be encrypted using the recipient's S/MIME certificate
368     *
369     * @var boolean
370     */
371     private $smime_encrypt = FALSE;
372    
373     /**
374     * If the message should be signed using the senders's S/MIME private key
375     *
376     * @var boolean
377     */
378     private $smime_sign = FALSE;
379    
380     /**
381     * The subject of the email
382     *
383     * @var string
384     */
385     private $subject = NULL;
386    
387     /**
388     * The email address(es) to send to
389     *
390     * @var array
391     */
392     private $to_emails = array();
393    
394    
395     /**
396     * Initializes fEmail for creating message ids
397     *
398     * @return fEmail
399     */
400     public function __construct()
401     {
402         if (self::$local_hostname !== NULL) {
403             return;
404         }
405        
406         if (isset($_ENV['HOST'])) {
407             self::$local_hostname = $_ENV['HOST'];
408         }
409         if (strpos(self::$local_hostname, '.') === FALSE && isset($_ENV['HOSTNAME'])) {
410             self::$local_hostname = $_ENV['HOSTNAME'];
411         }
412         if (strpos(self::$local_hostname, '.') === FALSE) {
413             self::$local_hostname = php_uname('n');
414         }
415         if (strpos(self::$local_hostname, '.') === FALSE && !in_array('exec', array_map('trim', explode(',', ini_get('disable_functions')))) && !ini_get('safe_mode') && !ini_get('open_basedir')) {
416             if (fCore::checkOS('linux')) {
417                 self::$local_hostname = trim(shell_exec('hostname --fqdn'));
418             } elseif (fCore::checkOS('windows') && is_executable($_SERVER['WINDIR'] . '\system32\ipconfig.exe')) {
419                 $output = shell_exec('ipconfig /all');
420                 if (preg_match('#DNS Suffix Search List[ .:]+([a-z0-9_.-]+)#i', $output, $match)) {
421                     self::$local_hostname .= '.' . $match[1];
422                 }
423             } elseif (fCore::checkOS('bsd', 'osx') && file_exists('/etc/resolv.conf')) {
424                 $output = file_get_contents('/etc/resolv.conf');
425                 if (preg_match('#^domain ([a-z0-9_.-]+)#im', $output, $match)) {
426                     self::$local_hostname .= '.' . $match[1];
427                 }
428             }
429         }
430     }
431    
432    
433     /**
434     * All requests that hit this method should be requests for callbacks
435     *
436     * @internal
437      *
438     * @param  string $method  The method to create a callback for
439     * @return callback  The callback for the method requested
440     */
441     public function __get($method)
442     {
443         return array($this, $method);       
444     }
445    
446    
447     /**
448     * Adds an attachment to the email
449     *
450     * If a duplicate filename is detected, it will be changed to be unique.
451     *
452     * @param  string $filename   The name of the file to attach
453     * @param  string $mime_type  The mime type of the file
454     * @param  string $contents   The contents of the file
455     * @return void
456     */
457     public function addAttachment($filename, $mime_type, $contents)
458     {
459         if (!self::stringlike($filename)) {
460             throw new fProgrammerException(
461                 'The filename specified, %s, does not appear to be a valid filename',
462                 $filename
463             );
464         }
465        
466         $filename = (string) $filename;
467        
468         $i = 1;
469         while (isset($this->attachments[$filename])) {
470             $filename_info = fFilesystem::getPathInfo($filename);
471             $extension     = ($filename_info['extension']) ? '.' . $filename_info['extension'] : '';
472             $filename      = preg_replace('#_copy\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension;
473             $i++;
474         }
475        
476         $this->attachments[$filename] = array(
477             'mime-type' => $mime_type,
478             'contents'  => $contents
479         );
480     }
481    
482    
483     /**
484     * Adds a blind carbon copy (BCC) email recipient
485     *
486     * @param  string $email  The email address to BCC
487     * @param  string $name   The recipient's name
488     * @return void
489     */
490     public function addBCCRecipient($email, $name=NULL)
491     {
492         if (!$email) {
493             return;
494         }
495        
496         $this->bcc_emails[] = $this->combineNameEmail($name, $email);
497     }
498    
499    
500     /**
501     * Adds a carbon copy (CC) email recipient
502     *
503     * @param  string $email  The email address to BCC
504     * @param  string $name   The recipient's name
505     * @return void
506     */
507     public function addCCRecipient($email, $name=NULL)
508     {
509         if (!$email) {
510             return;
511         }
512        
513         $this->cc_emails[] = $this->combineNameEmail($name, $email);
514     }
515    
516    
517     /**
518     * Adds an email recipient
519     *
520     * @param  string $email  The email address to send to
521     * @param  string $name   The recipient's name
522     * @return void
523     */
524     public function addRecipient($email, $name=NULL)
525     {
526         if (!$email) {
527             return;
528         }
529        
530         $this->to_emails[] = $this->combineNameEmail($name, $email);
531     }
532    
533    
534     /**
535     * Takes a multi-address email header and builds it out using an array of emails
536     *
537     * @param  string $header  The header name without `': '`, the header is non-blank, `': '` will be added
538     * @param  array  $emails  The email addresses for the header
539     * @return string  The email header with a trailing `\r\n`
540     */
541     private function buildMultiAddressHeader($header, $emails)
542     {
543         if ($header) {
544             $header .= ': ';
545         }
546        
547         $first = TRUE;
548         $line  = 1;
549         foreach ($emails as $email) {
550             if ($first) { $first = FALSE; } else { $header .= ', '; }
551            
552             // Make sure we don't go past the 978 char limit for email headers
553             if (strlen($header . $email) / 950 > $line) {
554                 $header .= "\r\n ";
555                 $line++;
556             }
557            
558             $header .= trim($email);
559         }
560        
561         return $header . "\r\n";
562     }
563    
564    
565     /**
566     * Removes all To, CC and BCC recipients from the email
567     *
568     * @return void
569     */
570     public function clearRecipients()
571     {
572         $this->to_emails  = array();
573         $this->cc_emails  = array();
574         $this->bcc_emails = array();
575     }
576    
577    
578     /**
579     * Creates a 32-character boundary for a multipart message
580     *
581     * @return string  A multipart boundary
582     */
583     private function createBoundary()
584     {
585         $chars      = 'ancdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-_';
586         $last_index = strlen($chars) - 1;
587         $output     = '';
588        
589         for ($i = 0; $i < 28; $i++) {
590             $output .= $chars[rand(0, $last_index)];
591         }
592         return $output;
593     }
594    
595    
596     /**
597     * Turns a name and email into a `"name" <email>` string, or just `email` if no name is provided
598     *
599     * This method will remove newline characters from the name and email, and
600     * will remove any backslash (`\`) and double quote (`"`) characters from
601     * the name.
602     *
603     * @param  string $name   The name associated with the email address
604     * @param  string $email  The email address
605     * @return string  The '"name" <email>' or 'email' string
606     */
607     private function combineNameEmail($name, $email)
608     {
609         // Strip lower ascii character since they aren't useful in email addresses
610         $email = preg_replace('#[\x0-\x19]+#', '', $email);
611         $name  = preg_replace('#[\x0-\x19]+#', '', $name);
612        
613         if (!$name) {
614             return $email;
615         }
616        
617         // If the name contains any non-ascii bytes or stuff not allowed
618         // in quoted strings we just make an encoded word out of it
619         if (preg_replace('#[\x80-\xff\x5C\x22]#', '', $name) != $name) {
620             $name = $this->makeEncodedWord($name);
621         } else {
622             $name = '"' . $name . '"';   
623         }
624        
625         return $name . ' <' . $email . '>';
626     }
627    
628    
629     /**
630     * Builds the body of the email
631     *
632     * @param  string $boundary  The boundary to use for the top level mime block
633     * @return string  The message body to be sent to the mail() function
634     */
635     private function createBody($boundary)
636     {
637         $mime_notice = self::compose(
638             "This message has been formatted using MIME. It does not appear that your\r\nemail client supports MIME."
639         );
640        
641         $body = '';
642        
643         // Build the multi-part/alternative section for the plaintext/HTML combo
644         if ($this->html_body) {
645            
646             $alternative_boundary = $boundary;
647            
648             // Depending on the other content, we may need to create a new boundary
649             if ($this->attachments) {
650                 $alternative_boundary = $this->createBoundary();
651                 $body    .= 'Content-Type: multipart/alternative; boundary="' . $alternative_boundary . "\"\r\n\r\n";
652             } else {
653                 $body .= $mime_notice . "\r\n\r\n";
654             }
655            
656             $body .= '--' . $alternative_boundary . "\r\n";
657             $body .= "Content-Type: text/plain; charset=utf-8\r\n";
658             $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
659             $body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
660             $body .= '--' . $alternative_boundary . "\r\n";
661             $body .= "Content-Type: text/html; charset=utf-8\r\n";
662             $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
663             $body .= $this->makeQuotedPrintable($this->html_body) . "\r\n";
664             $body .= '--' . $alternative_boundary . "--\r\n";
665        
666         // If there is no HTML, just encode the body
667         } else {
668            
669             // Depending on the other content, these headers may be inline or in the real headers
670             if ($this->attachments) {
671                 $body .= "Content-Type: text/plain; charset=utf-8\r\n";
672                 $body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
673             }
674            
675             $body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
676         }
677        
678         // If we have attachments, we need to wrap a multipart/mixed around the current body
679         if ($this->attachments) {
680            
681             $multipart_body  = $mime_notice . "\r\n\r\n";
682             $multipart_body .= '--' . $boundary . "\r\n";
683             $multipart_body .= $body;
684            
685             foreach ($this->attachments as $filename => $file_info) {
686                 $multipart_body .= '--' . $boundary . "\r\n";
687                 $multipart_body .= 'Content-Type: ' . $file_info['mime-type'] . "\r\n";
688                 $multipart_body .= "Content-Transfer-Encoding: base64\r\n";
689                 $multipart_body .= 'Content-Disposition: attachment; filename="' . $filename . "\";\r\n\r\n";
690                 $multipart_body .= $this->makeBase64($file_info['contents']) . "\r\n";
691             }
692            
693             $multipart_body .= '--' . $boundary . "--\r\n";
694            
695             $body = $multipart_body;
696         }
697        
698         return $body;
699     }
700    
701    
702     /**
703     * Builds the headers for the email
704     *
705     * @param  string $boundary    The boundary to use for the top level mime block
706     * @param  string $message_id  The message id for the message
707     * @return string  The headers to be sent to the [http://php.net/function.mail mail()] function
708     */
709     private function createHeaders($boundary, $message_id)
710     {
711         $headers = '';
712        
713         if ($this->cc_emails) {
714             $headers .= $this->buildMultiAddressHeader("Cc", $this->cc_emails);
715         }
716        
717         if ($this->bcc_emails) {
718             $headers .= $this->buildMultiAddressHeader("Bcc", $this->bcc_emails);
719         }
720        
721         $headers .= "From: " . trim($this->from_email) . "\r\n";
722        
723         if ($this->reply_to_email) {
724             $headers .= "Reply-To: " . trim($this->reply_to_email) . "\r\n";
725         }
726        
727         if ($this->sender_email) {
728             $headers .= "Sender: " . trim($this->sender_email) . "\r\n";
729         }
730        
731         $headers .= "Message-ID: " . $message_id . "\r\n";
732         $headers .= "MIME-Version: 1.0\r\n";
733        
734         if ($this->html_body && !$this->attachments) {
735             $headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n";
736         }
737        
738         if (!$this->html_body && !$this->attachments) {
739             $headers .= "Content-Type: text/plain; charset=utf-8\r\n";
740             $headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
741         }
742        
743         if ($this->attachments) {
744             $headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . "\"\r\n";
745         }
746        
747         return $headers . "\r\n";
748     }
749    
750    
751     /**
752     * Takes the body of the message and processes it with S/MIME
753     *
754     * @param  string $to       The recipients being sent to
755     * @param  string $subject  The subject of the email
756     * @param  string $headers  The headers for the message
757     * @param  string $body     The message body
758     * @return array  `0` => The message headers, `1` => The message body
759     */
760     private function createSMIMEBody($to, $subject, $headers, $body)
761     {
762         if (!$this->smime_encrypt && !$this->smime_sign) {
763             return array($headers, $body);
764         }
765        
766         $plaintext_file  = tempnam('', '__fEmail_');
767         $ciphertext_file = tempnam('', '__fEmail_');
768        
769         $headers_array = array(
770             'To'      => $to,
771             'Subject' => $subject
772         );
773        
774         preg_match_all('#^([\w\-]+):\s+([^\n]+\n( [^\n]+\n)*)#im', $headers, $header_matches, PREG_SET_ORDER);
775         foreach ($header_matches as $header_match) {
776             $headers_array[$header_match[1]] = trim($header_match[2]);
777         }
778        
779         $body_headers = "";
780         if (isset($headers_array['Content-Type'])) {
781             $body_headers .= 'Content-Type: ' . $headers_array['Content-Type'] . "\r\n";
782         }
783         if (isset($headers_array['Content-Transfer-Encoding'])) {
784             $body_headers .= 'Content-Transfer-Encoding: ' . $headers_array['Content-Transfer-Encoding'] . "\r\n";
785         }
786        
787         if ($body_headers) {
788             $body = $body_headers . "\r\n" . $body;
789         }
790        
791         file_put_contents($plaintext_file, $body);
792         file_put_contents($ciphertext_file, '');
793        
794         // Set up the neccessary S/MIME resources
795         if ($this->smime_sign) {
796             $senders_smime_cert  = file_get_contents($this->senders_smime_cert_file);
797             $senders_private_key = openssl_pkey_get_private(
798                 file_get_contents($this->senders_smime_pk_file),
799                 $this->senders_smime_pk_password
800             );
801            
802             if ($senders_private_key === FALSE) {
803                 throw new fValidationException(
804                     "The sender's S/MIME private key password specified does not appear to be valid for the private key"
805                 );
806             }
807         }
808        
809         if ($this->smime_encrypt) {
810             $recipients_smime_cert = file_get_contents($this->recipients_smime_cert_file);
811         }
812        
813        
814         // If we are going to sign and encrypt, the best way is to sign, encrypt and then sign again
815         if ($this->smime_encrypt && $this->smime_sign) {
816             openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, array());
817             openssl_pkcs7_encrypt($ciphertext_file, $plaintext_file, $recipients_smime_cert, array(), NULL, OPENSSL_CIPHER_RC2_128);
818             openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
819        
820         } elseif ($this->smime_sign) {
821             openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
822          
823         } elseif ($this->smime_encrypt) {
824             openssl_pkcs7_encrypt($plaintext_file, $ciphertext_file, $recipients_smime_cert, $headers_array, NULL, OPENSSL_CIPHER_RC2_128);
825         }
826        
827         // It seems that the contents of the ciphertext is not always \r\n line breaks
828         $message = file_get_contents($ciphertext_file);
829         $message = str_replace("\r\n", "\n", $message);
830         $message = str_replace("\r", "\n", $message);
831         $message = str_replace("\n", "\r\n", $message);
832        
833         list($new_headers, $new_body) = explode("\r\n\r\n", $message, 2);
834        
835         $new_headers = preg_replace('#^To:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
836         $new_headers = preg_replace('#^Subject:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
837         $new_headers = preg_replace("#^MIME-Version: 1.0\r?\n#mi", '', $new_headers, 1);
838         $new_headers = preg_replace('#^Content-Type:\s+' . preg_quote($headers_array['Content-Type'], '#') . "\r?\n#mi", '', $new_headers);
839         $new_headers = preg_replace('#^Content-Transfer-Encoding:\s+' . preg_quote($headers_array['Content-Transfer-Encoding'], '#') . "\r?\n#mi", '', $new_headers);
840        
841         unlink($plaintext_file);
842         unlink($ciphertext_file);
843        
844         if ($this->smime_sign) {
845             openssl_pkey_free($senders_private_key);
846         }
847                                  
848         return array($new_headers, $new_body);
849     }
850    
851    
852     /**
853     * Sets the email to be encrypted with S/MIME
854     *
855     * @param  string $recipients_smime_cert_file  The file path to the PEM-encoded S/MIME certificate for the recipient
856     * @return void
857     */
858     public function encrypt($recipients_smime_cert_file)
859     {
860         if (!extension_loaded('openssl')) {
861             throw new fEnvironmentException(
862                 'S/MIME encryption was requested for an email, but the %s extension is not installed',
863                 'openssl'
864             );
865         }
866        
867         if (!self::stringlike($recipients_smime_cert_file)) {
868             throw new fProgrammerException(
869                 "The recipient's S/MIME certificate filename specified, %s, does not appear to be a valid filename",
870                 $recipients_smime_cert_file
871             );
872         }
873        
874         $this->smime_encrypt              = TRUE;
875         $this->recipients_smime_cert_file = $recipients_smime_cert_file;
876     }
877    
878    
879     /**
880     * Extracts just the email addresses from an array of strings containing an
881     * <email@address.com> or "Name" <email@address.com> combination.
882     *
883     * @param array $list  The list of email or name/email to extract from
884     * @return array  The email addresses
885     */
886     private function extractEmails($list)
887     {
888         $output = array();
889         foreach ($list as $email) {
890             if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
891                 $output[] = $match[2];
892             } else {
893                 preg_match(self::EMAIL_REGEX, $email, $match);
894                 $output[] = $match[0];
895             }
896         }
897         return $output;
898     }
899    
900    
901     /**
902     * Loads the plaintext version of the email body from a file and applies replacements
903     *
904     * The should contain either ASCII or UTF-8 encoded text. Please see
905     * http://flourishlib.com/docs/UTF-8 for more information.
906     *
907     * @throws fValidationException  When no file was specified, the file does not exist or the path specified is not a file
908     *
909     * @param  string|fFile $file          The plaintext version of the email body
910     * @param  array        $replacements  The method will search the contents of the file for each key and replace it with the corresponding value
911     * @return void
912     */
913     public function loadBody($file, $replacements=array())
914     {
915         if (!$file instanceof fFile) {
916             $file = new fFile($file);   
917         }
918        
919         $plaintext = $file->read();
920         if ($replacements) {
921             $plaintext = strtr($plaintext, $replacements);   
922         }
923        
924         $this->plaintext_body = $plaintext;
925     }
926    
927    
928     /**
929     * Loads the plaintext version of the email body from a file and applies replacements
930     *
931     * The should contain either ASCII or UTF-8 encoded text. Please see
932     * http://flourishlib.com/docs/UTF-8 for more information.
933     *
934     * @throws fValidationException  When no file was specified, the file does not exist or the path specified is not a file
935     *
936     * @param  string|fFile $file          The plaintext version of the email body
937     * @param  array        $replacements  The method will search the contents of the file for each key and replace it with the corresponding value
938     * @return void
939     */
940     public function loadHTMLBody($file, $replacements=array())
941     {
942         if (!$file instanceof fFile) {
943             $file = new fFile($file);   
944         }
945        
946         $html = $file->read();
947         if ($replacements) {
948             $html = strtr($html, $replacements);   
949         }
950        
951         $this->html_body = $html;
952     }
953    
954    
955     /**
956     * Encodes a string to base64
957     *
958     * @param  string  $content  The content to encode
959     * @return string  The encoded string
960     */
961     private function makeBase64($content)
962     {
963         return chunk_split(base64_encode($content));
964     }
965    
966    
967     /**
968     * Encodes a string to UTF-8 encoded-word
969     *
970     * @param  string  $content  The content to encode
971     * @return string  The encoded string
972     */
973     private function makeEncodedWord($content)
974     {
975         // Homogenize the line-endings to CRLF
976         $content = str_replace("\r\n", "\n", $content);
977         $content = str_replace("\r", "\n", $content);
978         $content = str_replace("\n", "\r\n", $content);
979        
980         // A quick a dirty hex encoding
981         $content = rawurlencode($content);
982         $content = str_replace('=', '%3D', $content);
983         $content = str_replace('%', '=', $content);
984        
985         // Decode characters that don't have to be coded
986         $decodings = array(
987             '=20' => '_', '=21' => '!', '=22' => '"''=23' => '#',
988             '=24' => '$', '=25' => '%', '=26' => '&''=27' => "'",
989             '=28' => '(', '=29' => ')', '=2A' => '*''=2B' => '+',
990             '=2C' => ',', '=2D' => '-', '=2E' => '.''=2F' => '/',
991             '=3A' => ':', '=3B' => ';', '=3C' => '<''=3E' => '>',
992             '=40' => '@', '=5B' => '[', '=5C' => '\\', '=5D' => ']',
993             '=5E' => '^', '=60' => '`', '=7B' => '{''=7C' => '|',
994             '=7D' => '}', '=7E' => '~', ' '   => '_'
995         );
996        
997         $content = strtr($content, $decodings);
998        
999         $length = strlen($content);
1000        
1001         $prefix = '=?utf-8?Q?';
1002         $suffix = '?=';
1003        
1004         $prefix_length = 10;
1005         $suffix_length = 2;
1006        
1007         // This loop goes through and ensures we are wrapping by 75 chars
1008         // including the encoded word delimiters
1009         $output = $prefix;
1010         $line_length = $prefix_length;
1011        
1012         for ($i=0; $i<$length; $i++) {
1013            
1014             // Get info about the next character
1015             $char_length = ($content[$i] == '=') ? 3 : 1;
1016             $char        = $content[$i];
1017             if ($char_length == 3) {
1018                 $char .= $content[$i+1] . $content[$i+2];
1019             }
1020            
1021             // If we have too long a line, wrap it
1022             if ($line_length + $suffix_length + $char_length > 75) {
1023                 $output .= $suffix . "\r\n " . $prefix;
1024                 $line_length = $prefix_length + 2;
1025             }
1026            
1027             // Add the character
1028             $output .= $char;
1029            
1030             // Figure out how much longer the line is
1031             $line_length += $char_length;
1032            
1033             // Skip characters if we have an encoded character
1034             $i += $char_length-1;
1035         }
1036        
1037         if (substr($output, -2) != $suffix) {
1038             $output .= $suffix;
1039         }
1040        
1041         return $output;
1042     }
1043    
1044    
1045     /**
1046     * Encodes a string to quoted-printable, properly handles UTF-8
1047     *
1048     * @param  string  $content  The content to encode
1049     * @return string  The encoded string
1050     */
1051     private function makeQuotedPrintable($content)
1052     {
1053         // Homogenize the line-endings to CRLF
1054         $content = str_replace("\r\n", "\n", $content);
1055         $content = str_replace("\r", "\n", $content);
1056         $content = str_replace("\n", "\r\n", $content);
1057        
1058         // A quick a dirty hex encoding
1059         $content = rawurlencode($content);
1060         $content = str_replace('=', '%3D', $content);
1061         $content = str_replace('%', '=', $content);
1062        
1063         // Decode characters that don't have to be coded
1064         $decodings = array(
1065             '=20' => ' ', '=21' => '!', '=22' => '"', '=23' => '#',
1066             '=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
1067             '=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
1068             '=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
1069             '=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
1070             '=3F' => '?', '=40' => '@', '=5B' => '[', '=5C' => '\\',
1071             '=5D' => ']', '=5E' => '^', '=5F' => '_', '=60' => '`',
1072             '=7B' => '{', '=7C' => '|', '=7D' => '}', '=7E' => '~'
1073         );
1074        
1075         $content = strtr($content, $decodings);
1076        
1077         $output = '';
1078        
1079         $length = strlen($content);
1080        
1081         // This loop goes through and ensures we are wrapping by 76 chars
1082         $line_length = 0;
1083         for ($i=0; $i<$length; $i++) {
1084            
1085             // Get info about the next character
1086             $char_length = ($content[$i] == '=') ? 3 : 1;
1087             $char        = $content[$i];
1088             if ($char_length == 3) {
1089                 $char .= $content[$i+1] . $content[$i+2];
1090             }
1091            
1092             // Skip characters if we have an encoded character, this must be
1093             // done before checking for whitespace at the beginning and end of
1094             // lines or else characters in the content will be skipped
1095             $i += $char_length-1;
1096            
1097             // Spaces and tabs at the beginning and ending of lines have to be encoded
1098             $begining_or_end = $line_length > 69 || $line_length == 0;
1099             $tab_or_space    = $char == ' ' || $char == "\t";
1100             if ($begining_or_end && $tab_or_space) {
1101                 $char_length = 3;
1102                 $char        = ($char == ' ') ? '=20' : '=09';
1103             }
1104            
1105             // If we have too long a line, wrap it
1106             if ($char != "\r" && $char != "\n" && $line_length + $char_length > 75) {
1107                 $output .= "=\r\n";
1108                 $line_length = 0;
1109             }
1110            
1111             // Add the character
1112             $output .= $char;
1113            
1114             // Figure out how much longer the line is now
1115             if ($char == "\r" || $char == "\n") {
1116                 $line_length = 0;
1117             } else {
1118                 $line_length += $char_length;
1119             }
1120         }
1121        
1122         return $output;
1123     }
1124    
1125    
1126     /**
1127     * Sends the email
1128     *
1129     * The return value is the message id, which should be included as the
1130     * `Message-ID` header of the email. While almost all SMTP servers will not
1131     * modify this value, testing has indicated at least one (smtp.live.com
1132     * for Windows Live Mail) does.
1133     *
1134     * @throws fValidationException  When ::validate() throws an exception
1135     *
1136     * @param  fSMTP $connection  The SMTP connection to send the message over
1137     * @return string  The message id for the message - see method description for details
1138     */
1139     public function send($connection=NULL)
1140     {
1141         $this->validate();
1142        
1143         // The mail() function on Windows doesn't support names in headers so
1144         // we must strip them down to just the email address
1145         if ($connection === NULL && fCore::checkOS('windows')) {
1146             $vars = array('bcc_emails', 'bounce_to_email', 'cc_emails', 'from_email', 'reply_to_email', 'sender_email', 'to_emails');
1147             foreach ($vars as $var) {
1148                 if (!is_array($this->$var)) {
1149                     if (preg_match(self::NAME_EMAIL_REGEX, $this->$var, $match)) {
1150                         $this->$var = $match[2];
1151                     }
1152                 } else {
1153                     $new_emails = array();
1154                     foreach ($this->$var as $email) {
1155                         if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
1156                             $email = $match[2];
1157                         }
1158                         $new_emails[] = $email;
1159                     }
1160                     $this->$var = $new_emails;
1161                 }
1162             }
1163         }
1164        
1165         $to = trim($this->buildMultiAddressHeader("", $this->to_emails));
1166        
1167         $message_id         = '<' . fCryptography::randomString(32, 'hexadecimal') . '@' . self::$local_hostname . '>';
1168         $top_level_boundary = $this->createBoundary();
1169         $headers            = $this->createHeaders($top_level_boundary, $message_id);
1170        
1171         $subject = str_replace(array("\r", "\n"), '', $this->subject);
1172         $subject = $this->makeEncodedWord($subject);
1173        
1174         $body = $this->createBody($top_level_boundary);
1175        
1176         if ($this->smime_encrypt || $this->smime_sign) {
1177             list($headers, $body) = $this->createSMIMEBody($to, $subject, $headers, $body);
1178         }
1179        
1180         // Remove extra line breaks
1181         $headers = trim($headers);
1182         $body    = trim($body);
1183        
1184         if ($connection) {
1185             $to_emails = $this->extractEmails($this->to_emails);
1186             $to_emails = array_merge($to_emails, $this->extractEmails($this->cc_emails));
1187             $to_emails = array_merge($to_emails, $this->extractEmails($this->bcc_emails));
1188             $from = $this->bounce_to_email ? $this->bounce_to_email : current($this->extractEmails(array($this->from_email)));
1189             $connection->send($from, $to_emails, "To: " . $to . "\r\nSubject: " . $subject . "\r\n" . $headers, $body);
1190             return $message_id;
1191         }
1192        
1193         // Sendmail when not in safe mode will allow you to set the envelope from address via the -f parameter
1194         $parameters = NULL;
1195         if (!fCore::checkOS('windows') && $this->bounce_to_email) {
1196             preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
1197             $parameters = '-f ' . $matches[0];
1198        
1199         // Windows takes the Return-Path email from the sendmail_from ini setting
1200         } elseif (fCore::checkOS('windows') && $this->bounce_to_email) {
1201             $old_sendmail_from = ini_get('sendmail_from');
1202             preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
1203             ini_set('sendmail_from', $matches[0]);
1204         }
1205        
1206         // This is a gross qmail fix that is a last resort
1207         if (self::$popen_sendmail || self::$convert_crlf) {
1208             $to      = str_replace("\r\n", "\n", $to);
1209             $subject = str_replace("\r\n", "\n", $subject);
1210             $body    = str_replace("\r\n", "\n", $body);
1211             $headers = str_replace("\r\n", "\n", $headers);
1212         }
1213        
1214         // If the user is using qmail and wants to try to fix the \r\r\n line break issue
1215         if (self::$popen_sendmail) {
1216             $sendmail_command = ini_get('sendmail_path');
1217             if ($parameters) {
1218                 $sendmail_command .= ' ' . $parameters;
1219             }
1220            
1221             $sendmail_process = popen($sendmail_command, 'w');
1222             fprintf($sendmail_process, "To: %s\n", $to);
1223             fprintf($sendmail_process, "Subject: %s\n", $subject);
1224             if ($headers) {
1225                 fprintf($sendmail_process, "%s\n", $headers);
1226             }
1227             fprintf($sendmail_process, "\n%s\n", $body);
1228             $error = pclose($sendmail_process);
1229            
1230         // This is the normal way to send mail
1231         } else {
1232             if ($parameters) {
1233                 $error = !mail($to, $subject, $body, $headers, $parameters);
1234             } else {
1235                 $error = !mail($to, $subject, $body, $headers);
1236             }
1237         }
1238        
1239         if (fCore::checkOS('windows') && $this->bounce_to_email) {
1240             ini_set('sendmail_from', $old_sendmail_from);
1241         }
1242        
1243         if ($error) {
1244             throw new fConnectivityException(
1245                 'An error occured while trying to send the email entitled %s',
1246                 $this->subject
1247             );
1248         }
1249        
1250         return $message_id;
1251     }
1252    
1253    
1254     /**
1255     * Sets the plaintext version of the email body
1256     *
1257     * This method accepts either ASCII or UTF-8 encoded text. Please see
1258     * http://flourishlib.com/docs/UTF-8 for more information.
1259     *
1260     * @param  string  $plaintext                  The plaintext version of the email body
1261     * @param  boolean $unindent_expand_constants  If this is `TRUE`, the body will be unindented as much as possible and {CONSTANT_NAME} will be replaced with the value of the constant
1262     * @return void
1263     */
1264     public function setBody($plaintext, $unindent_expand_constants=FALSE)
1265     {
1266         if ($unindent_expand_constants) {
1267             $plaintext = self::unindentExpand($plaintext);
1268         }
1269        
1270         $this->plaintext_body = $plaintext;
1271     }
1272    
1273    
1274     /**
1275     * Adds the email address the email will be bounced to
1276     *
1277     * This email address will be set to the `Return-Path` header.
1278     *
1279     * @param  string $email  The email address to bounce to
1280     * @return void
1281     */
1282     public function setBounceToEmail($email)
1283     {
1284         if (ini_get('safe_mode') && !fCore::checkOS('windows')) {
1285             throw new fProgrammerException('It is not possible to set a Bounce-To Email address when safe mode is enabled on a non-Windows server');
1286         }
1287         if (!$email) {
1288             return;
1289         }
1290        
1291         $this->bounce_to_email = $this->combineNameEmail('', $email);
1292     }
1293    
1294    
1295     /**
1296     * Adds the `From:` email address to the email
1297     *
1298     * @param  string $email  The email address being sent from
1299     * @param  string $name   The from email user's name - unfortunately on windows this is ignored
1300     * @return void
1301     */
1302     public function setFromEmail($email, $name=NULL)
1303     {
1304         if (!$email) {
1305             return;
1306         }
1307        
1308         $this->from_email = $this->combineNameEmail($name, $email);
1309     }
1310    
1311    
1312     /**
1313     * Sets the HTML version of the email body
1314     *
1315     * This method accepts either ASCII or UTF-8 encoded text. Please see
1316     * http://flourishlib.com/docs/UTF-8 for more information.
1317     *
1318     * @param  string $html  The HTML version of the email body
1319     * @return void
1320     */
1321     public function setHTMLBody($html)
1322     {
1323         $this->html_body = $html;
1324     }
1325    
1326    
1327     /**
1328     * Adds the `Reply-To:` email address to the email
1329     *
1330     * @param  string $email  The email address to reply to
1331     * @param  string $name   The reply-to email user's name
1332     * @return void
1333     */
1334     public function setReplyToEmail($email, $name=NULL)
1335     {
1336         if (!$email) {
1337             return;
1338         }
1339        
1340         $this->reply_to_email = $this->combineNameEmail($name, $email);
1341     }
1342    
1343    
1344     /**
1345     * Adds the `Sender:` email address to the email
1346     *
1347     * The `Sender:` header is used to indicate someone other than the `From:`
1348     * address is actually submitting the message to the network.
1349     *
1350     * @param  string $email  The email address the message is actually being sent from
1351     * @param  string $name   The sender email user's name
1352     * @return void
1353     */
1354     public function setSenderEmail($email, $name=NULL)
1355     {
1356         if (!$email) {
1357             return;
1358         }
1359        
1360         $this->sender_email = $this->combineNameEmail($name, $email);
1361     }
1362    
1363    
1364     /**
1365     * Sets the subject of the email
1366     *
1367     * This method accepts either ASCII or UTF-8 encoded text. Please see
1368     * http://flourishlib.com/docs/UTF-8 for more information.
1369     *
1370     * @param  string $subject  The subject of the email
1371     * @return void
1372     */
1373     public function setSubject($subject)
1374     {
1375         $this->subject = $subject;
1376     }
1377    
1378    
1379     /**
1380     * Sets the email to be signed with S/MIME
1381     *
1382     * @param  string $senders_smime_cert_file    The file path to the sender's PEM-encoded S/MIME certificate
1383     * @param  string $senders_smime_pk_file      The file path to the sender's S/MIME private key
1384     * @param  string $senders_smime_pk_password  The password for the sender's S/MIME private key
1385     * @return void
1386     */
1387     public function sign($senders_smime_cert_file, $senders_smime_pk_file, $senders_smime_pk_password)
1388     {
1389         if (!extension_loaded('openssl')) {
1390             throw new fEnvironmentException(
1391                 'An S/MIME signature was requested for an email, but the %s extension is not installed',
1392                 'openssl'
1393             );
1394         }
1395        
1396         if (!self::stringlike($senders_smime_cert_file)) {
1397             throw new fProgrammerException(
1398                 "The sender's S/MIME certificate file specified, %s, does not appear to be a valid filename",
1399                 $senders_smime_cert_file
1400             );
1401         }
1402         if (!file_exists($senders_smime_cert_file) || !is_readable($senders_smime_cert_file)) {
1403             throw new fEnvironmentException(
1404                 "The sender's S/MIME certificate file specified, %s, does not exist or could not be read",
1405                 $senders_smime_cert_file
1406             );
1407         }
1408        
1409         if (!self::stringlike($senders_smime_pk_file)) {
1410             throw new fProgrammerException(
1411                 "The sender's S/MIME primary key file specified, %s, does not appear to be a valid filename",
1412                 $senders_smime_pk_file
1413             );
1414         }
1415         if (!file_exists($senders_smime_pk_file) || !is_readable($senders_smime_pk_file)) {
1416             throw new fEnvironmentException(
1417                 "The sender's S/MIME primary key file specified, %s, does not exist or could not be read",
1418                 $senders_smime_pk_file
1419             );
1420         }
1421        
1422         $this->smime_sign                = TRUE;
1423         $this->senders_smime_cert_file   = $senders_smime_cert_file;
1424         $this->senders_smime_pk_file     = $senders_smime_pk_file;
1425         $this->senders_smime_pk_password = $senders_smime_pk_password;
1426     }
1427    
1428    
1429     /**
1430     * Validates that all of the parts of the email are valid
1431     *
1432     * @throws fValidationException  When part of the email is missing or formatted incorrectly
1433     *
1434     * @return void
1435     */
1436     private function validate()
1437     {
1438         $validation_messages = array();
1439        
1440         // Check all multi-address email field
1441         $multi_address_field_list = array(
1442             'to_emails'  => self::compose('recipient'),
1443             'cc_emails'  => self::compose('CC recipient'),
1444             'bcc_emails' => self::compose('BCC recipient')
1445         );
1446        
1447         foreach ($multi_address_field_list as $field => $name) {
1448             foreach ($this->$field as $email) {
1449                 if ($email && !preg_match(self::NAME_EMAIL_REGEX, $email) && !preg_match(self::EMAIL_REGEX, $email)) {
1450                     $validation_messages[] = htmlspecialchars(self::compose(
1451                         'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.',
1452                         $name,
1453                         $email
1454                     ), ENT_QUOTES, 'UTF-8');
1455                 }
1456             }
1457         }
1458        
1459         // Check all single-address email fields
1460         $single_address_field_list = array(
1461             'from_email'      => self::compose('From email address'),
1462             'reply_to_email'  => self::compose('Reply-To email address'),
1463             'sender_email'    => self::compose('Sender email address'),
1464             'bounce_to_email' => self::compose('Bounce-To email address')
1465         );
1466        
1467         foreach ($single_address_field_list as $field => $name) {
1468             if ($this->$field && !preg_match(self::NAME_EMAIL_REGEX, $this->$field) && !preg_match(self::EMAIL_REGEX, $this->$field)) {
1469                 $validation_messages[] = htmlspecialchars(self::compose(
1470                     'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.',
1471                     $name,
1472                     $this->$field
1473                 ), ENT_QUOTES, 'UTF-8');
1474             }
1475         }
1476        
1477         // Make sure the required fields are all set
1478         if (!$this->to_emails) {
1479             $validation_messages[] = self::compose(
1480                 "Please provide at least one recipient"
1481             );
1482         }
1483        
1484         if (!$this->from_email) {
1485             $validation_messages[] = self::compose(
1486                 "Please provide the from email address"
1487             );
1488         }
1489        
1490         if (!self::stringlike($this->subject)) {
1491             $validation_messages[] = self::compose(
1492                 "Please provide an email subject"
1493             );
1494         }
1495        
1496         if (strpos($this->subject, "\n") !== FALSE) {
1497             $validation_messages[] = self::compose(
1498                 "The subject contains one or more newline characters"
1499             );   
1500         }
1501        
1502         if (!self::stringlike($this->plaintext_body)) {
1503             $validation_messages[] = self::compose(
1504                 "Please provide a plaintext email body"
1505             );
1506         }
1507        
1508         // Make sure the attachments look good
1509         foreach ($this->attachments as $filename => $file_info) {
1510             if (!self::stringlike($file_info['mime-type'])) {
1511                 $validation_messages[] = self::compose(
1512                     "No mime-type was specified for the attachment %s",
1513                     $filename
1514                 );
1515             }
1516             if (!self::stringlike($file_info['contents'])) {
1517                 $validation_messages[] = self::compose(
1518                     "The attachment %s appears to be a blank file",
1519                     $filename
1520                 );
1521             }
1522         }
1523        
1524         if ($validation_messages) {
1525             throw new fValidationException(
1526                 'The email could not be sent because:',
1527                 $validation_messages
1528             );   
1529         }
1530     }
1531 }
1532  
1533  
1534  
1535 /**
1536  * Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>, others
1537  *
1538  * Permission is hereby granted, free of charge, to any person obtaining a copy
1539  * of this software and associated documentation files (the "Software"), to deal
1540  * in the Software without restriction, including without limitation the rights
1541  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1542  * copies of the Software, and to permit persons to whom the Software is
1543  * furnished to do so, subject to the following conditions:
1544  *
1545  * The above copyright notice and this permission notice shall be included in
1546  * all copies or substantial portions of the Software.
1547  *
1548  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1549  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1550  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1551  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1552  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1553  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1554  * THE SOFTWARE.
1555  */