1<?php 2// mailer.php -- HotCRP mail template manager 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5class MailPreparation { 6 public $conf; 7 public $subject = ""; 8 public $body = ""; 9 public $preparation_owner = ""; 10 public $to = array(); 11 public $sendable = false; 12 public $headers = array(); 13 public $errors = array(); 14 public $unique_preparation = false; 15 16 function __construct($conf) { 17 $this->conf = $conf; 18 } 19 function can_merge($p) { 20 return $this->subject == $p->subject 21 && $this->body == $p->body 22 && get($this->headers, "cc") == get($p->headers, "cc") 23 && get($this->headers, "reply-to") == get($p->headers, "reply-to") 24 && $this->preparation_owner == $p->preparation_owner 25 && !$this->unique_preparation 26 && !$p->unique_preparation; 27 } 28 function add_recipients($to) { 29 if (count($to) != 1 30 || count($this->to) == 0 31 || $this->to[count($this->to) - 1] != $to[0]) 32 $this->to = array_merge($this->to, $to); 33 } 34 function send() { 35 if ($this->conf->call_hooks("send_mail", null, $this) === false) 36 return false; 37 38 $headers = $this->headers; 39 $eol = Mailer::eol(); 40 41 // create valid To: header 42 $to = $this->to; 43 if (is_array($to)) 44 $to = join(", ", $to); 45 $to = MimeText::encode_email_header("To: ", $to); 46 $headers["to"] = $to . $eol; 47 48 // set sendmail parameters 49 $extra = $this->conf->opt("sendmailParam"); 50 if (($sender = $this->conf->opt("emailSender")) !== null) { 51 @ini_set("sendmail_from", $sender); 52 if ($extra === null) 53 $extra = "-f" . escapeshellarg($sender); 54 } 55 56 if ($this->sendable 57 && $this->conf->opt("internalMailer", strncasecmp(PHP_OS, "WIN", 3) != 0) 58 && ($sendmail = ini_get("sendmail_path"))) { 59 $htext = join("", $headers); 60 $f = popen($extra ? "$sendmail $extra" : $sendmail, "wb"); 61 fwrite($f, $htext . $eol . $this->body); 62 $status = pclose($f); 63 if (pcntl_wifexitedsuccess($status)) 64 return true; 65 else { 66 $this->conf->set_opt("internalMailer", false); 67 error_log("Mail " . $headers["to"] . " failed to send, falling back (status $status)"); 68 } 69 } 70 71 if ($this->sendable) { 72 if (strpos($to, $eol) === false) { 73 unset($headers["to"]); 74 $to = substr($to, 4); // skip "To: " 75 } else 76 $to = ""; 77 unset($headers["subject"]); 78 $htext = substr(join("", $headers), 0, -2); 79 return mail($to, $this->subject, $this->body, $htext, $extra); 80 81 } else if (!$this->conf->opt("sendEmail") 82 && !preg_match('/\Aanonymous\d*\z/', $to)) { 83 unset($headers["mime-version"], $headers["content-type"]); 84 $text = join("", $headers) . $eol . $this->body; 85 if (PHP_SAPI != "cli") 86 $this->conf->infoMsg("<pre>" . htmlspecialchars($text) . "</pre>"); 87 else if (!$this->conf->opt("disablePrintEmail")) 88 fwrite(STDERR, "========================================\n" . str_replace("\r\n", "\n", $text) . "========================================\n"); 89 return null; 90 } 91 } 92} 93 94class Mailer { 95 const EXPAND_BODY = 0; 96 const EXPAND_HEADER = 1; 97 const EXPAND_EMAIL = 2; 98 99 public static $email_fields = array("to" => "To", "cc" => "Cc", "bcc" => "Bcc", 100 "reply-to" => "Reply-To"); 101 102 public $conf; 103 public $recipient = null; 104 105 protected $width = 75; 106 protected $sensitivity = null; 107 protected $reason = null; 108 protected $adminupdate = null; 109 protected $notes = null; 110 protected $preparation = null; 111 public $capability = null; 112 113 protected $expansionType = null; 114 115 protected $_unexpanded = array(); 116 117 static private $eol = null; 118 119 function __construct(Conf $conf, $recipient = null, $settings = array()) { 120 $this->conf = $conf; 121 $this->reset($recipient, $settings); 122 } 123 124 function reset($recipient = null, $settings = array()) { 125 $this->recipient = $recipient; 126 foreach (array("width", "sensitivity", "reason", "adminupdate", "notes", 127 "capability") as $k) 128 $this->$k = get($settings, $k); 129 if ($this->width === null) 130 $this->width = 75; 131 else if (!$this->width) 132 $this->width = 10000000; 133 } 134 135 static function eol() { 136 global $Conf; 137 if (self::$eol === null) { 138 if (($x = $Conf->opt("postfixMailer", null)) === null) 139 $x = $Conf->opt("postfixEOL"); 140 if (!$x) 141 self::$eol = "\r\n"; 142 else if ($x === true || !is_string($x)) 143 self::$eol = PHP_EOL; 144 else 145 self::$eol = $x; 146 } 147 return self::$eol; 148 } 149 150 151 function expand_user($contact, $out) { 152 $r = Text::analyze_name($contact); 153 if (is_object($contact) && get_s($contact, "preferredEmail") != "") 154 $r->email = $contact->preferredEmail; 155 156 // maybe infer username 157 if ($r->firstName == "" 158 && $r->lastName == "" 159 && is_object($contact) 160 && (get_s($contact, "email") !== "" 161 || get_s($contact, "preferredEmail") !== "")) 162 $this->infer_user_name($r, $contact); 163 164 if ($out == "NAME" || $out == "CONTACT") 165 $t = $r->name; 166 else if ($out == "FIRST") 167 $t = $r->firstName; 168 else if ($out == "LAST") 169 $t = $r->lastName; 170 else 171 $t = ""; 172 if ($t == "" && $out == "NAME" && $r->email 173 && $this->expansionType != self::EXPAND_EMAIL) 174 $t = $r->email; 175 if ($t != "" && $this->expansionType == self::EXPAND_EMAIL 176 && preg_match('#[\000-\037()[\]<>@,;:\\".]#', $t)) 177 $t = "\"" . addcslashes($t, '"\\') . "\""; 178 179 $email = $r->email; 180 if ($email == "" && $this->expansionType == self::EXPAND_EMAIL) 181 $email = "<none>"; 182 if ($out == "EMAIL") 183 $t = $email; 184 else if ($out == "CONTACT" && $this->expansionType == self::EXPAND_EMAIL) { 185 if ($t == "") 186 $t = $email; 187 else if ($email[0] == "<") 188 $t .= " $email"; 189 else 190 $t .= " <$email>"; 191 } else if ($out == "CONTACT" && $email != "") 192 $t = ($t == "" ? $email : "$t <$email>"); 193 194 return $t; 195 } 196 197 function infer_user_name($r, $contact) { 198 } 199 200 201 static function kw_null() { 202 return false; 203 } 204 205 function kw_opt($args, $isbool) { 206 $hasinner = $this->expandvar("%$args%", true); 207 if ($hasinner && !$isbool) 208 return $this->expandvar("%$args%", false); 209 else 210 return $hasinner; 211 } 212 213 static function kw_urlenc($args, $isbool, $m) { 214 $hasinner = $m->expandvar("%$args%", true); 215 if ($hasinner && !$isbool) 216 return urlencode($m->expandvar("%$args%", false)); 217 else 218 return $hasinner; 219 } 220 221 static function kw_ims_expand($args, $isbool, $mx) { 222 preg_match('/\A\s*(.*?)\s*(?:|,\s*(\d+)\s*)\z/', $args, $m); 223 $t = $mx->conf->_c("mail", $m[1]); 224 if ($m[2] && strlen($t) < $m[2]) 225 $t = str_repeat(" ", $m[2] - strlen($t)) . $t; 226 return $t; 227 } 228 229 function kw_confnames($args, $isbool, $uf) { 230 if ($uf->name === "CONFNAME") 231 return $this->conf->full_name(); 232 else if ($uf->name == "CONFSHORTNAME") 233 return $this->conf->short_name; 234 else 235 return $this->conf->long_name; 236 } 237 238 function kw_siteuser($args, $isbool, $uf) { 239 return $this->expand_user($this->conf->site_contact(), $uf->userx); 240 } 241 242 static function kw_signature($args, $isbool, $m) { 243 return $m->conf->opt("emailSignature") ? : "- " . $m->conf->short_name . " Submissions"; 244 } 245 246 static function kw_url($args, $isbool, $m) { 247 if (!$args) 248 return $m->conf->opt("paperSite"); 249 else { 250 $a = preg_split('/\s*,\s*/', $args); 251 foreach ($a as &$t) { 252 $t = preg_replace('/\&(?=\&|\z)/', "", $m->expand($t, "urlpart")); 253 } 254 return hoturl_absolute_nodefaults($a[0], isset($a[1]) ? $a[1] : ""); 255 } 256 } 257 258 static function kw_loginnotice($args, $isbool, $m) { 259 if ($m->conf->opt("disableCapabilities")) 260 return $m->expand($m->conf->opt("mailtool_loginNotice", " To sign in, either click the link below or paste it into your web browser's location field.\n\n%LOGINURL%"), $isbool); 261 else 262 return ""; 263 } 264 265 static function kw_adminupdate($args, $isbool, $m) { 266 if ($m->adminupdate) 267 return "An administrator performed this update. "; 268 else 269 return $m->recipient ? "" : null; 270 } 271 272 static function kw_notes($args, $isbool, $m, $uf) { 273 $which = strtolower($uf->name); 274 $value = $m->$which; 275 if ($value !== null || $m->recipient) 276 return (string) $value; 277 else 278 return null; 279 } 280 281 static function kw_recipient($args, $isbool, $m, $uf) { 282 if ($m->preparation) 283 $m->preparation->preparation_owner = $m->recipient->email; 284 return $m->expand_user($m->recipient, $uf->userx); 285 } 286 287 static function kw_capability($args, $isbool, $m, $uf) { 288 return $isbool || $m->capability ? $m->capability : ""; 289 } 290 291 function kw_login($args, $isbool, $uf) { 292 $external_password = $this->conf->external_login(); 293 if (!$this->recipient) 294 return $external_password ? false : null; 295 296 $password = false; 297 if (!$external_password) { 298 $pwd_plaintext = $this->recipient->plaintext_password(); 299 if ($pwd_plaintext && !$this->sensitivity) 300 $password = $pwd_plaintext; 301 else if ($pwd_plaintext && $this->sensitivity === "display") 302 $password = "HIDDEN"; 303 } 304 305 $loginparts = ""; 306 if (!$this->conf->opt("httpAuthLogin")) { 307 $loginparts = "email=" . urlencode($this->recipient->email); 308 if ($password) 309 $loginparts .= "&password=" . urlencode($password); 310 } 311 312 if ($uf->name === "LOGINURL") 313 return $this->conf->opt("paperSite") . ($loginparts ? "/?" . $loginparts : "/"); 314 else if ($uf->name === "LOGINURLPARTS") 315 return $loginparts; 316 else 317 return $password; 318 } 319 320 function expandvar($what, $isbool = false) { 321 if (str_ends_with($what, ")%") && ($paren = strpos($what, "("))) { 322 $name = substr($what, 1, $paren - 1); 323 $args = substr($what, $paren + 1, strlen($what) - $paren - 3); 324 } else { 325 $name = substr($what, 1, strlen($what) - 2); 326 $args = ""; 327 } 328 329 $mks = $this->conf->mail_keywords($name); 330 foreach ($mks as $uf) { 331 $ok = $this->recipient || (isset($uf->global) && $uf->global); 332 if ($ok && isset($uf->expand_if)) { 333 if (is_string($uf->expand_if)) { 334 if ($uf->expand_if[0] === "*") 335 $ok = call_user_func([$this, substr($uf->expand_if, 1)], $uf); 336 else 337 $ok = call_user_func($uf->expand_if, $this, $uf); 338 } else 339 $ok = $uf->expand_if; 340 } 341 342 if (!$ok) 343 $x = null; 344 else if ($uf->callback[0] === "*") 345 $x = call_user_func([$this, substr($uf->callback, 1)], $args, $isbool, $uf); 346 else 347 $x = call_user_func($uf->callback, $args, $isbool, $this, $uf); 348 349 if ($x !== null) 350 return $isbool ? $x : (string) $x; 351 } 352 353 if ($isbool) 354 return $mks ? null : false; 355 else { 356 $this->_unexpanded[$what] = true; 357 return $what; 358 } 359 } 360 361 362 private function _pushIf(&$ifstack, $text, $yes) { 363 if ($yes !== false && $yes !== true && $yes !== null) 364 $yes = (bool) $yes; 365 if ($yes === true || $yes === null) 366 array_push($ifstack, $yes); 367 else 368 array_push($ifstack, $text); 369 } 370 371 private function _popIf(&$ifstack, &$text) { 372 if (count($ifstack) == 0) 373 return null; 374 else if (($pop = array_pop($ifstack)) === true || $pop === null) 375 return $pop; 376 else { 377 $text = $pop; 378 return false; 379 } 380 } 381 382 private function _handleIf(&$ifstack, &$text, $cond, $haselse) { 383 assert($cond || $haselse); 384 if ($haselse) { 385 $yes = $this->_popIf($ifstack, $text); 386 if ($yes !== null) 387 $yes = !$yes; 388 } else 389 $yes = true; 390 if ($yes && $cond) 391 $yes = $this->expandvar("%" . substr($cond, 1, strlen($cond) - 2) . "%", true); 392 $this->_pushIf($ifstack, $text, $yes); 393 return $yes; 394 } 395 396 private function _expandConditionals($rest) { 397 $text = ""; 398 $ifstack = array(); 399 400 while (preg_match('/\A(.*?)%(IF|ELSE?IF|ELSE|ENDIF)((?:\(#?[-a-zA-Z0-9!@_:.\/]+(?:\([-a-zA-Z0-9!@_:.\/]*+\))*\))?)%(.*)\z/s', $rest, $m)) { 401 $text .= $m[1]; 402 $rest = $m[4]; 403 404 if ($m[2] == "IF" && $m[3] != "") 405 $yes = $this->_handleIf($ifstack, $text, $m[3], false); 406 else if (($m[2] == "ELSIF" || $m[2] == "ELSEIF") && $m[3] != "") 407 $yes = $this->_handleIf($ifstack, $text, $m[3], true); 408 else if ($m[2] == "ELSE" && $m[3] == "") 409 $yes = $this->_handleIf($ifstack, $text, false, true); 410 else if ($m[2] == "ENDIF" && $m[3] == "") 411 $yes = $this->_popIf($ifstack, $text); 412 else 413 $yes = null; 414 415 if ($yes === null) 416 $text .= "%" . $m[2] . $m[3] . "%"; 417 } 418 419 return $text . $rest; 420 } 421 422 private function _lineexpand($line, $info, $indent, $width) { 423 $text = ""; 424 while (preg_match('/^(.*?)(%#?[-a-zA-Z0-9!@_:.\/]+(?:|\([^\)]*\))%)(.*)$/s', $line, $m)) { 425 $text .= $m[1] . $this->expandvar($m[2], false); 426 $line = $m[3]; 427 } 428 $text .= $line; 429 return prefix_word_wrap($info, $text, $indent, $width); 430 } 431 432 function expand($text, $field = null) { 433 if (is_array($text)) { 434 foreach ($text as $k => &$t) 435 $t = $this->expand($t, $k); 436 return $text; 437 } 438 439 // leave early on empty string 440 if ($text == "") 441 return ""; 442 443 // width, expansion type based on field 444 $oldExpansionType = $this->expansionType; 445 $width = 100000; 446 if ($field == "to" || $field == "cc" || $field == "bcc" 447 || $field == "reply-to") 448 $this->expansionType = self::EXPAND_EMAIL; 449 else if ($field != "body" && $field != "") 450 $this->expansionType = self::EXPAND_HEADER; 451 else { 452 $this->expansionType = self::EXPAND_BODY; 453 $width = $this->width; 454 } 455 456 // expand out %IF% and %ELSE% and %ENDIF%. Need to do this first, 457 // or we get confused with wordwrapping. 458 $text = $this->_expandConditionals(cleannl($text)); 459 460 // separate text into lines 461 $lines = explode("\n", $text); 462 if (count($lines) && $lines[count($lines) - 1] === "") 463 array_pop($lines); 464 465 $text = ""; 466 $textstart = 0; 467 for ($i = 0; $i < count($lines); ++$i) { 468 $line = rtrim($lines[$i]); 469 if ($line == "") 470 $text .= "\n"; 471 else if (preg_match('/\A%(?:REVIEWS|COMMENTS)(?:[(].*[)])?%\z/s', $line)) { 472 if (($m = $this->expandvar($line, false)) != "") 473 $text .= $m . "\n"; 474 } else if (strpos($line, "%") === false) 475 $text .= prefix_word_wrap("", $line, 0, $width); 476 else { 477 if ($line[0] === " " || $line[0] === "\t") { 478 if (preg_match('/\A([ \t]*)(%\w+(?:|\([^\)]*\))%)(:.*)\z/s', $line, $m) 479 && $this->expandvar($m[2], true)) 480 $line = $m[1] . $this->expandvar($m[2]) . $m[3]; 481 if (preg_match('/\A([ \t]*.*?: )(%\w+(?:|\([^\)]*\))%|\S+)\s*\z/s', $line, $m) 482 && ($tl = tabLength($m[1], true)) <= 20) { 483 if (str_starts_with($m[2], "%OPT(")) { 484 if (($yes = $this->expandvar($m[2], true))) 485 $text .= prefix_word_wrap($m[1], $this->expandvar($m[2]), $tl, $width); 486 else if ($yes === null) 487 $text .= $line . "\n"; 488 } else 489 $text .= $this->_lineexpand($m[2], $m[1], $tl, $width); 490 continue; 491 } 492 } 493 $text .= $this->_lineexpand($line, "", 0, $width); 494 } 495 } 496 497 // lose newlines on header expansion 498 if ($this->expansionType != self::EXPAND_BODY) 499 $text = rtrim(preg_replace('/[\r\n\f\x0B]+/', ' ', $text)); 500 501 $this->expansionType = $oldExpansionType; 502 return $text; 503 } 504 505 506 static function get_template($templateName, $default = false) { 507 global $Conf, $mailTemplates; 508 $mail = $mailTemplates[$templateName]; 509 if (!$default && $Conf) { 510 if (($t = $Conf->setting_data("mailsubj_" . $templateName, false)) !== false) 511 $mail["subject"] = $t; 512 if (($t = $Conf->setting_data("mailbody_" . $templateName, false)) !== false) 513 $mail["body"] = $t; 514 } 515 return $mail; 516 } 517 518 function expand_template($templateName, $default = false) { 519 return $this->expand(self::get_template($templateName, $default)); 520 } 521 522 static function is_template($template) { 523 global $mailTemplates; 524 return (is_array($template) 525 && is_string(get($template, "subject")) 526 && is_string(get($template, "body"))) 527 || (is_string($template) 528 && $template[0] === "@" 529 && isset($mailTemplates[substr($template, 1)])); 530 } 531 532 533 static function allow_send($email) { 534 global $Conf; 535 return $Conf->opt("sendEmail") 536 && ($at = strpos($email, "@")) !== false 537 && ((($ch = $email[$at + 1]) !== "_" && $ch !== "e" && $ch !== "E") 538 || !preg_match(';\A(?:_.*|example\.(?:com|net|org))\z;i', substr($email, $at + 1))); 539 } 540 541 function create_preparation() { 542 return new MailPreparation($this->conf); 543 } 544 545 function make_preparation($template, $rest = array()) { 546 // look up template 547 if (is_string($template) && $template[0] === "@") 548 $template = self::get_template(substr($template, 1)); 549 // add rest fields to template for expansion 550 foreach (self::$email_fields as $lcfield => $field) 551 if (isset($rest[$lcfield])) 552 $template[$lcfield] = $rest[$lcfield]; 553 554 // expand the template 555 $prep = $this->preparation = $this->create_preparation(); 556 $mail = $this->expand($template); 557 $this->preparation = null; 558 559 $subject = MimeText::encode_header("Subject: ", $mail["subject"]); 560 $prep->subject = substr($subject, 9); 561 562 $prep->body = $mail["body"]; 563 564 // look up recipient; use preferredEmail if set 565 $recipient = $this->recipient; 566 if (!$recipient || !$recipient->email) 567 return Conf::msg_error("no email in Mailer::send"); 568 if (get($recipient, "preferredEmail")) { 569 $recipient = (object) array("email" => $recipient->preferredEmail); 570 foreach (array("firstName", "lastName", "name", "fullName") as $k) 571 if (get($this->recipient, $k)) 572 $recipient->$k = $this->recipient->$k; 573 } 574 $prep->to = array(Text::user_email_to($recipient)); 575 $mail["to"] = $prep->to[0]; 576 $prep->sendable = self::allow_send($recipient->email); 577 578 // parse headers 579 $fromHeader = $this->conf->opt("emailFromHeader"); 580 if ($fromHeader === null) { 581 $fromHeader = MimeText::encode_email_header("From: ", $this->conf->opt("emailFrom")); 582 $this->conf->set_opt("emailFromHeader", $fromHeader); 583 } 584 $eol = self::eol(); 585 $prep->headers = []; 586 if ($fromHeader) 587 $prep->headers["from"] = $fromHeader . $eol; 588 $prep->headers["subject"] = $subject . $eol; 589 $prep->headers["to"] = ""; 590 foreach (self::$email_fields as $lcfield => $field) 591 if (($text = get_s($mail, $lcfield)) !== "" && $text !== "<none>") { 592 if (($hdr = MimeText::encode_email_header($field . ": ", $text))) 593 $prep->headers[$lcfield] = $hdr . $eol; 594 else { 595 $prep->errors[$lcfield] = $text; 596 if (!get($rest, "no_error_quit")) 597 Conf::msg_error("$field destination “<samp>" . htmlspecialchars($text) . "</samp>” isn't a valid email list."); 598 } 599 } 600 $prep->headers["mime-version"] = "MIME-Version: 1.0" . $eol; 601 $prep->headers["content-type"] = "Content-Type: text/plain; charset=utf-8" . $eol; 602 603 if ($prep->errors && !get($rest, "no_error_quit")) 604 return false; 605 else 606 return $prep; 607 } 608 609 static function send_combined_preparations($preps) { 610 $last_p = null; 611 foreach ($preps as $p) { 612 if ($last_p && $last_p->can_merge($p)) 613 $last_p->add_recipients($p->to); 614 else { 615 $last_p && $last_p->send(); 616 $last_p = $p; 617 } 618 } 619 $last_p && $last_p->send(); 620 } 621 622 623 protected function unexpanded_warning() { 624 $a = array_keys($this->_unexpanded); 625 natcasesort($a); 626 for ($i = 0; $i < count($a); ++$i) 627 $a[$i] = "<code>" . htmlspecialchars($a[$i]) . "</code>"; 628 if (count($a) == 1) 629 return "Keyword-like string " . commajoin($a) . " was not recognized."; 630 else 631 return "Keyword-like strings " . commajoin($a) . " were not recognized."; 632 } 633 634 function nwarnings() { 635 return count($this->_unexpanded); 636 } 637 638 function warnings() { 639 $e = array(); 640 if (count($this->_unexpanded)) 641 $e[] = $this->unexpanded_warning(); 642 return $e; 643 } 644 645} 646 647class MimeText { 648 /// Quote potentially non-ASCII header text a la RFC2047 and/or RFC822. 649 static function append(&$result, &$linelen, $str, $utf8) { 650 if ($utf8) { 651 // replace all special characters used by the encoder 652 $str = str_replace(array('=', '_', '?', ' '), 653 array('=3D', '=5F', '=3F', '_'), $str); 654 // define nonsafe characters 655 if ($utf8 > 1) 656 $matcher = ',[^-0-9a-zA-Z!*+/=_],'; 657 else 658 $matcher = ',[\x80-\xFF],'; 659 preg_match_all($matcher, $str, $m, PREG_OFFSET_CAPTURE); 660 $xstr = ""; 661 $last = 0; 662 foreach ($m[0] as $mx) { 663 $xstr .= substr($str, $last, $mx[1] - $last) 664 . "=" . strtoupper(dechex(ord($mx[0]))); 665 $last = $mx[1] + 1; 666 } 667 $xstr .= substr($str, $last); 668 } else 669 $xstr = $str; 670 671 // append words to the line 672 while ($xstr != "") { 673 $z = strlen($xstr); 674 assert($z > 0); 675 676 // add a line break 677 $maxlinelen = ($utf8 ? 76 - 12 : 78); 678 if (($linelen + $z > $maxlinelen && $linelen > 30) 679 || ($utf8 && substr($result, strlen($result) - 2) == "?=")) { 680 $result .= Mailer::eol() . " "; 681 $linelen = 1; 682 while (!$utf8 && $xstr !== "" && ctype_space($xstr[0])) { 683 $xstr = substr($xstr, 1); 684 --$z; 685 } 686 } 687 688 // if encoding, skip intact UTF-8 characters; 689 // otherwise, try to break at a space 690 if ($utf8 && $linelen + $z > $maxlinelen) { 691 $z = $maxlinelen - $linelen; 692 if ($xstr[$z - 1] == "=") 693 $z -= 1; 694 else if ($xstr[$z - 2] == "=") 695 $z -= 2; 696 while ($z > 3 697 && $xstr[$z] == "=" 698 && ($chr = hexdec(substr($xstr, $z + 1, 2))) >= 128 699 && $chr < 192) 700 $z -= 3; 701 } else if ($linelen + $z > $maxlinelen) { 702 $y = strrpos(substr($xstr, 0, $maxlinelen - $linelen), " "); 703 if ($y > 0) 704 $z = $y; 705 } 706 707 // append 708 if ($utf8) 709 $astr = "=?utf-8?q?" . substr($xstr, 0, $z) . "?="; 710 else 711 $astr = substr($xstr, 0, $z); 712 713 $result .= $astr; 714 $linelen += strlen($astr); 715 716 $xstr = substr($xstr, $z); 717 } 718 } 719 720 static function encode_email_header($header, $str) { 721 if (preg_match('/[\r\n]/', $str)) 722 $str = simplify_whitespace($str); 723 724 $text = $header; 725 $linelen = strlen($text); 726 727 // separate $str into emails, quote each separately 728 while (true) { 729 730 // try three types of match in turn: 731 // 1. name <email> [RFC 822] 732 $match = preg_match("/\\A[,\\s]*((?:(?:\"(?:[^\"\\\\]|\\\\.)*\"|[^\\s\\000-\\037()[\\]<>@,;:\\\\\".]+)\\s*?)*)\\s*<\\s*(.*?)\\s*>\\s*(.*)\\z/s", $str, $m); 733 // 2. name including periods but no quotes <email> (canonicalize) 734 if (!$match) { 735 $match = preg_match("/\\A[,\\s]*((?:[^\\s\\000-\\037()[\\]<>@,;:\\\\\"]+\\s*?)*)\\s*<\\s*(.*?)\\s*>\\s*(.*)\\z/s", $str, $m); 736 if ($match) 737 $m[1] = "\"$m[1]\""; 738 } 739 // 3. bare email 740 if (!$match) 741 $match = preg_match("/\\A[,\\s]*()<?\\s*([^\\s\\000-\\037()[\\]<>,;:\\\\\"]+)\\s*>?\\s*(.*)\\z/s", $str, $m); 742 // otherwise, fail 743 if (!$match) 744 break; 745 746 list($name, $email, $str) = array($m[1], $m[2], $m[3]); 747 if (strpos($email, "@") !== false && !validate_email($email)) 748 return false; 749 if ($str != "" && $str[0] != ",") 750 return false; 751 if ($email == "none" || $email == "hidden") 752 continue; 753 754 if ($text !== $header) { 755 $text .= ", "; 756 $linelen += 2; 757 } 758 759 // unquote any existing UTF-8 encoding 760 if ($name != "" && $name[0] == "=" 761 && strcasecmp(substr($name, 0, 10), "=?utf-8?q?") == 0) 762 $name = self::decode_header($name); 763 764 $utf8 = preg_match('/[\x80-\xFF]/', $name) ? 2 : 0; 765 if ($name != "" && $name[0] == "\"" 766 && preg_match("/\\A\"([^\\\\\"]|\\\\.)*\"\\z/s", $name)) { 767 if ($utf8) 768 self::append($text, $linelen, substr($name, 1, -1), $utf8); 769 else 770 self::append($text, $linelen, $name, false); 771 } else if ($utf8) 772 self::append($text, $linelen, $name, $utf8); 773 else 774 self::append($text, $linelen, rfc2822_words_quote($name), false); 775 776 if ($name == "") 777 self::append($text, $linelen, $email, false); 778 else 779 self::append($text, $linelen, " <$email>", false); 780 } 781 782 if (!preg_match('/\A[\s,]*\z/', $str)) 783 return false; 784 return $text; 785 } 786 787 static function encode_header($header, $str) { 788 if (preg_match('/[\r\n]/', $str)) 789 $str = simplify_whitespace($str); 790 791 $text = $header; 792 $linelen = strlen($text); 793 if (preg_match('/[\x80-\xFF]/', $str)) 794 self::append($text, $linelen, $str, true); 795 else 796 self::append($text, $linelen, $str, false); 797 return $text; 798 } 799 800 static function chr_hexdec_callback($m) { 801 return chr(hexdec($m[1])); 802 } 803 804 static function decode_header($text) { 805 if (strlen($text) > 2 && $text[0] == '=' && $text[1] == '?') { 806 $out = ''; 807 while (preg_match('/\A=\?utf-8\?q\?(.*?)\?=(\r?\n )?/i', $text, $m)) { 808 $f = str_replace('_', ' ', $m[1]); 809 $out .= preg_replace_callback('/=([0-9A-F][0-9A-F])/', 810 "MimeText::chr_hexdec_callback", 811 $f); 812 $text = substr($text, strlen($m[0])); 813 } 814 return $out . $text; 815 } else 816 return $text; 817 } 818} 819