From 1d045709e66a239d6a0933c0d07dfbb9fb257d48 Mon Sep 17 00:00:00 2001
From: Andreas Gohr <andi@splitbrain.org>
Date: Wed, 2 Nov 2011 19:19:11 +0100
Subject: [PATCH] The Mailer class should work now

It's still not real world tested but the output *looks* right.

Plugin hook support is still missing.
---
 inc/Mailer.class.php | 173 ++++++++++++++++++++++++++++++++++++-------
 1 file changed, 148 insertions(+), 25 deletions(-)

diff --git a/inc/Mailer.class.php b/inc/Mailer.class.php
index 991ec6c3e..420277d9e 100644
--- a/inc/Mailer.class.php
+++ b/inc/Mailer.class.php
@@ -1,15 +1,19 @@
 <?php
 /**
+ * A class to build and send multi part mails (with HTML content and embedded
+ * attachments). All mails are assumed to be in UTF-8 encoding.
+ *
+ * Attachments are handled in memory so this shouldn't be used to send huge
+ * files, but then again mail shouldn't be used to send huge files either.
+ *
  * @author Andreas Gohr <andi@splitbrain.org>
  */
 
-
 // end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?)
 // think different
 if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL',"\n");
 #define('MAILHEADER_ASCIIONLY',1);
 
