splitbrain /
dokuwiki
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * A class to build and send multi part mails (with HTML content and embedded |
||
| 4 | * attachments). All mails are assumed to be in UTF-8 encoding. |
||
| 5 | * |
||
| 6 | * Attachments are handled in memory so this shouldn't be used to send huge |
||
| 7 | * files, but then again mail shouldn't be used to send huge files either. |
||
| 8 | * |
||
| 9 | * @author Andreas Gohr <[email protected]> |
||
| 10 | */ |
||
| 11 | |||
| 12 | // end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?) |
||
| 13 | // think different |
||
| 14 | if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL', "\n"); |
||
| 15 | #define('MAILHEADER_ASCIIONLY',1); |
||
| 16 | |||
| 17 | /** |
||
| 18 | * Mail Handling |
||
| 19 | */ |
||
| 20 | class Mailer { |
||
| 21 | |||
| 22 | protected $headers = array(); |
||
| 23 | protected $attach = array(); |
||
| 24 | protected $html = ''; |
||
| 25 | protected $text = ''; |
||
| 26 | |||
| 27 | protected $boundary = ''; |
||
| 28 | protected $partid = ''; |
||
| 29 | protected $sendparam = null; |
||
| 30 | |||
| 31 | protected $allowhtml = true; |
||
| 32 | |||
| 33 | protected $replacements = array('text'=> array(), 'html' => array()); |
||
| 34 | |||
| 35 | /** |
||
| 36 | * Constructor |
||
| 37 | * |
||
| 38 | * Initializes the boundary strings, part counters and token replacements |
||
| 39 | */ |
||
| 40 | public function __construct() { |
||
| 41 | global $conf; |
||
| 42 | /* @var Input $INPUT */ |
||
| 43 | global $INPUT; |
||
| 44 | |||
| 45 | $server = parse_url(DOKU_URL, PHP_URL_HOST); |
||
| 46 | if(strpos($server,'.') === false) $server = $server.'.localhost'; |
||
| 47 | |||
| 48 | $this->partid = substr(md5(uniqid(rand(), true)),0, 8).'@'.$server; |
||
| 49 | $this->boundary = '__________'.md5(uniqid(rand(), true)); |
||
| 50 | |||
| 51 | $listid = join('.', array_reverse(explode('/', DOKU_BASE))).$server; |
||
| 52 | $listid = strtolower(trim($listid, '.')); |
||
| 53 | |||
| 54 | $this->allowhtml = (bool)$conf['htmlmail']; |
||
| 55 | |||
| 56 | // add some default headers for mailfiltering FS#2247 |
||
| 57 | if(!empty($conf['mailreturnpath'])) { |
||
| 58 | $this->setHeader('Return-Path', $conf['mailreturnpath']); |
||
| 59 | } |
||
| 60 | $this->setHeader('X-Mailer', 'DokuWiki'); |
||
| 61 | $this->setHeader('X-DokuWiki-User', $INPUT->server->str('REMOTE_USER')); |
||
| 62 | $this->setHeader('X-DokuWiki-Title', $conf['title']); |
||
| 63 | $this->setHeader('X-DokuWiki-Server', $server); |
||
| 64 | $this->setHeader('X-Auto-Response-Suppress', 'OOF'); |
||
| 65 | $this->setHeader('List-Id', $conf['title'].' <'.$listid.'>'); |
||
| 66 | $this->setHeader('Date', date('r'), false); |
||
| 67 | |||
| 68 | $this->prepareTokenReplacements(); |
||
| 69 | } |
||
| 70 | |||
| 71 | /** |
||
| 72 | * Attach a file |
||
| 73 | * |
||
| 74 | * @param string $path Path to the file to attach |
||
| 75 | * @param string $mime Mimetype of the attached file |
||
| 76 | * @param string $name The filename to use |
||
| 77 | * @param string $embed Unique key to reference this file from the HTML part |
||
| 78 | */ |
||
| 79 | public function attachFile($path, $mime, $name = '', $embed = '') { |
||
| 80 | if(!$name) { |
||
| 81 | $name = utf8_basename($path); |
||
| 82 | } |
||
| 83 | |||
| 84 | $this->attach[] = array( |
||
| 85 | 'data' => file_get_contents($path), |
||
| 86 | 'mime' => $mime, |
||
| 87 | 'name' => $name, |
||
| 88 | 'embed' => $embed |
||
| 89 | ); |
||
| 90 | } |
||
| 91 | |||
| 92 | /** |
||
| 93 | * Attach a file |
||
| 94 | * |
||
| 95 | * @param string $data The file contents to attach |
||
| 96 | * @param string $mime Mimetype of the attached file |
||
| 97 | * @param string $name The filename to use |
||
| 98 | * @param string $embed Unique key to reference this file from the HTML part |
||
| 99 | */ |
||
| 100 | public function attachContent($data, $mime, $name = '', $embed = '') { |
||
| 101 | if(!$name) { |
||
| 102 | list(, $ext) = explode('/', $mime); |
||
| 103 | $name = count($this->attach).".$ext"; |
||
| 104 | } |
||
| 105 | |||
| 106 | $this->attach[] = array( |
||
| 107 | 'data' => $data, |
||
| 108 | 'mime' => $mime, |
||
| 109 | 'name' => $name, |
||
| 110 | 'embed' => $embed |
||
| 111 | ); |
||
| 112 | } |
||
| 113 | |||
| 114 | /** |
||
| 115 | * Callback function to automatically embed images referenced in HTML templates |
||
| 116 | * |
||
| 117 | * @param array $matches |
||
| 118 | * @return string placeholder |
||
| 119 | */ |
||
| 120 | protected function autoembed_cb($matches) { |
||
| 121 | static $embeds = 0; |
||
| 122 | $embeds++; |
||
| 123 | |||
| 124 | // get file and mime type |
||
| 125 | $media = cleanID($matches[1]); |
||
| 126 | list(, $mime) = mimetype($media); |
||
| 127 | $file = mediaFN($media); |
||
| 128 | if(!file_exists($file)) return $matches[0]; //bad reference, keep as is |
||
| 129 | |||
| 130 | // attach it and set placeholder |
||
| 131 | $this->attachFile($file, $mime, '', 'autoembed'.$embeds); |
||
| 132 | return '%%autoembed'.$embeds.'%%'; |
||
| 133 | } |
||
| 134 | |||
| 135 | /** |
||
| 136 | * Add an arbitrary header to the mail |
||
| 137 | * |
||
| 138 | * If an empy value is passed, the header is removed |
||
| 139 | * |
||
| 140 | * @param string $header the header name (no trailing colon!) |
||
| 141 | * @param string|string[] $value the value of the header |
||
| 142 | * @param bool $clean remove all non-ASCII chars and line feeds? |
||
| 143 | */ |
||
| 144 | public function setHeader($header, $value, $clean = true) { |
||
| 145 | $header = str_replace(' ', '-', ucwords(strtolower(str_replace('-', ' ', $header)))); // streamline casing |
||
| 146 | if($clean) { |
||
| 147 | $header = preg_replace('/[^a-zA-Z0-9_ \-\.\+\@]+/', '', $header); |
||
| 148 | $value = preg_replace('/[^a-zA-Z0-9_ \-\.\+\@<>]+/', '', $value); |
||
| 149 | } |
||
| 150 | |||
| 151 | // empty value deletes |
||
| 152 | if(is_array($value)){ |
||
| 153 | $value = array_map('trim', $value); |
||
| 154 | $value = array_filter($value); |
||
| 155 | if(!$value) $value = ''; |
||
| 156 | }else{ |
||
| 157 | $value = trim($value); |
||
| 158 | } |
||
| 159 | if($value === '') { |
||
| 160 | if(isset($this->headers[$header])) unset($this->headers[$header]); |
||
| 161 | } else { |
||
| 162 | $this->headers[$header] = $value; |
||
| 163 | } |
||
| 164 | } |
||
| 165 | |||
| 166 | /** |
||
| 167 | * Set additional parameters to be passed to sendmail |
||
| 168 | * |
||
| 169 | * Whatever is set here is directly passed to PHP's mail() command as last |
||
| 170 | * parameter. Depending on the PHP setup this might break mailing alltogether |
||
| 171 | * |
||
| 172 | * @param string $param |
||
| 173 | */ |
||
| 174 | public function setParameters($param) { |
||
| 175 | $this->sendparam = $param; |
||
| 176 | } |
||
| 177 | |||
| 178 | /** |
||
| 179 | * Set the text and HTML body and apply replacements |
||
| 180 | * |
||
| 181 | * This function applies a whole bunch of default replacements in addition |
||
| 182 | * to the ones specified as parameters |
||
| 183 | * |
||
| 184 | * If you pass the HTML part or HTML replacements yourself you have to make |
||
| 185 | * sure you encode all HTML special chars correctly |
||
| 186 | * |
||
| 187 | * @param string $text plain text body |
||
| 188 | * @param array $textrep replacements to apply on the text part |
||
| 189 | * @param array $htmlrep replacements to apply on the HTML part, null to use $textrep (with urls wrapped in <a> tags) |
||
| 190 | * @param string $html the HTML body, leave null to create it from $text |
||
| 191 | * @param bool $wrap wrap the HTML in the default header/Footer |
||
| 192 | */ |
||
| 193 | public function setBody($text, $textrep = null, $htmlrep = null, $html = null, $wrap = true) { |
||
| 194 | |||
| 195 | $htmlrep = (array)$htmlrep; |
||
| 196 | $textrep = (array)$textrep; |
||
| 197 | |||
| 198 | // create HTML from text if not given |
||
| 199 | if(is_null($html)) { |
||
| 200 | $html = $text; |
||
| 201 | $html = hsc($html); |
||
| 202 | $html = preg_replace('/^----+$/m', '<hr >', $html); |
||
| 203 | $html = nl2br($html); |
||
| 204 | } |
||
| 205 | if($wrap) { |
||
| 206 | $wrap = rawLocale('mailwrap', 'html'); |
||
| 207 | $html = preg_replace('/\n-- <br \/>.*$/s', '', $html); //strip signature |
||
| 208 | $html = str_replace('@EMAILSIGNATURE@', '', $html); //strip @EMAILSIGNATURE@ |
||
| 209 | $html = str_replace('@HTMLBODY@', $html, $wrap); |
||
| 210 | } |
||
| 211 | |||
| 212 | if(strpos($text, '@EMAILSIGNATURE@') === false) { |
||
| 213 | $text .= '@EMAILSIGNATURE@'; |
||
| 214 | } |
||
| 215 | |||
| 216 | // copy over all replacements missing for HTML (autolink URLs) |
||
| 217 | foreach($textrep as $key => $value) { |
||
| 218 | if(isset($htmlrep[$key])) continue; |
||
| 219 | if(media_isexternal($value)) { |
||
| 220 | $htmlrep[$key] = '<a href="'.hsc($value).'">'.hsc($value).'</a>'; |
||
| 221 | } else { |
||
| 222 | $htmlrep[$key] = hsc($value); |
||
| 223 | } |
||
| 224 | } |
||
| 225 | |||
| 226 | // embed media from templates |
||
| 227 | $html = preg_replace_callback( |
||
| 228 | '/@MEDIA\(([^\)]+)\)@/', |
||
| 229 | array($this, 'autoembed_cb'), $html |
||
| 230 | ); |
||
| 231 | |||
| 232 | // add default token replacements |
||
| 233 | $trep = array_merge($this->replacements['text'], (array)$textrep); |
||
| 234 | $hrep = array_merge($this->replacements['html'], (array)$htmlrep); |
||
| 235 | |||
| 236 | // Apply replacements |
||
| 237 | foreach($trep as $key => $substitution) { |
||
| 238 | $text = str_replace('@'.strtoupper($key).'@', $substitution, $text); |
||
| 239 | } |
||
| 240 | foreach($hrep as $key => $substitution) { |
||
| 241 | $html = str_replace('@'.strtoupper($key).'@', $substitution, $html); |
||
| 242 | } |
||
| 243 | |||
| 244 | $this->setHTML($html); |
||
| 245 | $this->setText($text); |
||
| 246 | } |
||
| 247 | |||
| 248 | /** |
||
| 249 | * Set the HTML part of the mail |
||
| 250 | * |
||
| 251 | * Placeholders can be used to reference embedded attachments |
||
| 252 | * |
||
| 253 | * You probably want to use setBody() instead |
||
| 254 | * |
||
| 255 | * @param string $html |
||
| 256 | */ |
||
| 257 | public function setHTML($html) { |
||
| 258 | $this->html = $html; |
||
| 259 | } |
||
| 260 | |||
| 261 | /** |
||
| 262 | * Set the plain text part of the mail |
||
| 263 | * |
||
| 264 | * You probably want to use setBody() instead |
||
| 265 | * |
||
| 266 | * @param string $text |
||
| 267 | */ |
||
| 268 | public function setText($text) { |
||
| 269 | $this->text = $text; |
||
| 270 | } |
||
| 271 | |||
| 272 | /** |
||
| 273 | * Add the To: recipients |
||
| 274 | * |
||
| 275 | * @see cleanAddress |
||
| 276 | * @param string|string[] $address Multiple adresses separated by commas or as array |
||
| 277 | */ |
||
| 278 | public function to($address) { |
||
| 279 | $this->setHeader('To', $address, false); |
||
| 280 | } |
||
| 281 | |||
| 282 | /** |
||
| 283 | * Add the Cc: recipients |
||
| 284 | * |
||
| 285 | * @see cleanAddress |
||
| 286 | * @param string|string[] $address Multiple adresses separated by commas or as array |
||
| 287 | */ |
||
| 288 | public function cc($address) { |
||
| 289 | $this->setHeader('Cc', $address, false); |
||
| 290 | } |
||
| 291 | |||
| 292 | /** |
||
| 293 | * Add the Bcc: recipients |
||
| 294 | * |
||
| 295 | * @see cleanAddress |
||
| 296 | * @param string|string[] $address Multiple adresses separated by commas or as array |
||
| 297 | */ |
||
| 298 | public function bcc($address) { |
||
| 299 | $this->setHeader('Bcc', $address, false); |
||
| 300 | } |
||
| 301 | |||
| 302 | /** |
||
| 303 | * Add the From: address |
||
| 304 | * |
||
| 305 | * This is set to $conf['mailfrom'] when not specified so you shouldn't need |
||
| 306 | * to call this function |
||
| 307 | * |
||
| 308 | * @see cleanAddress |
||
| 309 | * @param string $address from address |
||
| 310 | */ |
||
| 311 | public function from($address) { |
||
| 312 | $this->setHeader('From', $address, false); |
||
| 313 | } |
||
| 314 | |||
| 315 | /** |
||
| 316 | * Add the mail's Subject: header |
||
| 317 | * |
||
| 318 | * @param string $subject the mail subject |
||
| 319 | */ |
||
| 320 | public function subject($subject) { |
||
| 321 | $this->headers['Subject'] = $subject; |
||
| 322 | } |
||
| 323 | |||
| 324 | /** |
||
| 325 | * Return a clean name which can be safely used in mail address |
||
| 326 | * fields. That means the name will be enclosed in '"' if it includes |
||
| 327 | * a '"' or a ','. Also a '"' will be escaped as '\"'. |
||
| 328 | * |
||
| 329 | * @param string $name the name to clean-up |
||
| 330 | * @see cleanAddress |
||
| 331 | */ |
||
| 332 | public function getCleanName($name) { |
||
| 333 | $name = trim($name, ' \t"'); |
||
| 334 | $name = str_replace('"', '\"', $name, $count); |
||
| 335 | if ($count > 0 || strpos($name, ',') !== false) { |
||
| 336 | $name = '"'.$name.'"'; |
||
| 337 | } |
||
| 338 | return $name; |
||
| 339 | } |
||
| 340 | |||
| 341 | /** |
||
| 342 | * Sets an email address header with correct encoding |
||
| 343 | * |
||
| 344 | * Unicode characters will be deaccented and encoded base64 |
||
| 345 | * for headers. Addresses may not contain Non-ASCII data! |
||
| 346 | * |
||
| 347 | * If @$addresses is a string then it will be split into multiple |
||
| 348 | * addresses. Addresses must be separated by a comma. If the display |
||
| 349 | * name includes a comma then it MUST be properly enclosed by '"' to |
||
| 350 | * prevent spliting at the wrong point. |
||
| 351 | * |
||
| 352 | * Example: |
||
| 353 | * cc("föö <[email protected]>, [email protected]","TBcc"); |
||
| 354 | * to("foo, Dr." <[email protected]>, [email protected]"); |
||
| 355 | * |
||
| 356 | * @param string|string[] $addresses Multiple adresses separated by commas or as array |
||
| 357 | * @return false|string the prepared header (can contain multiple lines) |
||
| 358 | */ |
||
| 359 | public function cleanAddress($addresses) { |
||
| 360 | $headers = ''; |
||
| 361 | if(!is_array($addresses)){ |
||
| 362 | $count = preg_match_all('/\s*(?:("[^"]*"[^,]+),*)|([^,]+)\s*,*/', $addresses, $matches, PREG_SET_ORDER); |
||
| 363 | if ($count !== false && is_array($matches)) { |
||
| 364 | $addresses = array(); |
||
| 365 | foreach ($matches as $match) { |
||
| 366 | array_push($addresses, $match[0]); |
||
| 367 | } |
||
| 368 | } |
||
| 369 | } |
||
| 370 | |||
| 371 | foreach($addresses as $part) { |
||
|
0 ignored issues
–
show
|
|||
| 372 | $part = preg_replace('/[\r\n\0]+/', ' ', $part); // remove attack vectors |
||
| 373 | $part = trim($part); |
||
| 374 | |||
| 375 | // parse address |
||
| 376 | if(preg_match('#(.*?)<(.*?)>#', $part, $matches)) { |
||
| 377 | $text = trim($matches[1]); |
||
| 378 | $addr = $matches[2]; |
||
| 379 | } else { |
||
| 380 | $addr = $part; |
||
| 381 | } |
||
| 382 | // skip empty ones |
||
| 383 | if(empty($addr)) { |
||
| 384 | continue; |
||
| 385 | } |
||
| 386 | |||
| 387 | // FIXME: is there a way to encode the localpart of a emailaddress? |
||
| 388 | if(!utf8_isASCII($addr)) { |
||
| 389 | msg(hsc("E-Mail address <$addr> is not ASCII"), -1); |
||
| 390 | continue; |
||
| 391 | } |
||
| 392 | |||
| 393 | if(!mail_isvalid($addr)) { |
||
| 394 | msg(hsc("E-Mail address <$addr> is not valid"), -1); |
||
| 395 | continue; |
||
| 396 | } |
||
| 397 | |||
| 398 | // text was given |
||
| 399 | if(!empty($text) && !isWindows()) { // No named recipients for To: in Windows (see FS#652) |
||
| 400 | // add address quotes |
||
| 401 | $addr = "<$addr>"; |
||
| 402 | |||
| 403 | if(defined('MAILHEADER_ASCIIONLY')) { |
||
| 404 | $text = utf8_deaccent($text); |
||
| 405 | $text = utf8_strip($text); |
||
| 406 | } |
||
| 407 | |||
| 408 | if(strpos($text, ',') !== false || !utf8_isASCII($text)) { |
||
| 409 | $text = '=?UTF-8?B?'.base64_encode($text).'?='; |
||
| 410 | } |
||
| 411 | } else { |
||
| 412 | $text = ''; |
||
| 413 | } |
||
| 414 | |||
| 415 | // add to header comma seperated |
||
| 416 | if($headers != '') { |
||
| 417 | $headers .= ', '; |
||
| 418 | } |
||
| 419 | $headers .= $text.' '.$addr; |
||
| 420 | } |
||
| 421 | |||
| 422 | $headers = trim($headers); |
||
| 423 | if(empty($headers)) return false; |
||
| 424 | |||
| 425 | return $headers; |
||
| 426 | } |
||
| 427 | |||
| 428 | |||
| 429 | /** |
||
| 430 | * Prepare the mime multiparts for all attachments |
||
| 431 | * |
||
| 432 | * Replaces placeholders in the HTML with the correct CIDs |
||
| 433 | * |
||
| 434 | * @return string mime multiparts |
||
| 435 | */ |
||
| 436 | protected function prepareAttachments() { |
||
| 437 | $mime = ''; |
||
| 438 | $part = 1; |
||
| 439 | // embedded attachments |
||
| 440 | foreach($this->attach as $media) { |
||
| 441 | $media['name'] = str_replace(':', '_', cleanID($media['name'], true)); |
||
| 442 | |||
| 443 | // create content id |
||
| 444 | $cid = 'part'.$part.'.'.$this->partid; |
||
| 445 | |||
| 446 | // replace wildcards |
||
| 447 | if($media['embed']) { |
||
| 448 | $this->html = str_replace('%%'.$media['embed'].'%%', 'cid:'.$cid, $this->html); |
||
| 449 | } |
||
| 450 | |||
| 451 | $mime .= '--'.$this->boundary.MAILHEADER_EOL; |
||
| 452 | $mime .= $this->wrappedHeaderLine('Content-Type', $media['mime'].'; id="'.$cid.'"'); |
||
| 453 | $mime .= $this->wrappedHeaderLine('Content-Transfer-Encoding', 'base64'); |
||
| 454 | $mime .= $this->wrappedHeaderLine('Content-ID',"<$cid>"); |
||
| 455 | if($media['embed']) { |
||
| 456 | $mime .= $this->wrappedHeaderLine('Content-Disposition', 'inline; filename='.$media['name']); |
||
| 457 | } else { |
||
| 458 | $mime .= $this->wrappedHeaderLine('Content-Disposition', 'attachment; filename='.$media['name']); |
||
| 459 | } |
||
| 460 | $mime .= MAILHEADER_EOL; //end of headers |
||
| 461 | $mime .= chunk_split(base64_encode($media['data']), 74, MAILHEADER_EOL); |
||
| 462 | |||
| 463 | $part++; |
||
| 464 | } |
||
| 465 | return $mime; |
||
| 466 | } |
||
| 467 | |||
| 468 | /** |
||
| 469 | * Build the body and handles multi part mails |
||
| 470 | * |
||
| 471 | * Needs to be called before prepareHeaders! |
||
| 472 | * |
||
| 473 | * @return string the prepared mail body, false on errors |
||
| 474 | */ |
||
| 475 | protected function prepareBody() { |
||
| 476 | |||
| 477 | // no HTML mails allowed? remove HTML body |
||
| 478 | if(!$this->allowhtml) { |
||
| 479 | $this->html = ''; |
||
| 480 | } |
||
| 481 | |||
| 482 | // check for body |
||
| 483 | if(!$this->text && !$this->html) { |
||
| 484 | return false; |
||
| 485 | } |
||
| 486 | |||
| 487 | // add general headers |
||
| 488 | $this->headers['MIME-Version'] = '1.0'; |
||
| 489 | |||
| 490 | $body = ''; |
||
| 491 | |||
| 492 | if(!$this->html && !count($this->attach)) { // we can send a simple single part message |
||
| 493 | $this->headers['Content-Type'] = 'text/plain; charset=UTF-8'; |
||
| 494 | $this->headers['Content-Transfer-Encoding'] = 'base64'; |
||
| 495 | $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL); |
||
| 496 | } else { // multi part it is |
||
| 497 | $body .= "This is a multi-part message in MIME format.".MAILHEADER_EOL; |
||
| 498 | |||
| 499 | // prepare the attachments |
||
| 500 | $attachments = $this->prepareAttachments(); |
||
| 501 | |||
| 502 | // do we have alternative text content? |
||
| 503 | if($this->text && $this->html) { |
||
| 504 | $this->headers['Content-Type'] = 'multipart/alternative;'.MAILHEADER_EOL. |
||
| 505 | ' boundary="'.$this->boundary.'XX"'; |
||
| 506 | $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; |
||
| 507 | $body .= 'Content-Type: text/plain; charset=UTF-8'.MAILHEADER_EOL; |
||
| 508 | $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; |
||
| 509 | $body .= MAILHEADER_EOL; |
||
| 510 | $body .= chunk_split(base64_encode($this->text), 72, MAILHEADER_EOL); |
||
| 511 | $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; |
||
| 512 | $body .= 'Content-Type: multipart/related;'.MAILHEADER_EOL. |
||
| 513 | ' boundary="'.$this->boundary.'";'.MAILHEADER_EOL. |
||
| 514 | ' type="text/html"'.MAILHEADER_EOL; |
||
| 515 | $body .= MAILHEADER_EOL; |
||
| 516 | } |
||
| 517 | |||
| 518 | $body .= '--'.$this->boundary.MAILHEADER_EOL; |
||
| 519 | $body .= 'Content-Type: text/html; charset=UTF-8'.MAILHEADER_EOL; |
||
| 520 | $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; |
||
| 521 | $body .= MAILHEADER_EOL; |
||
| 522 | $body .= chunk_split(base64_encode($this->html), 72, MAILHEADER_EOL); |
||
| 523 | $body .= MAILHEADER_EOL; |
||
| 524 | $body .= $attachments; |
||
| 525 | $body .= '--'.$this->boundary.'--'.MAILHEADER_EOL; |
||
| 526 | |||
| 527 | // close open multipart/alternative boundary |
||
| 528 | if($this->text && $this->html) { |
||
| 529 | $body .= '--'.$this->boundary.'XX--'.MAILHEADER_EOL; |
||
| 530 | } |
||
| 531 | } |
||
| 532 | |||
| 533 | return $body; |
||
| 534 | } |
||
| 535 | |||
| 536 | /** |
||
| 537 | * Cleanup and encode the headers array |
||
| 538 | */ |
||
| 539 | protected function cleanHeaders() { |
||
| 540 | global $conf; |
||
| 541 | |||
| 542 | // clean up addresses |
||
| 543 | if(empty($this->headers['From'])) $this->from($conf['mailfrom']); |
||
| 544 | $addrs = array('To', 'From', 'Cc', 'Bcc', 'Reply-To', 'Sender'); |
||
| 545 | foreach($addrs as $addr) { |
||
| 546 | if(isset($this->headers[$addr])) { |
||
| 547 | $this->headers[$addr] = $this->cleanAddress($this->headers[$addr]); |
||
| 548 | } |
||
| 549 | } |
||
| 550 | |||
| 551 | if(isset($this->headers['Subject'])) { |
||
| 552 | // add prefix to subject |
||
| 553 | if(empty($conf['mailprefix'])) { |
||
| 554 | if(utf8_strlen($conf['title']) < 20) { |
||
| 555 | $prefix = '['.$conf['title'].']'; |
||
| 556 | } else { |
||
| 557 | $prefix = '['.utf8_substr($conf['title'], 0, 20).'...]'; |
||
| 558 | } |
||
| 559 | } else { |
||
| 560 | $prefix = '['.$conf['mailprefix'].']'; |
||
| 561 | } |
||
| 562 | $len = strlen($prefix); |
||
| 563 | if(substr($this->headers['Subject'], 0, $len) != $prefix) { |
||
| 564 | $this->headers['Subject'] = $prefix.' '.$this->headers['Subject']; |
||
| 565 | } |
||
| 566 | |||
| 567 | // encode subject |
||
| 568 | if(defined('MAILHEADER_ASCIIONLY')) { |
||
| 569 | $this->headers['Subject'] = utf8_deaccent($this->headers['Subject']); |
||
| 570 | $this->headers['Subject'] = utf8_strip($this->headers['Subject']); |
||
| 571 | } |
||
| 572 | if(!utf8_isASCII($this->headers['Subject'])) { |
||
| 573 | $this->headers['Subject'] = '=?UTF-8?B?'.base64_encode($this->headers['Subject']).'?='; |
||
| 574 | } |
||
| 575 | } |
||
| 576 | |||
| 577 | } |
||
| 578 | |||
| 579 | /** |
||
| 580 | * Returns a complete, EOL terminated header line, wraps it if necessary |
||
| 581 | * |
||
| 582 | * @param string $key |
||
| 583 | * @param string $val |
||
| 584 | * @return string line |
||
| 585 | */ |
||
| 586 | protected function wrappedHeaderLine($key, $val){ |
||
| 587 | return wordwrap("$key: $val", 78, MAILHEADER_EOL.' ').MAILHEADER_EOL; |
||
| 588 | } |
||
| 589 | |||
| 590 | /** |
||
| 591 | * Create a string from the headers array |
||
| 592 | * |
||
| 593 | * @returns string the headers |
||
| 594 | */ |
||
| 595 | protected function prepareHeaders() { |
||
| 596 | $headers = ''; |
||
| 597 | foreach($this->headers as $key => $val) { |
||
| 598 | if ($val === '' || is_null($val)) continue; |
||
| 599 | $headers .= $this->wrappedHeaderLine($key, $val); |
||
| 600 | } |
||
| 601 | return $headers; |
||
| 602 | } |
||
| 603 | |||
| 604 | /** |
||
| 605 | * return a full email with all headers |
||
| 606 | * |
||
| 607 | * This is mainly intended for debugging and testing but could also be |
||
| 608 | * used for MHT exports |
||
| 609 | * |
||
| 610 | * @return string the mail, false on errors |
||
| 611 | */ |
||
| 612 | public function dump() { |
||
| 613 | $this->cleanHeaders(); |
||
| 614 | $body = $this->prepareBody(); |
||
| 615 | if($body === false) return false; |
||
| 616 | $headers = $this->prepareHeaders(); |
||
| 617 | |||
| 618 | return $headers.MAILHEADER_EOL.$body; |
||
| 619 | } |
||
| 620 | |||
| 621 | /** |
||
| 622 | * Prepare default token replacement strings |
||
| 623 | * |
||
| 624 | * Populates the '$replacements' property. |
||
| 625 | * Should be called by the class constructor |
||
| 626 | */ |
||
| 627 | protected function prepareTokenReplacements() { |
||
| 628 | global $INFO; |
||
| 629 | global $conf; |
||
| 630 | /* @var Input $INPUT */ |
||
| 631 | global $INPUT; |
||
| 632 | global $lang; |
||
| 633 | |||
| 634 | $ip = clientIP(); |
||
| 635 | $cip = gethostsbyaddrs($ip); |
||
| 636 | |||
| 637 | $this->replacements['text'] = array( |
||
| 638 | 'DATE' => dformat(), |
||
| 639 | 'BROWSER' => $INPUT->server->str('HTTP_USER_AGENT'), |
||
| 640 | 'IPADDRESS' => $ip, |
||
| 641 | 'HOSTNAME' => $cip, |
||
| 642 | 'TITLE' => $conf['title'], |
||
| 643 | 'DOKUWIKIURL' => DOKU_URL, |
||
| 644 | 'USER' => $INPUT->server->str('REMOTE_USER'), |
||
| 645 | 'NAME' => $INFO['userinfo']['name'], |
||
| 646 | 'MAIL' => $INFO['userinfo']['mail'] |
||
| 647 | ); |
||
| 648 | $signature = str_replace('@DOKUWIKIURL@', $this->replacements['text']['DOKUWIKIURL'], $lang['email_signature_text']); |
||
| 649 | $this->replacements['text']['EMAILSIGNATURE'] = "\n-- \n" . $signature . "\n"; |
||
| 650 | |||
| 651 | $this->replacements['html'] = array( |
||
| 652 | 'DATE' => '<i>' . hsc(dformat()) . '</i>', |
||
| 653 | 'BROWSER' => hsc($INPUT->server->str('HTTP_USER_AGENT')), |
||
| 654 | 'IPADDRESS' => '<code>' . hsc($ip) . '</code>', |
||
| 655 | 'HOSTNAME' => '<code>' . hsc($cip) . '</code>', |
||
| 656 | 'TITLE' => hsc($conf['title']), |
||
| 657 | 'DOKUWIKIURL' => '<a href="' . DOKU_URL . '">' . DOKU_URL . '</a>', |
||
| 658 | 'USER' => hsc($INPUT->server->str('REMOTE_USER')), |
||
| 659 | 'NAME' => hsc($INFO['userinfo']['name']), |
||
| 660 | 'MAIL' => '<a href="mailto:"' . hsc($INFO['userinfo']['mail']) . '">' . |
||
| 661 | hsc($INFO['userinfo']['mail']) . '</a>' |
||
| 662 | ); |
||
| 663 | $signature = $lang['email_signature_text']; |
||
| 664 | if(!empty($lang['email_signature_html'])) { |
||
| 665 | $signature = $lang['email_signature_html']; |
||
| 666 | } |
||
| 667 | $signature = str_replace( |
||
| 668 | array( |
||
| 669 | '@DOKUWIKIURL@', |
||
| 670 | "\n" |
||
| 671 | ), |
||
| 672 | array( |
||
| 673 | $this->replacements['html']['DOKUWIKIURL'], |
||
| 674 | '<br />' |
||
| 675 | ), |
||
| 676 | $signature |
||
| 677 | ); |
||
| 678 | $this->replacements['html']['EMAILSIGNATURE'] = $signature; |
||
| 679 | } |
||
| 680 | |||
| 681 | /** |
||
| 682 | * Send the mail |
||
| 683 | * |
||
| 684 | * Call this after all data was set |
||
| 685 | * |
||
| 686 | * @triggers MAIL_MESSAGE_SEND |
||
| 687 | * @return bool true if the mail was successfully passed to the MTA |
||
| 688 | */ |
||
| 689 | public function send() { |
||
| 690 | $success = false; |
||
| 691 | |||
| 692 | // prepare hook data |
||
| 693 | $data = array( |
||
| 694 | // pass the whole mail class to plugin |
||
| 695 | 'mail' => $this, |
||
| 696 | // pass references for backward compatibility |
||
| 697 | 'to' => &$this->headers['To'], |
||
| 698 | 'cc' => &$this->headers['Cc'], |
||
| 699 | 'bcc' => &$this->headers['Bcc'], |
||
| 700 | 'from' => &$this->headers['From'], |
||
| 701 | 'subject' => &$this->headers['Subject'], |
||
| 702 | 'body' => &$this->text, |
||
| 703 | 'params' => &$this->sendparam, |
||
| 704 | 'headers' => '', // plugins shouldn't use this |
||
| 705 | // signal if we mailed successfully to AFTER event |
||
| 706 | 'success' => &$success, |
||
| 707 | ); |
||
| 708 | |||
| 709 | // do our thing if BEFORE hook approves |
||
| 710 | $evt = new Doku_Event('MAIL_MESSAGE_SEND', $data); |
||
| 711 | if($evt->advise_before(true)) { |
||
| 712 | // clean up before using the headers |
||
| 713 | $this->cleanHeaders(); |
||
| 714 | |||
| 715 | // any recipients? |
||
| 716 | if(trim($this->headers['To']) === '' && |
||
| 717 | trim($this->headers['Cc']) === '' && |
||
| 718 | trim($this->headers['Bcc']) === '' |
||
| 719 | ) return false; |
||
| 720 | |||
| 721 | // The To: header is special |
||
| 722 | if(array_key_exists('To', $this->headers)) { |
||
| 723 | $to = (string)$this->headers['To']; |
||
| 724 | unset($this->headers['To']); |
||
| 725 | } else { |
||
| 726 | $to = ''; |
||
| 727 | } |
||
| 728 | |||
| 729 | // so is the subject |
||
| 730 | if(array_key_exists('Subject', $this->headers)) { |
||
| 731 | $subject = (string)$this->headers['Subject']; |
||
| 732 | unset($this->headers['Subject']); |
||
| 733 | } else { |
||
| 734 | $subject = ''; |
||
| 735 | } |
||
| 736 | |||
| 737 | // make the body |
||
| 738 | $body = $this->prepareBody(); |
||
| 739 | if($body === false) return false; |
||
| 740 | |||
| 741 | // cook the headers |
||
| 742 | $headers = $this->prepareHeaders(); |
||
| 743 | // add any headers set by legacy plugins |
||
| 744 | if(trim($data['headers'])) { |
||
| 745 | $headers .= MAILHEADER_EOL.trim($data['headers']); |
||
| 746 | } |
||
| 747 | |||
| 748 | // send the thing |
||
| 749 | if(is_null($this->sendparam)) { |
||
| 750 | $success = @mail($to, $subject, $body, $headers); |
||
| 751 | } else { |
||
| 752 | $success = @mail($to, $subject, $body, $headers, $this->sendparam); |
||
| 753 | } |
||
| 754 | } |
||
| 755 | // any AFTER actions? |
||
| 756 | $evt->advise_after(); |
||
| 757 | return $success; |
||
| 758 | } |
||
| 759 | } |
||
| 760 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.