Merge remote-tracking branch 'origin/support/2.7' into support/3.0

# Conflicts:
#	composer.json
#	composer.lock
#	core/config.class.inc.php
#	core/dbobject.class.php
#	core/email.class.inc.php
#	datamodels/2.x/itop-core-update/dictionaries/hu.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/dictionaries/it.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/dictionaries/ja.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/dictionaries/nl.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/dictionaries/ru.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/dictionaries/sk.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/dictionaries/tr.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/dictionaries/zh_cn.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/pt_br.dict.itop-core-update.php
#	datamodels/2.x/itop-core-update/view/ConfirmUpdate.html.twig
#	datamodels/2.x/itop-portal-base/portal/src/Twig/AppExtension.php
#	datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml
#	datamodels/2.x/itop-welcome-itil/datamodel.itop-welcome-itil.xml
#	dictionaries/cs.dictionary.itop.core.php
#	dictionaries/cs.dictionary.itop.ui.php
#	dictionaries/da.dictionary.itop.core.php
#	dictionaries/da.dictionary.itop.ui.php
#	dictionaries/de.dictionary.itop.core.php
#	dictionaries/de.dictionary.itop.ui.php
#	dictionaries/en.dictionary.itop.ui.php
#	dictionaries/es_cr.dictionary.itop.core.php
#	dictionaries/es_cr.dictionary.itop.ui.php
#	dictionaries/fr.dictionary.itop.core.php
#	dictionaries/fr.dictionary.itop.ui.php
#	dictionaries/hu.dictionary.itop.core.php
#	dictionaries/hu.dictionary.itop.ui.php
#	dictionaries/it.dictionary.itop.core.php
#	dictionaries/it.dictionary.itop.ui.php
#	dictionaries/ja.dictionary.itop.core.php
#	dictionaries/ja.dictionary.itop.ui.php
#	dictionaries/nl.dictionary.itop.core.php
#	dictionaries/nl.dictionary.itop.ui.php
#	dictionaries/pt_br.dictionary.itop.core.php
#	dictionaries/pt_br.dictionary.itop.ui.php
#	dictionaries/ru.dictionary.itop.core.php
#	dictionaries/ru.dictionary.itop.ui.php
#	dictionaries/sk.dictionary.itop.core.php
#	dictionaries/sk.dictionary.itop.ui.php
#	dictionaries/tr.dictionary.itop.core.php
#	dictionaries/tr.dictionary.itop.ui.php
#	dictionaries/zh_cn.dictionary.itop.core.php
#	dictionaries/zh_cn.dictionary.itop.ui.php
#	lib/composer/autoload_classmap.php
#	lib/composer/autoload_real.php
#	lib/composer/autoload_static.php
#	lib/composer/installed.json
#	lib/composer/installed.php
#	sources/application/TwigBase/Controller/Controller.php
#	sources/application/TwigBase/Twig/TwigHelper.php
This commit is contained in:
Benjamin Dalsass
2022-06-02 14:27:08 +02:00
702 changed files with 90691 additions and 2659 deletions

View File

@@ -0,0 +1,20 @@
<?php
namespace Combodo\iTop\Core\Email;
use EMailLaminas;
use EmailSwiftMailer;
use utils;
class EmailFactory
{
public static function GetMailer()
{
$sTransport = utils::GetConfig()->Get('email_transport');
if ($sTransport == 'SMTP_OAuth') {
return EMailLaminas::GetMailer();
}
return EmailSwiftMailer::GetMailer();
}
}

View File