-
 class Mailer {
 
     private $headers = array();
@@ -19,10 +23,23 @@ class Mailer {
 
     private $boundary = '';
     private $partid   = '';
-    private $sendparam= '';
+    private $sendparam= null;
+
+    private $validator = null;
+
+    /**
+     * Constructor
+     *
+     * Initializes the boundary strings and part counters
+     */
+    public function __construct(){
+        if(isset($_SERVER['SERVER_NAME'])){
+            $server = $_SERVER['SERVER_NAME'];
+        }else{
+            $server = 'localhost';
+        }
 
-    function __construct(){
-        $this->partid = md5(uniqid(rand(),true)).'@'.$_SERVER['SERVER_NAME'];
+        $this->partid = md5(uniqid(rand(),true)).'@'.$server;
         $this->boundary = '----------'.md5(uniqid(rand(),true));
     }
 
@@ -69,24 +86,50 @@ class Mailer {
         );
     }
 
+    /**
+     * Add an arbitrary header to the mail
+     *
+     * @param string $header the header name (no trailing colon!)
+     * @param string $value  the value of the header
+     * @param bool   $clean  remove all non-ASCII chars and line feeds?
+     */
+    public function setHeader($header,$value,$clean=true){
+        $header = ucwords(strtolower($header)); // streamline casing
+        if($clean){
+            $header = preg_replace('/[^\w \-\.\+\@]+/','',$header);
+            $value  = preg_replace('/[^\w \-\.\+\@]+/','',$value);
+        }
+        $this->headers[$header] = $value;
+    }
+
+    /**
+     * Set additional parameters to be passed to sendmail
+     *
+     * Whatever is set here is directly passed to PHP's mail() command as last
+     * parameter. Depending on the PHP setup this might break mailing alltogether
+     */
+    public function setParameters($param){
+        $this->sendparam = $param;
+    }
+
     /**
      * Set the HTML part of the mail
      *
      * Placeholders can be used to reference embedded attachments
      */
-    public function setHTMLBody($html){
+    public function setHTML($html){
         $this->html = $html;
     }
 
     /**
      * Set the plain text part of the mail
      */
-    public function setTextBody($text){
+    public function setText($text){
         $this->text = $text;
     }
 
     /**
-     * Ses an email address header with correct encoding
+     * Sets an email address header with correct encoding
      *
      * Unicode characters will be deaccented and encoded base64
      * for headers. Addresses may not contain Non-ASCII data!
@@ -97,10 +140,13 @@ class Mailer {
      * @param string  $address Multiple adresses separated by commas
      * @param string  $header  Name of the header (To,Bcc,Cc,...)
      */
-    function mail_encode_address($address,$header){
+    function setAddress($address,$header){
         // No named recipients for To: in Windows (see FS#652)
         $names = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? false : true;
 
+        $header = ucwords(strtolower($header)); // streamline casing
+        $address = preg_replace('/[\r\n\0]+/',' ',$address); // remove attack vectors
+
         $headers = '';
         $parts = explode(',',$address);
         foreach ($parts as $part){
@@ -124,7 +170,11 @@ class Mailer {
                 continue;
             }
 
-            if(!mail_isvalid($addr)){
+            if(is_null($this->validator)){
+                $this->validator = new EmailAddressValidator();
+                $this->validator->allowLocalAddresses = true;
+            }
+            if(!$this->validator->check_email_address($addr)){
                 msg(htmlspecialchars("E-Mail address <$addr> is not valid"),-1);
                 continue;
             }
@@ -140,7 +190,7 @@ class Mailer {
                 }
 
                 if(!utf8_isASCII($text)){
-                    //FIXME
+                    //FIXME check if this is needed for base64 too
                     // put the quotes outside as in =?UTF-8?Q?"Elan Ruusam=C3=A4e"?= vs "=?UTF-8?Q?Elan Ruusam=C3=A4e?="
                     /*
                     if (preg_match('/^"(.+)"$/', $text, $matches)) {
@@ -199,6 +249,19 @@ class Mailer {
         $this->setAddress($address, 'Bcc');
     }
 
+    /**
+     * Add the From: address
+     *
+     * This is set to $conf['mailfrom'] when not specified so you shouldn't need
+     * to call this function
+     *
+     * @see setAddress
+     * @param string  $address from address
+     */
+    public function from($address){
+        $this->setAddress($address, 'From');
+    }
+
     /**
      * Add the mail's Subject: header
      *
@@ -246,20 +309,33 @@ class Mailer {
         return $mime;
     }
 
-    protected function createBody(){
+    /**
+     * Build the body and handles multi part mails
+     *
+     * Needs to be called before prepareHeaders!
+     *
+     * @return string the prepared mail body, false on errors
+     */
+    protected function prepareBody(){
+        global $conf;
+
         // check for body
         if(!$this->text && !$this->html){
             return false;
         }
 
         // add general headers
+        if(!isset($this->headers['From'])) $this->from($conf['mailfrom']);
         $this->headers['MIME-Version'] = '1.0';
 
+        $body = '';
+
         if(!$this->html && !count($this->attach)){ // we can send a simple single part message
             $this->headers['Content-Type'] = 'text/plain; charset=UTF-8';
             $this->headers['Content-Transfer-Encoding'] = 'base64';
-            $body = chunk_split(base64_encode($this->text),74,MAILHEADER_EOL);
+            $body .= chunk_split(base64_encode($this->text),74,MAILHEADER_EOL);
         }else{ // multi part it is
+            $body .= "This is a multi-part message in MIME format.".MAILHEADER_EOL;
 
             // prepare the attachments
             $attachments = $this->prepareAttachments();
@@ -267,25 +343,21 @@ class Mailer {
             // do we have alternative text content?
             if($this->text && $this->html){
                 $this->headers['Content-Type'] = 'multipart/alternative; boundary="'.$this->boundary.'XX"';
-                $body  = "This is a multi-part message in MIME format.".MAILHEADER_EOL;
                 $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL;
+                $body .= 'Content-Type: text/plain; charset=UTF-8'.MAILHEADER_EOL;
+                $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL;
                 $body .= MAILHEADER_EOL;
-                $body .= 'Content-Type: text/plain; charset=UTF-8';
-                $body .= 'Content-Transfer-Encoding: base64';
                 $body .= chunk_split(base64_encode($this->text),74,MAILHEADER_EOL);
                 $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL;
                 $body .= 'Content-Type: multipart/related; boundary="'.$this->boundary.'"'.MAILHEADER_EOL;
                 $body .= MAILHEADER_EOL;
-            }else{
-                $this->headers['Content-Type'] = 'multipart/related; boundary="'.$this->boundary.'"';
-                $body  = "This is a multi-part message in MIME format.".MAILHEADER_EOL;
             }
 
-            $body .= '--'.$this->boundary."\n";
-            $body .= "Content-Type: text/html; charset=UTF-8\n";
-            $body .= "Content-Transfer-Encoding: base64\n";
+            $body .= '--'.$this->boundary.MAILHEADER_EOL;
+            $body .= 'Content-Type: text/html; charset=UTF-8'.MAILHEADER_EOL;
+            $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL;
             $body .= MAILHEADER_EOL;
-            $body = chunk_split(base64_encode($this->html),74,MAILHEADER_EOL);
+            $body .= chunk_split(base64_encode($this->html),74,MAILHEADER_EOL);
             $body .= MAILHEADER_EOL;
             $body .= $attachments;
             $body .= '--'.$this->boundary.'--'.MAILHEADER_EOL;
@@ -301,6 +373,8 @@ class Mailer {
 
     /**
      * Create a string from the headers array
+     *
+     * @returns string the headers
      */
     protected function prepareHeaders(){
         $headers = '';
@@ -313,12 +387,61 @@ class Mailer {
     /**
      * return a full email with all headers
      *
-     * This is mainly for debugging and testing
+     * This is mainly intended for debugging and testing but could also be
+     * used for MHT exports
+     *
+     * @return string the mail, false on errors
      */
     public function dump(){
-        $headers = $this->prepareHeaders();
         $body    = $this->prepareBody();
+        if($body === 'false') return false;
+        $headers = $this->prepareHeaders();
 
         return $headers.MAILHEADER_EOL.$body;
     }
+
+    /**
+     * Send the mail
+     *
+     * Call this after all data was set
+     *
+     * @fixme we need to support the old plugin hook here!
+     * @return bool true if the mail was successfully passed to the MTA
+     */
+    public function send(){
+        // any recipients?
+        if(trim($this->headers['To'])  === '' &&
+           trim($this->headers['Cc'])  === '' &&
+           trim($this->headers['Bcc']) === '') return false;
+
+        // The To: header is special
+        if(isset($this->headers['To'])){
+            $to = $this->headers['To'];
+            unset($this->headers['To']);
+        }else{
+            $to = '';
+        }
+
+        // so is the subject
+        if(isset($this->headers['Subject'])){
+            $subject = $this->headers['Subject'];
+            unset($this->headers['Subject']);
+        }else{
+            $subject = '';
+        }
+
+        // make the body
+        $body    = $this->prepareBody();
+        if($body === 'false') return false;
+
+        // cook the headers
+        $headers = $this->prepareHeaders();
+
+        // send the thing
+        if(is_null($this->sendparam)){
+            return @mail($to,$subject,$body,$headers);
+        }else{
+            return @mail($to,$subject,$body,$headers,$this->sendparam);
+        }
+    }
 }
-- 
GitLab