1<?php 2/** 3 * @author Andreas Gohr <andi@splitbrain.org> 4 */ 5 6 7// end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?) 8// think different 9if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL',"\n"); 10#define('MAILHEADER_ASCIIONLY',1); 11 12 13class Mailer { 14 15 private $headers = array(); 16 private $attach = array(); 17 private $html = ''; 18 private $text = ''; 19 20 private $boundary = ''; 21 private $partid = ''; 22 private $sendparam= ''; 23 24 function __construct(){ 25 $this->partid = md5(uniqid(rand(),true)).'@'.$_SERVER['SERVER_NAME']; 26 $this->boundary = '----------'.md5(uniqid(rand(),true)); 27 } 28 29 /** 30 * Attach a file 31 * 32 * @param $path Path to the file to attach 33 * @param $mime Mimetype of the attached file 34 * @param $name The filename to use 35 * @param $embed Unique key to reference this file from the HTML part 36 */ 37 public function attachFile($path,$mime,$name='',$embed=''){ 38 if(!$name){ 39 $name = basename($path); 40 } 41 42 $this->attach[] = array( 43 'data' => file_get_contents($path), 44 'mime' => $mime, 45 'name' => $name, 46 'embed' => $embed 47 ); 48 } 49 50 /** 51 * Attach a file 52 * 53 * @param $path The file contents to attach 54 * @param $mime Mimetype of the attached file 55 * @param $name The filename to use 56 * @param $embed Unique key to reference this file from the HTML part 57 */ 58 public function attachContent($data,$mime,$name='',$embed=''){ 59 if(!$name){ 60 list($junk,$ext) = split('/',$mime); 61 $name = count($this->attach).".$ext"; 62 } 63 64 $this->attach[] = array( 65 'data' => $data, 66 'mime' => $mime, 67 'name' => $name, 68 'embed' => $embed 69 ); 70 } 71 72 /** 73 * Set the HTML part of the mail 74 * 75 * Placeholders can be used to reference embedded attachments 76 */ 77 public function setHTMLBody($html){ 78 $this->html = $html; 79 } 80 81 /** 82 * Set the plain text part of the mail 83 */ 84 public function setTextBody($text){ 85 $this->text = $text; 86 } 87 88 /** 89 * Ses an email address header with correct encoding 90 * 91 * Unicode characters will be deaccented and encoded base64 92 * for headers. Addresses may not contain Non-ASCII data! 93 * 94 * Example: 95 * setAddress("föö <foo@bar.com>, me@somewhere.com","TBcc"); 96 * 97 * @param string $address Multiple adresses separated by commas 98 * @param string $header Name of the header (To,Bcc,Cc,...) 99 */ 100 function mail_encode_address($address,$header){ 101 // No named recipients for To: in Windows (see FS#652) 102 $names = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? false : true; 103 104 $headers = ''; 105 $parts = explode(',',$address); 106 foreach ($parts as $part){ 107 $part = trim($part); 108 109 // parse address 110 if(preg_match('#(.*?)<(.*?)>#',$part,$matches)){ 111 $text = trim($matches[1]); 112 $addr = $matches[2]; 113 }else{ 114 $addr = $part; 115 } 116 // skip empty ones 117 if(empty($addr)){ 118 continue; 119 } 120 121 // FIXME: is there a way to encode the localpart of a emailaddress? 122 if(!utf8_isASCII($addr)){ 123 msg(htmlspecialchars("E-Mail address <$addr> is not ASCII"),-1); 124 continue; 125 } 126 127 if(!mail_isvalid($addr)){ 128 msg(htmlspecialchars("E-Mail address <$addr> is not valid"),-1); 129 continue; 130 } 131 132 // text was given 133 if(!empty($text) && $names){ 134 // add address quotes 135 $addr = "<$addr>"; 136 137 if(defined('MAILHEADER_ASCIIONLY')){ 138 $text = utf8_deaccent($text); 139 $text = utf8_strip($text); 140 } 141 142 if(!utf8_isASCII($text)){ 143 //FIXME 144 // put the quotes outside as in =?UTF-8?Q?"Elan Ruusam=C3=A4e"?= vs "=?UTF-8?Q?Elan Ruusam=C3=A4e?=" 145 /* 146 if (preg_match('/^"(.+)"$/', $text, $matches)) { 147 $text = '"=?UTF-8?Q?'.mail_quotedprintable_encode($matches[1], 0).'?="'; 148 } else { 149 $text = '=?UTF-8?Q?'.mail_quotedprintable_encode($text, 0).'?='; 150 } 151 */ 152 $text = '=?UTF-8?B?'.base64_encode($text).'?='; 153 } 154 }else{ 155 $text = ''; 156 } 157 158 // add to header comma seperated 159 if($headers != ''){ 160 $headers .= ','; 161 $headers .= MAILHEADER_EOL.' '; // avoid overlong mail headers 162 } 163 $headers .= $text.' '.$addr; 164 } 165 166 if(empty($headers)) return false; 167 168 $this->headers[$header] = $headers; 169 return $headers; 170 } 171 172 /** 173 * Add the To: recipients 174 * 175 * @see setAddress 176 * @param string $address Multiple adresses separated by commas 177 */ 178 public function to($address){ 179 $this->setAddress($address, 'To'); 180 } 181 182 /** 183 * Add the Cc: recipients 184 * 185 * @see setAddress 186 * @param string $address Multiple adresses separated by commas 187 */ 188 public function cc($address){ 189 $this->setAddress($address, 'Cc'); 190 } 191 192 /** 193 * Add the Bcc: recipients 194 * 195 * @see setAddress 196 * @param string $address Multiple adresses separated by commas 197 */ 198 public function bcc($address){ 199 $this->setAddress($address, 'Bcc'); 200 } 201 202 /** 203 * Add the mail's Subject: header 204 * 205 * @param string $subject the mail subject 206 */ 207 public function subject($subject){ 208 if(!utf8_isASCII($subject)){ 209 $subject = '=?UTF-8?B?'.base64_encode($subject).'?='; 210 } 211 $this->headers['Subject'] = $subject; 212 } 213 214 /** 215 * Prepare the mime multiparts for all attachments 216 * 217 * Replaces placeholders in the HTML with the correct CIDs 218 */ 219 protected function prepareAttachments(){ 220 $mime = ''; 221 $part = 1; 222 // embedded attachments 223 foreach($this->attach as $media){ 224 // create content id 225 $cid = 'part'.$part.'.'.$this->partid; 226 227 // replace wildcards 228 if($media['embed']){ 229 $this->html = str_replace('%%'.$media['embed'].'%%','cid:'.$cid,$this->html); 230 } 231 232 $mime .= '--'.$this->boundary.MAILHEADER_EOL; 233 $mime .= 'Content-Type: '.$media['mime'].';'.MAILHEADER_EOL; 234 $mime .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; 235 $mime .= "Content-ID: <$cid>".MAILHEADER_EOL; 236 if($media['embed']){ 237 $mime .= 'Content-Disposition: inline; filename="'.$media['name'].'"'.MAILHEADER_EOL; 238 }else{ 239 $mime .= 'Content-Disposition: attachment; filename="'.$media['name'].'"'.MAILHEADER_EOL; 240 } 241 $mime .= MAILHEADER_EOL; //end of headers 242 $mime .= chunk_split(base64_encode($media['data']),74,MAILHEADER_EOL); 243 244 $part++; 245 } 246 return $mime; 247 } 248 249 protected function createBody(){ 250 // check for body 251 if(!$this->text && !$this->html){ 252 return false; 253 } 254 255 // add general headers 256 $this->headers['MIME-Version'] = '1.0'; 257 258 if(!$this->html && !count($this->attach)){ // we can send a simple single part message 259 $this->headers['Content-Type'] = 'text/plain; charset=UTF-8'; 260 $this->headers['Content-Transfer-Encoding'] = 'base64'; 261 $body = chunk_split(base64_encode($this->text),74,MAILHEADER_EOL); 262 }else{ // multi part it is 263 264 // prepare the attachments 265 $attachments = $this->prepareAttachments(); 266 267 // do we have alternative text content? 268 if($this->text && $this->html){ 269 $this->headers['Content-Type'] = 'multipart/alternative; boundary="'.$this->boundary.'XX"'; 270 $body = "This is a multi-part message in MIME format.".MAILHEADER_EOL; 271 $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; 272 $body .= MAILHEADER_EOL; 273 $body .= 'Content-Type: text/plain; charset=UTF-8'; 274 $body .= 'Content-Transfer-Encoding: base64'; 275 $body .= chunk_split(base64_encode($this->text),74,MAILHEADER_EOL); 276 $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; 277 $body .= 'Content-Type: multipart/related; boundary="'.$this->boundary.'"'.MAILHEADER_EOL; 278 $body .= MAILHEADER_EOL; 279 }else{ 280 $this->headers['Content-Type'] = 'multipart/related; boundary="'.$this->boundary.'"'; 281 $body = "This is a multi-part message in MIME format.".MAILHEADER_EOL; 282 } 283 284 $body .= '--'.$this->boundary."\n"; 285 $body .= "Content-Type: text/html; charset=UTF-8\n"; 286 $body .= "Content-Transfer-Encoding: base64\n"; 287 $body .= MAILHEADER_EOL; 288 $body = chunk_split(base64_encode($this->html),74,MAILHEADER_EOL); 289 $body .= MAILHEADER_EOL; 290 $body .= $attachments; 291 $body .= '--'.$this->boundary.'--'.MAILHEADER_EOL; 292 293 // close open multipart/alternative boundary 294 if($this->text && $this->html){ 295 $body .= '--'.$this->boundary.'XX--'.MAILHEADER_EOL; 296 } 297 } 298 299 return $body; 300 } 301 302 /** 303 * Create a string from the headers array 304 */ 305 protected function prepareHeaders(){ 306 $headers = ''; 307 foreach($this->headers as $key => $val){ 308 $headers .= "$key: $val".MAILHEADER_EOL; 309 } 310 return $headers; 311 } 312 313 /** 314 * return a full email with all headers 315 * 316 * This is mainly for debugging and testing 317 */ 318 public function dump(){ 319 $headers = $this->prepareHeaders(); 320 $body = $this->prepareBody(); 321 322 return $headers.MAILHEADER_EOL.$body; 323 } 324} 325