@@ -0,0 +1,591 @@
<?php
// Copyright (C) 2010-2021 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Send an email (abstraction for synchronous/asynchronous modes)
*
* @copyright Copyright (C) 2010-2021 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
@include APPROOT."/core/oauth.php";
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderFactory;
use Laminas\Mail\Header\ContentType;
use Laminas\Mail\Message;
use Laminas\Mail\Transport\File;
use Laminas\Mail\Transport\FileOptions;
use Laminas\Mail\Transport\Smtp;
use Laminas\Mail\Transport\SmtpOptions;
use Laminas\Mime\Mime;
use Laminas\Mime\Part;
use Pelago\Emogrifier\CssInliner;
use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
class EMailLaminas extends Email
{
// Serialization formats
const ORIGINAL_FORMAT = 1; // Original format, consisting in serializing the whole object, inculding the Swift Mailer's object.
// Did not work with attachements since their binary representation cannot be stored as a valid UTF-8 string
const FORMAT_V2 = 2; // New format, only the raw data are serialized (base64 encoded if needed)
protected static $m_oConfig = null;
protected $m_aData; // For storing data to serialize
public static function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE)
{
if (is_null(self::$m_oConfig)) {
self::$m_oConfig = new Config($sConfigFile);
}
}
protected $m_oMessage;
/** @noinspection PhpMissingParentConstructorInspection */
public function __construct()
{
$this->m_aData = array();
$this->m_oMessage = new Message();
$this->m_oMessage->setEncoding('UTF-8');
$this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label'));
}
/**
* Custom serialization method
* No longer use the brute force "serialize" method since
* 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field)
* 2) The size tends to be quite big (sometimes ten times the size of the email)
*/
public function SerializeV2()
{
return serialize($this->m_aData);
}
/**
* Custom de-serialization method
*
* @param string $sSerializedMessage The serialized representation of the message
*
* @return \EMail
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \Symfony\Component\CssSelector\Exception\SyntaxErrorException
*/
public static function UnSerializeV2($sSerializedMessage)
{
$aData = unserialize($sSerializedMessage);
$oMessage = new Email();
if (array_key_exists('body', $aData)) {
$oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']);
}
if (array_key_exists('message_id', $aData)) {
$oMessage->SetMessageId($aData['message_id']);
}
if (array_key_exists('bcc', $aData)) {
$oMessage->SetRecipientBCC($aData['bcc']);
}
if (array_key_exists('cc', $aData)) {
$oMessage->SetRecipientCC($aData['cc']);
}
if (array_key_exists('from', $aData)) {
$oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']);
}
if (array_key_exists('reply_to', $aData)) {
$oMessage->SetRecipientReplyTo($aData['reply_to']['address'], $aData['reply_to']['label']);
}
if (array_key_exists('to', $aData)) {
$oMessage->SetRecipientTO($aData['to']);
}
if (array_key_exists('subject', $aData)) {
$oMessage->SetSubject($aData['subject']);
}
if (array_key_exists('headers', $aData)) {
foreach ($aData['headers'] as $sKey => $sValue) {
$oMessage->AddToHeader($sKey, $sValue);
}
}
if (array_key_exists('parts', $aData)) {
foreach ($aData['parts'] as $aPart) {
$oMessage->AddPart($aPart['text'], $aPart['mimeType']);
}
}
if (array_key_exists('attachments', $aData)) {
foreach ($aData['attachments'] as $aAttachment) {
$oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']);
}
}
return $oMessage;
}
protected function SendAsynchronous(&$aIssues, $oLog = null)
{
try {
AsyncSendEmail::AddToQueue($this, $oLog);
}
catch (Exception $e) {
$aIssues = array($e->GetMessage());
return EMAIL_SEND_ERROR;
}
$aIssues = array();
return EMAIL_SEND_PENDING;
}
public static function GetMailer()
{
return new EMailLaminas();
}
/**
* @throws \Exception
*/
protected function SendSynchronous(&$aIssues, $oLog = null)
{
$this->LoadConfig();
$sTransport = self::$m_oConfig->Get('email_transport');
switch ($sTransport) {
case 'SMTP':
$sHost = self::$m_oConfig->Get('email_transport_smtp.host');
$sPort = self::$m_oConfig->Get('email_transport_smtp.port');
$sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption');
$sUserName = self::$m_oConfig->Get('email_transport_smtp.username');
$sPassword = self::$m_oConfig->Get('email_transport_smtp.password');
$oTransport = new Smtp();
$aOptions = [
'host' => $sHost,
'port' => $sPort,
'connection_class' => 'login',
'connection_config' => [
'ssl' => $sEncryption,
],
];
if (strlen($sUserName) > 0) {
$aOptions['connection_config']['username'] = $sUserName;
$aOptions['connection_config']['password'] = $sPassword;
}
$oOptions = new SmtpOptions($aOptions);
$oTransport->setOptions($oOptions);
break;
case 'SMTP_OAuth':
$sHost = self::$m_oConfig->Get('email_transport_smtp.host');
$sPort = self::$m_oConfig->Get('email_transport_smtp.port');
$sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption');
$sUserName = self::$m_oConfig->Get('email_transport_smtp.username');
$oTransport = new Smtp();
$aOptions = [
'host' => $sHost,
'port' => $sPort,
'connection_class' => 'Laminas\Mail\Protocol\Smtp\Auth\Oauth',
'connection_config' => [
'ssl' => $sEncryption,
],
];
if (strlen($sUserName) > 0) {
$aOptions['connection_config']['username'] = $sUserName;
}
$oOptions = new SmtpOptions($aOptions);
$oTransport->setOptions($oOptions);
\Laminas\Mail\Protocol\Smtp\Auth\Oauth::setProvider(OAuthClientProviderFactory::getProviderForSMTP());
break;
case 'Null':
$oTransport = new Smtp();
break;
case 'LogFile':
$oTransport = new File();
$aOptions = new FileOptions([
'path' => APPROOT.'log/mail.log',
]);
$oTransport->setOptions($aOptions);
break;
case 'PHPMail':
default:
$oTransport = new Smtp();
}
$oKPI = new ExecutionKPI();
try {
$oTransport->send($this->m_oMessage);
$aIssues = array();
$oKPI->ComputeStats('Email Sent', 'Succeded');
return EMAIL_SEND_OK;
}
catch (Laminas\Mail\Transport\Exception\RuntimeException $e) {
IssueLog::Warning('Email sending failed: Some recipients were invalid');
$aIssues = array('Some recipients were invalid.');
$oKPI->ComputeStats('Email Sent', 'Error received');
return EMAIL_SEND_ERROR;
}
catch (Exception $e) {
$oKPI->ComputeStats('Email Sent', 'Error received');
throw $e;
}
}
/**
* Reprocess the body of the message (if it is an HTML message)
* to replace the URL of images based on attachments by a link
* to an embedded image (i.e. cid:....) and returns images to be attached as an array
*
* @param string $sBody Email body to process/alter
*
* @return array Array of Part that needs to be added as inline attachment later to render as embed
* @throws \ArchivedObjectException
* @throws \CoreException
*/
protected function EmbedInlineImages(string &$sBody)
{
$oDOMDoc = new DOMDocument();
$oDOMDoc->preserveWhitespace = true;
@$oDOMDoc->loadHTML('<?xml encoding="UTF-8"?>'.$sBody); // For loading HTML chunks where the character set is not specified
$oXPath = new DOMXPath($oDOMDoc);
$sXPath = '//img[@'.InlineImage::DOM_ATTR_ID.']';
$oImagesList = $oXPath->query($sXPath);
$oImagesContent = new \Laminas\Mime\Message();
$aImagesParts = [];
if ($oImagesList->length != 0) {
foreach ($oImagesList as $oImg) {
$iAttId = $oImg->getAttribute(InlineImage::DOM_ATTR_ID);
$oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */);
if ($oAttachment) {
$sImageSecret = $oImg->getAttribute('data-img-secret');
$sAttachmentSecret = $oAttachment->Get('secret');
if ($sImageSecret !== $sAttachmentSecret) {
// @see N°1921
// If copying from another iTop we could get an IMG pointing to an InlineImage with wrong secret
continue;
}
$oDoc = $oAttachment->Get('contents');
$sCid = uniqid('', true);
$oNewAttachment = new Part($oDoc->GetData());
$oNewAttachment->id = $sCid;
$oNewAttachment->type = $oDoc->GetMimeType();
$oNewAttachment->filename = $oDoc->GetFileName();
$oNewAttachment->disposition = Mime::DISPOSITION_INLINE;
$oNewAttachment->encoding = Mime::ENCODING_BASE64;
$oImagesContent->addPart($oNewAttachment);
$oImg->setAttribute('src', 'cid:'.$sCid);
$aImagesParts[] = $oNewAttachment;
}
}
}
$sBody = $oDOMDoc->saveHTML();
return $aImagesParts;
}
public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null)
{
//select a default sender if none is provided.
if (empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])) {
$this->SetRecipientFrom($this->m_aData['to']);
}
if ($bForceSynchronous) {
return $this->SendSynchronous($aIssues, $oLog);
} else {
$bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous');
if ($bConfigASYNC) {
return $this->SendAsynchronous($aIssues, $oLog);
} else {
return $this->SendSynchronous($aIssues, $oLog);
}
}
}
public function AddToHeader($sKey, $sValue)
{
if (!array_key_exists('headers', $this->m_aData)) {
$this->m_aData['headers'] = array();
}
$this->m_aData['headers'][$sKey] = $sValue;
if (strlen($sValue) > 0) {
$oHeaders = $this->m_oMessage->getHeaders();
$oHeaders->addHeaderLine($sKey, $sValue);
}
}
public function SetMessageId($sId)
{
$this->m_aData['message_id'] = $sId;
// Note: Swift will add the angle brackets for you
// so let's remove the angle brackets if present, for historical reasons
$sId = str_replace(array('<', '>'), '', $sId);
$this->m_oMessage->getHeaders()->addHeaderLine('Message-ID', $sId);
}
public function SetReferences($sReferences)
{
$this->AddToHeader('References', $sReferences);
}
/**
* Set the "In-Reply-To" header to allow emails to group as a conversation in modern mail clients (GMail, Outlook 2016+, ...)
*
* @link https://en.wikipedia.org/wiki/Email#Header_fields
*
* @param string $sMessageId
*
* @since 3.0.1 N°4849
*/
public function SetInReplyTo(string $sMessageId)
{
$this->AddToHeader('In-Reply-To', $sMessageId);
}
/**
* Set current Email body and process inline images.
*
* @param $sBody
* @param string $sMimeType
* @param $sCustomStyles
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \Symfony\Component\CssSelector\Exception\SyntaxErrorException
*/
public function SetBody($sBody, $sMimeType = Mime::TYPE_HTML, $sCustomStyles = null)
{
$oBody = new Laminas\Mime\Message();
$aAdditionalParts = [];
if (($sMimeType === Mime::TYPE_HTML) && ($sCustomStyles !== null)) {
$oDomDocument = CssInliner::fromHtml($sBody)->inlineCss($sCustomStyles)->getDomDocument();
HtmlPruner::fromDomDocument($oDomDocument)->removeElementsWithDisplayNone();
$sBody = CssToAttributeConverter::fromDomDocument($oDomDocument)->convertCssToVisualAttributes()->render(); // Adds html/body tags if not already present
}
$this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType);
// We don't want these modifications in m_aData['body'], otherwise it'll ruin asynchronous mail as they go through this method twice
if ($sMimeType === Mime::TYPE_HTML) {
$aAdditionalParts = $this->EmbedInlineImages($sBody);
}
// Add body content to as a new part
$oNewPart = new Part($sBody);
$oNewPart->encoding = Mime::ENCODING_8BIT;
$oNewPart->type = $sMimeType;
$oBody->addPart($oNewPart);
// Add additional images as new body parts
foreach ($aAdditionalParts as $oAdditionalPart) {
$oBody->addPart($oAdditionalPart);
}
if ($oBody->isMultiPart()) {
$oContentTypeHeader = $this->m_oMessage->getHeaders();
foreach ($oContentTypeHeader as $oHeader) {
if (!$oHeader instanceof ContentType) {
continue;
}
$oHeader->setType(Mime::MULTIPART_MIXED);
$oHeader->addParameter('boundary', $oBody->getMime()->boundary());
break;
}
}
$this->m_oMessage->setBody($oBody);
}
/**
* Add a new part to the existing body
*
* @param $sText
* @param string $sMimeType
*
* @return void
*/
public function AddPart($sText, $sMimeType = Mime::TYPE_HTML)
{
if (!array_key_exists('parts', $this->m_aData)) {
$this->m_aData['parts'] = array();
}
$this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType);
$oNewPart = new Part($sText);
$oNewPart->encoding = Mime::ENCODING_8BIT;
$oNewPart->type = $sMimeType;
$this->m_oMessage->getBody()->addPart($oNewPart);
}
public function AddAttachment($data, $sFileName, $sMimeType)
{
$oBody = $this->m_oMessage->getBody();
if (!$oBody->isMultiPart()) {
$multipart_content = new Part($oBody->generateMessage());
$multipart_content->setType($oBody->getParts()[0]->getType());
$multipart_content->setBoundary($oBody->getMime()->boundary());
$oBody = new Laminas\Mime\Message();
$oBody->addPart($multipart_content);
}
if (!array_key_exists('attachments', $this->m_aData)) {
$this->m_aData['attachments'] = array();
}
$this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType);
$oNewAttachment = new Part($data);
$oNewAttachment->type = $sMimeType;
$oNewAttachment->filename = $sFileName;
$oNewAttachment->disposition = Mime::DISPOSITION_ATTACHMENT;
$oNewAttachment->encoding = Mime::ENCODING_BASE64;
$oBody->addPart($oNewAttachment);
if ($oBody->isMultiPart()) {
$oContentTypeHeader = $this->m_oMessage->getHeaders();
foreach ($oContentTypeHeader as $oHeader) {
if (!$oHeader instanceof ContentType) {
continue;
}
$oHeader->setType(Mime::MULTIPART_MIXED);
$oHeader->addParameter('boundary', $oBody->getMime()->boundary());
break;
}
}
$this->m_oMessage->setBody($oBody);
}
public function SetSubject($sSubject)
{
$this->m_aData['subject'] = $sSubject;
$this->m_oMessage->setSubject($sSubject);
}
public function GetSubject()
{
return $this->m_oMessage->getSubject();
}
/**
* Helper to transform and sanitize addresses
* - get rid of empty addresses
*/
protected function AddressStringToArray($sAddressCSVList)
{
$aAddresses = array();
foreach (explode(',', $sAddressCSVList) as $sAddress) {
$sAddress = trim($sAddress);
if (strlen($sAddress) > 0) {
$aAddresses[] = $sAddress;
}
}
return $aAddresses;
}
public function SetRecipientTO($sAddress)
{
$this->m_aData['to'] = $sAddress;
if (!empty($sAddress)) {
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setTo($aAddresses);
}
}
public function GetRecipientTO($bAsString = false)
{
$aRes = $this->m_oMessage->getTo();
if ($aRes === null || $aRes->count() === 0) {
// There is no "To" header field
$aRes = array();
}
if ($bAsString) {
$aStrings = array();
foreach ($aRes as $oEmail) {
$sName = $oEmail->getName();
$sEmail = $oEmail->getEmail();
if (is_null($sName)) {
$aStrings[] = $sEmail;
} else {
$sName = str_replace(array('<', '>'), '', $sName);
$aStrings[] = "$sName <$sEmail>";
}
}
return implode(', ', $aStrings);
} else {
return $aRes;
}
}
public function SetRecipientCC($sAddress)
{
$this->m_aData['cc'] = $sAddress;
if (!empty($sAddress)) {
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setCc($aAddresses);
}
}
public function SetRecipientBCC($sAddress)
{
$this->m_aData['bcc'] = $sAddress;
if (!empty($sAddress)) {
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setBcc($aAddresses);
}
}
public function SetRecipientFrom($sAddress, $sLabel = '')
{
$this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel);
if ($sLabel != '') {
$this->m_oMessage->setFrom(array($sAddress => $sLabel));
} else if (!empty($sAddress)) {
$this->m_oMessage->setFrom($sAddress);
}
}
public function SetRecipientReplyTo($sAddress, $sLabel = '')
{
$this->m_aData['reply_to'] = array('address' => $sAddress, 'label' => $sLabel);
if ($sLabel != '') {
$this->m_oMessage->setReplyTo(array($sAddress => $sLabel));
} else if (!empty($sAddress)) {
$this->m_oMessage->setReplyTo($sAddress);
}
}
}

