1<?php 2// Copyright (C) 2010-2016 Combodo SARL 3// 4// This file is part of iTop. 5// 6// iTop is free software; you can redistribute it and/or modify 7// it under the terms of the GNU Affero General Public License as published by 8// the Free Software Foundation, either version 3 of the License, or 9// (at your option) any later version. 10// 11// iTop is distributed in the hope that it will be useful, 12// but WITHOUT ANY WARRANTY; without even the implied warranty of 13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14// GNU Affero General Public License for more details. 15// 16// You should have received a copy of the GNU Affero General Public License 17// along with iTop. If not, see <http://www.gnu.org/licenses/> 18 19 20/** 21 * Send an email (abstraction for synchronous/asynchronous modes) 22 * 23 * @copyright Copyright (C) 2010-2016 Combodo SARL 24 * @license http://opensource.org/licenses/AGPL-3.0 25 */ 26 27require_once(APPROOT.'/lib/swiftmailer/lib/swift_required.php'); 28 29Swift_Preferences::getInstance()->setCharset('UTF-8'); 30 31 32define ('EMAIL_SEND_OK', 0); 33define ('EMAIL_SEND_PENDING', 1); 34define ('EMAIL_SEND_ERROR', 2); 35 36class EMail 37{ 38 // Serialization formats 39 const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object. 40 // Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string 41 const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed) 42 43 protected static $m_oConfig = null; 44 protected $m_aData; // For storing data to serialize 45 46 public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE) 47 { 48 if (is_null(self::$m_oConfig)) 49 { 50 self::$m_oConfig = new Config($sConfigFile); 51 } 52 } 53 54 protected $m_oMessage; 55 56 public function __construct() 57 { 58 $this->m_aData = array(); 59 $this->m_oMessage = Swift_Message::newInstance(); 60 $this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label')); 61 } 62 63 /** 64 * Custom serialization method 65 * No longer use the brute force "serialize" method since 66 * 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field) 67 * 2) The size tends to be quite big (sometimes ten times the size of the email) 68 */ 69 public function SerializeV2() 70 { 71 return serialize($this->m_aData); 72 } 73 74 /** 75 * Custom de-serialization method 76 * @param string $sSerializedMessage The serialized representation of the message 77 */ 78 static public function UnSerializeV2($sSerializedMessage) 79 { 80 $aData = unserialize($sSerializedMessage); 81 $oMessage = new Email(); 82 83 if (array_key_exists('body', $aData)) 84 { 85 $oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']); 86 } 87 if (array_key_exists('message_id', $aData)) 88 { 89 $oMessage->SetMessageId($aData['message_id']); 90 } 91 if (array_key_exists('bcc', $aData)) 92 { 93 $oMessage->SetRecipientBCC($aData['bcc']); 94 } 95 if (array_key_exists('cc', $aData)) 96 { 97 $oMessage->SetRecipientCC($aData['cc']); 98 } 99 if (array_key_exists('from', $aData)) 100 { 101 $oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']); 102 } 103 if (array_key_exists('reply_to', $aData)) 104 { 105 $oMessage->SetRecipientReplyTo($aData['reply_to']); 106 } 107 if (array_key_exists('to', $aData)) 108 { 109 $oMessage->SetRecipientTO($aData['to']); 110 } 111 if (array_key_exists('subject', $aData)) 112 { 113 $oMessage->SetSubject($aData['subject']); 114 } 115 116 117 if (array_key_exists('headers', $aData)) 118 { 119 foreach($aData['headers'] as $sKey => $sValue) 120 { 121 $oMessage->AddToHeader($sKey, $sValue); 122 } 123 } 124 if (array_key_exists('parts', $aData)) 125 { 126 foreach($aData['parts'] as $aPart) 127 { 128 $oMessage->AddPart($aPart['text'], $aPart['mimeType']); 129 } 130 } 131 if (array_key_exists('attachments', $aData)) 132 { 133 foreach($aData['attachments'] as $aAttachment) 134 { 135 $oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']); 136 } 137 } 138 return $oMessage; 139 } 140 141 protected function SendAsynchronous(&$aIssues, $oLog = null) 142 { 143 try 144 { 145 AsyncSendEmail::AddToQueue($this, $oLog); 146 } 147 catch(Exception $e) 148 { 149 $aIssues = array($e->GetMessage()); 150 return EMAIL_SEND_ERROR; 151 } 152 $aIssues = array(); 153 return EMAIL_SEND_PENDING; 154 } 155 156 protected function SendSynchronous(&$aIssues, $oLog = null) 157 { 158 // If the body of the message is in HTML, embed all images based on attachments 159 $this->EmbedInlineImages(); 160 161 $this->LoadConfig(); 162 163 $sTransport = self::$m_oConfig->Get('email_transport'); 164 switch ($sTransport) 165 { 166 case 'SMTP': 167 $sHost = self::$m_oConfig->Get('email_transport_smtp.host'); 168 $sPort = self::$m_oConfig->Get('email_transport_smtp.port'); 169 $sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption'); 170 $sUserName = self::$m_oConfig->Get('email_transport_smtp.username'); 171 $sPassword = self::$m_oConfig->Get('email_transport_smtp.password'); 172 173 $oTransport = Swift_SmtpTransport::newInstance($sHost, $sPort, $sEncryption); 174 if (strlen($sUserName) > 0) 175 { 176 $oTransport->setUsername($sUserName); 177 $oTransport->setPassword($sPassword); 178 } 179 break; 180 181 case 'Null': 182 $oTransport = Swift_NullTransport::newInstance(); 183 break; 184 185 case 'LogFile': 186 $oTransport = Swift_LogFileTransport::newInstance(); 187 $oTransport->setLogFile(APPROOT.'log/mail.log'); 188 break; 189 190 case 'PHPMail': 191 default: 192 $oTransport = Swift_MailTransport::newInstance(); 193 } 194 195 $oMailer = Swift_Mailer::newInstance($oTransport); 196 197 $aFailedRecipients = array(); 198 $this->m_oMessage->setMaxLineLength(0); 199 $oKPI = new ExecutionKPI(); 200 try 201 { 202 $iSent = $oMailer->send($this->m_oMessage, $aFailedRecipients); 203 if ($iSent === 0) 204 { 205 // Beware: it seems that $aFailedRecipients sometimes contains the recipients that actually received the message !!! 206 IssueLog::Warning('Email sending failed: Some recipients were invalid, aFailedRecipients contains: '.implode(', ', $aFailedRecipients)); 207 $aIssues = array('Some recipients were invalid.'); 208 $oKPI->ComputeStats('Email Sent', 'Error received'); 209 return EMAIL_SEND_ERROR; 210 } 211 else 212 { 213 $aIssues = array(); 214 $oKPI->ComputeStats('Email Sent', 'Succeded'); 215 return EMAIL_SEND_OK; 216 } 217 } 218 catch (Exception $e) 219 { 220 $oKPI->ComputeStats('Email Sent', 'Error received'); 221 throw $e; 222 } 223 } 224 225 /** 226 * Reprocess the body of the message (if it is an HTML message) 227 * to replace the URL of images based on attachments by a link 228 * to an embedded image (i.e. cid:....) 229 */ 230 protected function EmbedInlineImages() 231 { 232 if ($this->m_aData['body']['mimeType'] == 'text/html') 233 { 234 $oDOMDoc = new DOMDocument(); 235 $oDOMDoc->preserveWhitespace = true; 236 @$oDOMDoc->loadHTML('<?xml encoding="UTF-8"?>'.$this->m_aData['body']['body']); // For loading HTML chunks where the character set is not specified 237 238 $oXPath = new DOMXPath($oDOMDoc); 239 $sXPath = '//img[@'.InlineImage::DOM_ATTR_ID.']'; 240 $oImagesList = $oXPath->query($sXPath); 241 242 if ($oImagesList->length != 0) 243 { 244 foreach($oImagesList as $oImg) 245 { 246 $iAttId = $oImg->getAttribute(InlineImage::DOM_ATTR_ID); 247 $oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */); 248 if ($oAttachment) 249 { 250 $sImageSecret = $oImg->getAttribute('data-img-secret'); 251 $sAttachmentSecret = $oAttachment->Get('secret'); 252 if ($sImageSecret !== $sAttachmentSecret) 253 { 254 // @see N°1921 255 // If copying from another iTop we could get an IMG pointing to an InlineImage with wrong secret 256 continue; 257 } 258 259 $oDoc = $oAttachment->Get('contents'); 260 $oSwiftImage = new Swift_Image($oDoc->GetData(), $oDoc->GetFileName(), $oDoc->GetMimeType()); 261 $sCid = $this->m_oMessage->embed($oSwiftImage); 262 $oImg->setAttribute('src', $sCid); 263 } 264 } 265 } 266 $sHtmlBody = $oDOMDoc->saveHTML(); 267 $this->m_oMessage->setBody($sHtmlBody, 'text/html', 'UTF-8'); 268 } 269 } 270 271 public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null) 272 { 273 //select a default sender if none is provided. 274 if(empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])){ 275 $this->SetRecipientFrom($this->m_aData['to']); 276 } 277 278 if ($bForceSynchronous) 279 { 280 return $this->SendSynchronous($aIssues, $oLog); 281 } 282 else 283 { 284 $bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous'); 285 if ($bConfigASYNC) 286 { 287 return $this->SendAsynchronous($aIssues, $oLog); 288 } 289 else 290 { 291 return $this->SendSynchronous($aIssues, $oLog); 292 } 293 } 294 } 295 296 public function AddToHeader($sKey, $sValue) 297 { 298 if (!array_key_exists('headers', $this->m_aData)) 299 { 300 $this->m_aData['headers'] = array(); 301 } 302 $this->m_aData['headers'][$sKey] = $sValue; 303 304 if (strlen($sValue) > 0) 305 { 306 $oHeaders = $this->m_oMessage->getHeaders(); 307 switch(strtolower($sKey)) 308 { 309 default: 310 $oHeaders->addTextHeader($sKey, $sValue); 311 } 312 } 313 } 314 315 public function SetMessageId($sId) 316 { 317 $this->m_aData['message_id'] = $sId; 318 319 // Note: Swift will add the angle brackets for you 320 // so let's remove the angle brackets if present, for historical reasons 321 $sId = str_replace(array('<', '>'), '', $sId); 322 323 $oMsgId = $this->m_oMessage->getHeaders()->get('Message-ID'); 324 $oMsgId->SetId($sId); 325 } 326 327 public function SetReferences($sReferences) 328 { 329 $this->AddToHeader('References', $sReferences); 330 } 331 332 public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null) 333 { 334 if (($sMimeType === 'text/html') && ($sCustomStyles !== null)) 335 { 336 require_once(APPROOT.'lib/emogrifier/Classes/Emogrifier.php'); 337 $emogrifier = new \Pelago\Emogrifier($sBody, $sCustomStyles); 338 $sBody = $emogrifier->emogrify(); // Adds html/body tags if not already present 339 } 340 $this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType); 341 $this->m_oMessage->setBody($sBody, $sMimeType); 342 } 343 344 public function AddPart($sText, $sMimeType = 'text/html') 345 { 346 if (!array_key_exists('parts', $this->m_aData)) 347 { 348 $this->m_aData['parts'] = array(); 349 } 350 $this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType); 351 $this->m_oMessage->addPart($sText, $sMimeType); 352 } 353 354 public function AddAttachment($data, $sFileName, $sMimeType) 355 { 356 if (!array_key_exists('attachments', $this->m_aData)) 357 { 358 $this->m_aData['attachments'] = array(); 359 } 360 $this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType); 361 $this->m_oMessage->attach(Swift_Attachment::newInstance($data, $sFileName, $sMimeType)); 362 } 363 364 public function SetSubject($sSubject) 365 { 366 $this->m_aData['subject'] = $sSubject; 367 $this->m_oMessage->setSubject($sSubject); 368 } 369 370 public function GetSubject() 371 { 372 return $this->m_oMessage->getSubject(); 373 } 374 375 /** 376 * Helper to transform and sanitize addresses 377 * - get rid of empty addresses 378 */ 379 protected function AddressStringToArray($sAddressCSVList) 380 { 381 $aAddresses = array(); 382 foreach(explode(',', $sAddressCSVList) as $sAddress) 383 { 384 $sAddress = trim($sAddress); 385 if (strlen($sAddress) > 0) 386 { 387 $aAddresses[] = $sAddress; 388 } 389 } 390 return $aAddresses; 391 } 392 393 public function SetRecipientTO($sAddress) 394 { 395 $this->m_aData['to'] = $sAddress; 396 if (!empty($sAddress)) 397 { 398 $aAddresses = $this->AddressStringToArray($sAddress); 399 $this->m_oMessage->setTo($aAddresses); 400 } 401 } 402 403 public function GetRecipientTO($bAsString = false) 404 { 405 $aRes = $this->m_oMessage->getTo(); 406 if ($aRes === null) 407 { 408 // There is no "To" header field 409 $aRes = array(); 410 } 411 if ($bAsString) 412 { 413 $aStrings = array(); 414 foreach ($aRes as $sEmail => $sName) 415 { 416 if (is_null($sName)) 417 { 418 $aStrings[] = $sEmail; 419 } 420 else 421 { 422 $sName = str_replace(array('<', '>'), '', $sName); 423 $aStrings[] = "$sName <$sEmail>"; 424 } 425 } 426 return implode(', ', $aStrings); 427 } 428 else 429 { 430 return $aRes; 431 } 432 } 433 434 public function SetRecipientCC($sAddress) 435 { 436 $this->m_aData['cc'] = $sAddress; 437 if (!empty($sAddress)) 438 { 439 $aAddresses = $this->AddressStringToArray($sAddress); 440 $this->m_oMessage->setCc($aAddresses); 441 } 442 } 443 444 public function SetRecipientBCC($sAddress) 445 { 446 $this->m_aData['bcc'] = $sAddress; 447 if (!empty($sAddress)) 448 { 449 $aAddresses = $this->AddressStringToArray($sAddress); 450 $this->m_oMessage->setBcc($aAddresses); 451 } 452 } 453 454 public function SetRecipientFrom($sAddress, $sLabel = '') 455 { 456 $this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel); 457 if ($sLabel != '') 458 { 459 $this->m_oMessage->setFrom(array($sAddress => $sLabel)); 460 } 461 else if (!empty($sAddress)) 462 { 463 $this->m_oMessage->setFrom($sAddress); 464 } 465 } 466 467 public function SetRecipientReplyTo($sAddress) 468 { 469 $this->m_aData['reply_to'] = $sAddress; 470 if (!empty($sAddress)) 471 { 472 $this->m_oMessage->setReplyTo($sAddress); 473 } 474 } 475 476} 477 478///////////////////////////////////////////////////////////////////////////////////// 479 480/** 481 * Extension to SwiftMailer: "debug" transport that pretends messages have been sent, 482 * but just log them to a file. 483 * 484 * @package Swift 485 * @author Denis Flaven 486 */ 487class Swift_Transport_LogFileTransport extends Swift_Transport_NullTransport 488{ 489 protected $sLogFile; 490 491 /** 492 * Sends the given message. 493 * 494 * @param Swift_Mime_Message $message 495 * @param string[] $failedRecipients An array of failures by-reference 496 * 497 * @return int The number of sent emails 498 */ 499 public function send(Swift_Mime_Message $message, &$failedRecipients = null) 500 { 501 $hFile = @fopen($this->sLogFile, 'a'); 502 if ($hFile) 503 { 504 $sTxt = "================== ".date('Y-m-d H:i:s')." ==================\n"; 505 $sTxt .= $message->toString()."\n"; 506 507 @fwrite($hFile, $sTxt); 508 @fclose($hFile); 509 } 510 511 return parent::send($message, $failedRecipients); 512 } 513 514 public function setLogFile($sFilename) 515 { 516 $this->sLogFile = $sFilename; 517 } 518} 519 520/** 521 * Pretends messages have been sent, but just log them to a file. 522 * 523 * @package Swift 524 * @author Denis Flaven 525 */ 526class Swift_LogFileTransport extends Swift_Transport_LogFileTransport 527{ 528 /** 529 * Create a new LogFileTransport. 530 */ 531 public function __construct() 532 { 533 call_user_func_array( 534 array($this, 'Swift_Transport_LogFileTransport::__construct'), 535 Swift_DependencyContainer::getInstance() 536 ->createDependenciesFor('transport.null') 537 ); 538 } 539 540 /** 541 * Create a new LogFileTransport instance. 542 * 543 * @return Swift_LogFileTransport 544 */ 545 public static function newInstance() 546 { 547 return new self(); 548 } 549}