1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8/** 9 * set some default params (mainly utf8 as tiki is utf8) + use the mailCharset pref from a user 10 */ 11 12class TikiMail 13{ 14 /** 15 * @var \Zend\Mail\Message 16 */ 17 private $mail; 18 private $charset; 19 public $errors; 20 21 /** 22 * @param string|null $user to username 23 * @param string|null $from from email 24 * @param string|null $fromName from Name 25 */ 26 function __construct($user = null, $from = null, $fromName = null) 27 { 28 global $user_preferences, $prefs; 29 30 require_once __DIR__ . '/../mail/maillib.php'; 31 32 $tikilib = TikiLib::lib('tiki'); 33 $userlib = TikiLib::lib('user'); 34 35 $to = ''; 36 $this->errors = []; 37 if (! empty($user)) { 38 if ($userlib->user_exists($user)) { 39 $to = $userlib->get_user_email($user); 40 $tikilib->get_user_preferences($user, ['mailCharset']); 41 $this->charset = $user_preferences[$user]['mailCharset']; 42 } else { 43 $str = tra('Mail to: User not found'); 44 trigger_error($str); 45 $this->errors = [$str]; 46 return; 47 } 48 } 49 50 if (! empty($from)) { 51 $this->mail = tiki_get_basic_mail(); 52 try { 53 $this->mail->setFrom($from, $fromName); 54 $this->mail->setSender($from); 55 } catch (Exception $e) { 56 // was already set, then do nothing 57 } 58 } else { 59 $this->mail = tiki_get_admin_mail($fromName); 60 } 61 if (! empty($to)) { 62 $this->mail->addTo($to); 63 } 64 65 if (empty($this->charset)) { 66 $this->charset = $prefs['users_prefs_mailCharset']; 67 } 68 } 69 70 function setUser($user) 71 { 72 } 73 74 function setFrom($email, $name = null) 75 { 76 if (! $name) { 77 $name = null; // zend now requires "Name must be a string" (or null, not false) 78 } 79 $this->mail->setFrom($email, $name); 80 } 81 82 function setReplyTo($email, $name = null) 83 { 84 if (! $name) { 85 $name = null; // zend now requires "Name must be a string" (or null, not false) 86 } 87 $this->mail->setReplyTo($email, $name); 88 } 89 90 function setSubject($subject) 91 { 92 $this->mail->setSubject($subject); 93 } 94 95 function setHtml($html, $text = null, $images_dir = null) 96 { 97 global $prefs; 98 if ($prefs['mail_apply_css'] != 'n') { 99 $html = $this->applyStyle($html); 100 } 101 102 $body = $this->mail->getBody(); 103 if (! ($body instanceof \Zend\Mime\Message) && ! empty($body)) { 104 $this->convertBodyToMime($body); 105 $body = $this->mail->getBody(); 106 } 107 108 if (! $body instanceof Zend\Mime\Message) { 109 $body = new Zend\Mime\Message(); 110 } 111 112 $partHtml = false; 113 $partText = false; 114 115 $parts = []; 116 foreach ($body->getParts() as $part) { 117 /* @var $part Zend\Mime\Part */ 118 if ($part->getType() == Zend\Mime\Mime::TYPE_HTML) { 119 $partHtml = $part; 120 $part->setContent($html); 121 if ($this->charset) { 122 $part->setCharset($this->charset); 123 } 124 } elseif ($part->getType() == Zend\Mime\Mime::TYPE_TEXT) { 125 $partText = $part; 126 if ($text) { 127 $part->setContent($text); 128 if ($this->charset) { 129 $part->setCharset($this->charset); 130 } 131 } 132 } else { 133 $parts[] = $part; 134 } 135 } 136 137 if (! $partText && $text) { 138 $partText = new Zend\Mime\Part($text); 139 $partText->setType(Zend\Mime\Mime::TYPE_TEXT); 140 if ($this->charset) { 141 $partText->setCharset($this->charset); 142 } 143 } 144 if ($partText) { 145 $parts[] = $partText; 146 } 147 148 if (! $partHtml) { 149 $partHtml = new Zend\Mime\Part($html); 150 $partHtml->setType(Zend\Mime\Mime::TYPE_HTML); 151 if ($this->charset) { 152 $partHtml->setCharset($this->charset); 153 } 154 } 155 $parts[] = $partHtml; 156 157 $body->setParts($parts); 158 $this->mail->setBody($body); 159 // use multipart/alternative for mail clients to display html and fall back to plain text parts 160 if ($text) { 161 $this->mail->getHeaders()->get('content-type')->setType('multipart/alternative'); 162 } 163 } 164 165 function setText($text = '') 166 { 167 $body = $this->mail->getBody(); 168 if ($body instanceof \Zend\Mime\Message) { 169 $parts = $body->getParts(); 170 $textPartFound = false; 171 foreach ($parts as $part) { 172 /* @var $part Zend\Mime\Part */ 173 if ($part->getType() == Zend\Mime\Mime::TYPE_TEXT) { 174 $part->setContent($text); 175 if ($this->charset) { 176 $part->setCharset($this->charset); 177 } 178 $textPartFound = true; 179 break; 180 } 181 } 182 if (! $textPartFound) { 183 $part = new Zend\Mime\Part($text); 184 $part->setType(Zend\Mime\Mime::TYPE_TEXT); 185 if ($this->charset) { 186 $part->setCharset($this->charset); 187 } 188 $parts[] = $part; 189 } 190 $body->setParts($parts); 191 } else { 192 $this->mail->setBody($text); 193 if ($this->charset) { 194 $headers = $this->mail->getHeaders(); 195 $headers->removeHeader($headers->get('Content-type')); 196 $headers->addHeaderLine( 197 'Content-type: text/plain; charset=' . $this->charset 198 ); 199 } 200 } 201 } 202 203 function setCc($address) 204 { 205 foreach ((array) $address as $cc) { 206 $this->mail->addCc($cc); 207 } 208 } 209 210 function setBcc($address) 211 { 212 foreach ((array) $address as $bcc) { 213 $this->mail->addBcc($bcc); 214 } 215 } 216 217 function setHeader($name, $value) 218 { 219 $headers = $this->mail->getHeaders(); 220 switch ($name) { 221 case 'Message-Id': 222 $headers->addHeader(Zend\Mail\Header\MessageId::fromString('Message-ID: ' . trim($value))); 223 break; 224 case 'In-Reply-To': 225 $headers->addHeader(Zend\Mail\Header\InReplyTo::fromString('In-Reply-To: ' . trim($value))); 226 break; 227 case 'References': 228 $headers->addHeader(Zend\Mail\Header\References::fromString('References: ' . trim($value))); 229 break; 230 default: 231 $this->mail->getHeaders()->addHeaderLine($name, $value); 232 break; 233 } 234 } 235 236 function addPart($content, $type) { 237 $body = $this->mail->getBody(); 238 if (! ($body instanceof \Zend\Mime\Message)) { 239 $this->convertBodyToMime($body); 240 $body = $this->mail->getBody(); 241 } 242 $part = new Zend\Mime\Part($content); 243 $part->setType($type); 244 $part->setCharset($this->charset); 245 $body->addPart($part); 246 $headers = $this->mail->getHeaders(); 247 $headers->removeHeader('Content-type'); 248 $headers->addHeaderLine( 249 'Content-type: multipart/mixed; boundary="'.$body->getMime()->boundary().'"' 250 ); 251 } 252 253 /** 254 * Get the Zend Message object 255 * 256 * @return \Zend\Mail\Message 257 */ 258 function getMessage() { 259 return $this->mail; 260 } 261 262 function send($recipients, $type = 'mail') 263 { 264 global $tikilib, $prefs; 265 $logslib = TikiLib::lib('logs'); 266 267 $this->mail->getHeaders()->removeHeader('to'); 268 foreach ((array) $recipients as $to) { 269 try { 270 $this->mail->addTo($to); 271 } catch (Zend\Mail\Exception\InvalidArgumentException $e) { 272 $title = 'mail error'; 273 $error = $e->getMessage(); 274 $this->errors[] = $error; 275 $error = ' [' . $error . ']'; 276 $logslib->add_log($title, $to . '/' . $this->mail->getSubject() . $error); 277 } 278 } 279 280 if ($prefs['zend_mail_handler'] == 'smtp' && $prefs['zend_mail_queue'] == 'y') { 281 $query = "INSERT INTO `tiki_mail_queue` (message) VALUES (?)"; 282 $bindvars = [serialize($this->mail)]; 283 $tikilib->query($query, $bindvars, -1, 0); 284 $title = 'mail'; 285 } else { 286 try { 287 tiki_send_email($this->mail); 288 $title = 'mail'; 289 $error = ''; 290 } catch (Zend\Mail\Exception\ExceptionInterface $e) { 291 $title = 'mail error'; 292 $error = $e->getMessage(); 293 $this->errors[] = $error; 294 $error = ' [' . $error . ']'; 295 } 296 297 if ($title == 'mail error' || $prefs['log_mail'] == 'y') { 298 foreach ($recipients as $u) { 299 $logslib->add_log($title, $u . '/' . $this->mail->getSubject() . $error); 300 } 301 } 302 } 303 return $title == 'mail'; 304 } 305 306 protected function convertBodyToMime($text) 307 { 308 $textPart = new Zend\Mime\Part($text); 309 $textPart->setType(Zend\Mime\Mime::TYPE_TEXT); 310 $newBody = new Zend\Mime\Message(); 311 $newBody->addPart($textPart); 312 $this->mail->setBody($newBody); 313 } 314 315 function addAttachment($data, $filename, $mimetype) 316 { 317 $body = $this->mail->getBody(); 318 if (! ($body instanceof \Zend\Mime\Message)) { 319 $this->convertBodyToMime($body); 320 $body = $this->mail->getBody(); 321 } 322 323 $attachment = new Zend\Mime\Part($data); 324 $attachment->setFileName($filename); 325 $attachment->setType($mimetype); 326 $attachment->setEncoding(Zend\Mime\Mime::ENCODING_BASE64); 327 $attachment->setDisposition(Zend\Mime\Mime::DISPOSITION_INLINE); 328 $body->addPart($attachment); 329 } 330 331 /** 332 * scramble an email with a method 333 * 334 * @param string $email email address to be scrambled 335 * @param string $method unicode or y: each character is replaced with the unicode value 336 * strtr: mr@tw.org -> mr AT tw DOT org 337 * x: mr@tw.org -> mr@xxxxxx 338 * 339 * @return string scrambled email 340 */ 341 static function scrambleEmail($email, $method = 'unicode') 342 { 343 switch ($method) { 344 case 'strtr': 345 $trans = [ "@" => tra("(AT)"), 346 "." => tra("(DOT)") 347 ]; 348 return strtr($email, $trans); 349 case 'x': 350 $encoded = $email; 351 for ($i = strpos($email, "@") + 1, $istrlen_email = strlen($email); $i < $istrlen_email; $i++) { 352 if ($encoded[$i] != ".") { 353 $encoded[$i] = 'x'; 354 } 355 } 356 return $encoded; 357 case 'unicode': 358 case 'y':// for previous compatibility 359 $encoded = ''; 360 for ($i = 0, $istrlen_email = strlen($email); $i < $istrlen_email; $i++) { 361 $encoded .= '&#' . ord($email[$i]) . ';'; 362 } 363 return $encoded; 364 case 'n': 365 default: 366 return $email; 367 } 368 } 369 370 private function collectCss() 371 { 372 static $css; 373 if ($css) { 374 return $css; 375 } 376 377 $cachelib = TikiLib::lib('cache'); 378 if ($css = $cachelib->getCached('email_css')) { 379 return $css; 380 } 381 382 $headerlib = TikiLib::lib('header'); 383 $files = $headerlib->get_css_files(); 384 $contents = array_map(function ($file) { 385 if ($file{0} == '/') { 386 return file_get_contents($file); 387 } elseif (substr($file, 0, 4) == 'http') { 388 return TikiLib::lib('tiki')->httprequest($file); 389 } else { 390 if (strpos($file, 'themes/') === 0) { // only use the tiki base and current theme files 391 return file_get_contents(TIKI_PATH . '/' . $file); 392 } 393 } 394 }, $files); 395 396 $css = implode("\n\n", array_filter($contents)); 397 $cachelib->cacheItem('email_css', $css); 398 return $css; 399 } 400 401 private function applyStyle($html) 402 { 403 $html = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . $html; 404 $css = $this->collectCss(); 405 $processor = new \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles(); 406 $html = $processor->convert($html, $css); 407 return $html; 408 } 409} 410 411/** 412 * Format text, sender and date for a plain text email reply 413 * - Split into 75 char long lines prepended with > 414 * 415 * @param $text email text to be quoted 416 * @param $from email from name/address to be quoted 417 * @param $date date of mail to be quoted 418 * @return string text ready for replying in a plain text email 419 */ 420function format_email_reply(&$text, $from, $date) 421{ 422 $lines = preg_split('/[\n\r]+/', wordwrap($text)); 423 424 for ($i = 0, $icount_lines = count($lines); $i < $icount_lines; $i++) { 425 $lines[$i] = '> ' . $lines[$i] . "\n"; 426 } 427 $str = ! empty($from) ? $from . ' wrote' : ''; 428 $str .= ! empty($date) ? ' on ' . $date : ''; 429 $str = "\n\n\n" . $str . "\n" . implode($lines); 430 431 return $str; 432} 433 434/** 435 * Attempt to close any unclosed HTML tags 436 * Needs to work with what's inside the BODY 437 * originally from http://snipplr.com/view/3618/close-tags-in-a-htmlsnippet/ 438 * 439 * @param $html html input 440 * @return string corrected html out 441 */ 442function closetags($html) 443{ 444 #put all opened tags into an array 445 preg_match_all("#<([a-z]+)( .*)?(?!/)>#iU", $html, $result); 446 $openedtags = $result[1]; 447 448 #put all closed tags into an array 449 preg_match_all("#</([a-z]+)>#iU", $html, $result); 450 $closedtags = $result[1]; 451 $len_opened = count($openedtags); 452 453 # all tags are closed 454 if (count($closedtags) == $len_opened) { 455 return $html; 456 } 457 $openedtags = array_reverse($openedtags); 458 459 # close tags 460 for ($i = 0; $i < $len_opened; $i++) { 461 if (! in_array($openedtags[$i], $closedtags)) { 462 $html .= "</" . $openedtags[$i] . ">"; 463 } else { 464 unset($closedtags[array_search($openedtags[$i], $closedtags)]); 465 } 466 } 467 return $html; 468} 469 470