View File

@@ -0,0 +1,545 @@
<?php
// Copyright (C) 2010-2016 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Send an email (abstraction for synchronous/asynchronous modes)
*
* @copyright Copyright (C) 2010-2016 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Pelago\Emogrifier\CssInliner;
use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;
Swift_Preferences::getInstance()->setCharset('UTF-8');
class EmailSwiftMailer extends EMail
{
protected static $m_oConfig = null;
protected $m_aData; // For storing data to serialize
public function LoadConfig($sConfigFile = ITOP_DEFAULT_CONFIG_FILE)
{
if (is_null(self::$m_oConfig))
{
self::$m_oConfig = new Config($sConfigFile);
}
}
protected $m_oMessage;
/** @noinspection PhpMissingParentConstructorInspection */
public function __construct()
{
$this->m_aData = array();
$this->m_oMessage = new Swift_Message();
$this->SetRecipientFrom(MetaModel::GetConfig()->Get('email_default_sender_address'), MetaModel::GetConfig()->Get('email_default_sender_label'));
}
/**
* Custom serialization method
* No longer use the brute force "serialize" method since
* 1) It does not work with binary attachments (since they cannot be stored in a UTF-8 text field)
* 2) The size tends to be quite big (sometimes ten times the size of the email)
*/
public function SerializeV2()
{
return serialize($this->m_aData);
}
/**
* Custom de-serialization method
* @param string $sSerializedMessage The serialized representation of the message
*/
static public function UnSerializeV2($sSerializedMessage)
{
$aData = unserialize($sSerializedMessage);
$oMessage = new Email();
if (array_key_exists('body', $aData))
{
$oMessage->SetBody($aData['body']['body'], $aData['body']['mimeType']);
}
if (array_key_exists('message_id', $aData))
{
$oMessage->SetMessageId($aData['message_id']);
}
if (array_key_exists('bcc', $aData))
{
$oMessage->SetRecipientBCC($aData['bcc']);
}
if (array_key_exists('cc', $aData))
{
$oMessage->SetRecipientCC($aData['cc']);
}
if (array_key_exists('from', $aData))
{
$oMessage->SetRecipientFrom($aData['from']['address'], $aData['from']['label']);
}
if (array_key_exists('reply_to', $aData))
{
$oMessage->SetRecipientReplyTo($aData['reply_to']);
}
if (array_key_exists('to', $aData))
{
$oMessage->SetRecipientTO($aData['to']);
}
if (array_key_exists('subject', $aData))
{
$oMessage->SetSubject($aData['subject']);
}
if (array_key_exists('headers', $aData))
{
foreach($aData['headers'] as $sKey => $sValue)
{
$oMessage->AddToHeader($sKey, $sValue);
}
}
if (array_key_exists('parts', $aData))
{
foreach($aData['parts'] as $aPart)
{
$oMessage->AddPart($aPart['text'], $aPart['mimeType']);
}
}
if (array_key_exists('attachments', $aData))
{
foreach($aData['attachments'] as $aAttachment)
{
$oMessage->AddAttachment(base64_decode($aAttachment['data']), $aAttachment['filename'], $aAttachment['mimeType']);
}
}
return $oMessage;
}
protected function SendAsynchronous(&$aIssues, $oLog = null)
{
try
{
AsyncSendEmail::AddToQueue($this, $oLog);
}
catch(Exception $e)
{
$aIssues = array($e->GetMessage());
return EMAIL_SEND_ERROR;
}
$aIssues = array();
return EMAIL_SEND_PENDING;
}
public static function GetMailer()
{
return new EmailSwiftMailer();
}
protected function SendSynchronous(&$aIssues, $oLog = null)
{
// If the body of the message is in HTML, embed all images based on attachments
$this->EmbedInlineImages();
$this->LoadConfig();
$sTransport = self::$m_oConfig->Get('email_transport');
switch ($sTransport)
{
case 'SMTP':
$sHost = self::$m_oConfig->Get('email_transport_smtp.host');
$sPort = self::$m_oConfig->Get('email_transport_smtp.port');
$sEncryption = self::$m_oConfig->Get('email_transport_smtp.encryption');
$sUserName = self::$m_oConfig->Get('email_transport_smtp.username');
$sPassword = self::$m_oConfig->Get('email_transport_smtp.password');
$oTransport = new Swift_SmtpTransport($sHost, $sPort, $sEncryption);
if (strlen($sUserName) > 0)
{
$oTransport->setUsername($sUserName);
$oTransport->setPassword($sPassword);
}
break;
case 'Null':
$oTransport = new Swift_NullTransport();
break;
case 'LogFile':
$oTransport = new Swift_LogFileTransport(new Swift_Events_SimpleEventDispatcher());
$oTransport->setLogFile(APPROOT.'log/mail.log');
break;
case 'PHPMail':
default:
$oTransport = new Swift_SendmailTransport();
}
$oMailer = new Swift_Mailer($oTransport);
$aFailedRecipients = array();
$this->m_oMessage->setMaxLineLength(0);
$oKPI = new ExecutionKPI();
try
{
$iSent = $oMailer->send($this->m_oMessage, $aFailedRecipients);
if ($iSent === 0)
{
// Beware: it seems that $aFailedRecipients sometimes contains the recipients that actually received the message !!!
IssueLog::Warning('Email sending failed: Some recipients were invalid, aFailedRecipients contains: '.implode(', ', $aFailedRecipients));
$aIssues = array('Some recipients were invalid.');
$oKPI->ComputeStats('Email Sent', 'Error received');
return EMAIL_SEND_ERROR;
}
else
{
$aIssues = array();
$oKPI->ComputeStats('Email Sent', 'Succeded');
return EMAIL_SEND_OK;
}
}
catch (Exception $e)
{
$oKPI->ComputeStats('Email Sent', 'Error received');
throw $e;
}
}
/**
* Reprocess the body of the message (if it is an HTML message)
* to replace the URL of images based on attachments by a link
* to an embedded image (i.e. cid:....)
*/
protected function EmbedInlineImages()
{
if ($this->m_aData['body']['mimeType'] == 'text/html')
{
$oDOMDoc = new DOMDocument();
$oDOMDoc->preserveWhitespace = true;
@$oDOMDoc->loadHTML('<?xml encoding="UTF-8"?>'.$this->m_aData['body']['body']); // For loading HTML chunks where the character set is not specified
$oXPath = new DOMXPath($oDOMDoc);
$sXPath = '//img[@'.InlineImage::DOM_ATTR_ID.']';
$oImagesList = $oXPath->query($sXPath);
if ($oImagesList->length != 0)
{
foreach($oImagesList as $oImg)
{
$iAttId = $oImg->getAttribute(InlineImage::DOM_ATTR_ID);
$oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */);
if ($oAttachment)
{
$sImageSecret = $oImg->getAttribute('data-img-secret');
$sAttachmentSecret = $oAttachment->Get('secret');
if ($sImageSecret !== $sAttachmentSecret)
{
// @see N°1921
// If copying from another iTop we could get an IMG pointing to an InlineImage with wrong secret
continue;
}
$oDoc = $oAttachment->Get('contents');
$oSwiftImage = new Swift_Image($oDoc->GetData(), $oDoc->GetFileName(), $oDoc->GetMimeType());
$sCid = $this->m_oMessage->embed($oSwiftImage);
$oImg->setAttribute('src', $sCid);
}
}
}
$sHtmlBody = $oDOMDoc->saveHTML();
$this->m_oMessage->setBody($sHtmlBody, 'text/html', 'UTF-8');
}
}
public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null)
{
//select a default sender if none is provided.
if(empty($this->m_aData['from']['address']) && !empty($this->m_aData['to'])){
$this->SetRecipientFrom($this->m_aData['to']);
}
if ($bForceSynchronous)
{
return $this->SendSynchronous($aIssues, $oLog);
}
else
{
$bConfigASYNC = MetaModel::GetConfig()->Get('email_asynchronous');
if ($bConfigASYNC)
{
return $this->SendAsynchronous($aIssues, $oLog);
}
else
{
return $this->SendSynchronous($aIssues, $oLog);
}
}
}
public function AddToHeader($sKey, $sValue)
{
if (!array_key_exists('headers', $this->m_aData))
{
$this->m_aData['headers'] = array();
}
$this->m_aData['headers'][$sKey] = $sValue;
if (strlen($sValue) > 0)
{
$oHeaders = $this->m_oMessage->getHeaders();
switch(strtolower($sKey))
{
case 'return-path':
$this->m_oMessage->setReturnPath($sValue);
break;
default:
$oHeaders->addTextHeader($sKey, $sValue);
}
}
}
public function SetMessageId($sId)
{
$this->m_aData['message_id'] = $sId;
// Note: Swift will add the angle brackets for you
// so let's remove the angle brackets if present, for historical reasons
$sId = str_replace(array('<', '>'), '', $sId);
$oMsgId = $this->m_oMessage->getHeaders()->get('Message-ID');
$oMsgId->SetId($sId);
}
public function SetReferences($sReferences)
{
$this->AddToHeader('References', $sReferences);
}
public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null)
{
if (($sMimeType === 'text/html') && ($sCustomStyles !== null))
{
$oDomDocument = CssInliner::fromHtml($sBody)->inlineCss($sCustomStyles)->getDomDocument();
HtmlPruner::fromDomDocument($oDomDocument)->removeElementsWithDisplayNone();
$sBody = CssToAttributeConverter::fromDomDocument($oDomDocument)->convertCssToVisualAttributes()->render(); // Adds html/body tags if not already present
}
$this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType);
$this->m_oMessage->setBody($sBody, $sMimeType);
}
public function AddPart($sText, $sMimeType = 'text/html')
{
if (!array_key_exists('parts', $this->m_aData))
{
$this->m_aData['parts'] = array();
}
$this->m_aData['parts'][] = array('text' => $sText, 'mimeType' => $sMimeType);
$this->m_oMessage->addPart($sText, $sMimeType);
}
public function AddAttachment($data, $sFileName, $sMimeType)
{
if (!array_key_exists('attachments', $this->m_aData))
{
$this->m_aData['attachments'] = array();
}
$this->m_aData['attachments'][] = array('data' => base64_encode($data), 'filename' => $sFileName, 'mimeType' => $sMimeType);
$this->m_oMessage->attach(new Swift_Attachment($data, $sFileName, $sMimeType));
}
public function SetSubject($sSubject)
{
$this->m_aData['subject'] = $sSubject;
$this->m_oMessage->setSubject($sSubject);
}
public function GetSubject()
{
return $this->m_oMessage->getSubject();
}
/**
* Helper to transform and sanitize addresses
* - get rid of empty addresses
*/
protected function AddressStringToArray($sAddressCSVList)
{
$aAddresses = array();
foreach(explode(',', $sAddressCSVList) as $sAddress)
{
$sAddress = trim($sAddress);
if (strlen($sAddress) > 0)
{
$aAddresses[] = $sAddress;
}
}
return $aAddresses;
}
public function SetRecipientTO($sAddress)
{
$this->m_aData['to'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setTo($aAddresses);
}
}
public function GetRecipientTO($bAsString = false)
{
$aRes = $this->m_oMessage->getTo();
if ($aRes === null)
{
// There is no "To" header field
$aRes = array();
}
if ($bAsString)
{
$aStrings = array();
foreach ($aRes as $sEmail => $sName)
{
if (is_null($sName))
{
$aStrings[] = $sEmail;
}
else
{
$sName = str_replace(array('<', '>'), '', $sName);
$aStrings[] = "$sName <$sEmail>";
}
}
return implode(', ', $aStrings);
}
else
{
return $aRes;
}
}
public function SetRecipientCC($sAddress)
{
$this->m_aData['cc'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setCc($aAddresses);
}
}
public function SetRecipientBCC($sAddress)
{
$this->m_aData['bcc'] = $sAddress;
if (!empty($sAddress))
{
$aAddresses = $this->AddressStringToArray($sAddress);
$this->m_oMessage->setBcc($aAddresses);
}
}
public function SetRecipientFrom($sAddress, $sLabel = '')
{
$this->m_aData['from'] = array('address' => $sAddress, 'label' => $sLabel);
if ($sLabel != '')
{
$this->m_oMessage->setFrom(array($sAddress => $sLabel));
}
else if (!empty($sAddress))
{
$this->m_oMessage->setFrom($sAddress);
}
}
public function SetRecipientReplyTo($sAddress, $sLabel = '')
{
$this->m_aData['reply_to'] = $sAddress;
if (!empty($sAddress)) {
$this->m_oMessage->setReplyTo($sAddress);
}
}
}
/////////////////////////////////////////////////////////////////////////////////////
/**
* Extension to SwiftMailer: "debug" transport that pretends messages have been sent,
* but just log them to a file.
*
* @package Swift
* @author Denis Flaven
*/
class Swift_Transport_LogFileTransport extends Swift_Transport_NullTransport
{
protected $sLogFile;
/**
* @inheritDoc
*/
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{
$hFile = @fopen($this->sLogFile, 'a');
if ($hFile) {
$sTxt = "================== ".date('Y-m-d H:i:s')." ==================\n";
$sTxt .= $message->toString()."\n";
@fwrite($hFile, $sTxt);
@fclose($hFile);
}
return parent::send($message, $failedRecipients);
}
public function setLogFile($sFilename)
{
$this->sLogFile = $sFilename;
}
}
/**
* Pretends messages have been sent, but just log them to a file.
*
* @package Swift
* @author Denis Flaven
*/
class Swift_LogFileTransport extends Swift_Transport_LogFileTransport
{
/**
* Create a new LogFileTransport.
*/
public function __construct(Swift_Events_EventDispatcher $eventDispatcher)
{
parent::__construct($eventDispatcher);
call_user_func_array(
array($this, 'Swift_Transport_LogFileTransport::__construct'),
Swift_DependencyContainer::getInstance()
->createDependenciesFor('transport.null')
);
}
/**
* Create a new LogFileTransport instance.
*
* @return Swift_LogFileTransport
*/
public static function newInstance()
{
return new self(new Swift_Events_SimpleEventDispatcher());
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Combodo\iTop\Core\Email;
interface iEMail
{
public function SerializeV2();
/**
* Custom de-serialization method
*
* @param string $sSerializedMessage The serialized representation of the message
*
* @return \Email
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \Symfony\Component\CssSelector\Exception\SyntaxErrorException
*/
static public function UnSerializeV2($sSerializedMessage);
public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null);
public function AddToHeader($sKey, $sValue);
public function SetMessageId($sId);
public function SetReferences($sReferences);
public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null);
public function AddPart($sText, $sMimeType = 'text/html');
public function AddAttachment($data, $sFileName, $sMimeType);
public function SetSubject($sSubject);
public function GetSubject();
public function SetRecipientTO($sAddress);
public function GetRecipientTO($bAsString = false);
public function SetRecipientCC($sAddress);
public function SetRecipientBCC($sAddress);
public function SetRecipientFrom($sAddress, $sLabel = '');
public function SetRecipientReplyTo($sAddress, $sLabel = '');
}