diff --git a/core/action.class.inc.php b/core/action.class.inc.php
index b07ad65ac..756f98a8f 100644
--- a/core/action.class.inc.php
+++ b/core/action.class.inc.php
@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see
+use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
/**
* Persistent classes (internal): user defined actions
@@ -225,7 +226,22 @@ class ActionEmail extends ActionNotification
* @since 3.0.1
*/
const ENUM_HEADER_NAME_REFERENCES = 'References';
-
+ /**
+ * @var string
+ * @since 3.1.0
+ */
+ const TEMPLATE_BODY_CONTENT = '$content$';
+ /**
+ * Wraps the 'body' of the message for previewing inside an IFRAME -- i.e. without any of the iTop stylesheets being applied
+ * @var string
+ * @since 3.1.0
+ */
+ const CONTENT_HIGHLIGHT = '
';
+ /**
+ * Wraps a placeholder of the email's body for previewing inside an IFRAME -- i.e. without any of the iTop stylesheets being applied
+ * @var string
+ */
+ const FIELD_HIGHLIGHT = '\\$$1\\$';
/**
* @inheritDoc
*/
@@ -257,6 +273,10 @@ class ActionEmail extends ActionNotification
MetaModel::Init_AddAttribute(new AttributeTemplateString("subject", array("allowed_values" => null, "sql" => "subject", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
MetaModel::Init_AddAttribute(new AttributeTemplateHTML("body", array("allowed_values" => null, "sql" => "body", "default_value" => null, "is_null_allowed" => false, "depends_on" => array())));
MetaModel::Init_AddAttribute(new AttributeEnum("importance", array("allowed_values" => new ValueSetEnum('low,normal,high'), "sql" => "importance", "default_value" => 'normal', "is_null_allowed" => false, "depends_on" => array())));
+ MetaModel::Init_AddAttribute(new AttributeApplicationLanguage("language", array("sql"=>"language", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
+ MetaModel::Init_AddAttribute(new AttributeBlob("html_template", array("is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false)));
+ MetaModel::Init_AddAttribute(new AttributeEnum("ignore_notify", array("allowed_values" => new ValueSetEnum('yes,no'), "sql" => "ignore_notify", "default_value" => 'yes', "is_null_allowed" => false, "depends_on" => array())));
+
// Display lists
// - Attributes to be displayed for the complete details
@@ -266,8 +286,10 @@ class ActionEmail extends ActionNotification
0 => 'name',
1 => 'description',
2 => 'status',
- 3 => 'subject',
- 4 => 'body',
+ 3 => 'language',
+ 4 => 'html_template',
+ 5 => 'subject',
+ 6 => 'body',
// 5 => 'importance', not handled when sending the mail, better hide it then
),
'fieldset:ActionEmail:trigger' => array(
@@ -281,20 +303,21 @@ class ActionEmail extends ActionNotification
2 => 'reply_to',
3 => 'reply_to_label',
4 => 'test_recipient',
- 5 => 'to',
- 6 => 'cc',
- 7 => 'bcc',
+ 5 => 'ignore_notify',
+ 6 => 'to',
+ 7 => 'cc',
+ 8 => 'bcc',
),
),
));
// - Attributes to be displayed for a list
- MetaModel::Init_SetZListItems('list', array('status', 'to', 'subject'));
+ MetaModel::Init_SetZListItems('list', array('status', 'to', 'subject', 'language'));
// Search criteria
// - Standard criteria of the search
- MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'status', 'subject'));
+ MetaModel::Init_SetZListItems('standard_search', array('name', 'description', 'status', 'subject', 'language'));
// - Default criteria for the search
- MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'status', 'subject'));
+ MetaModel::Init_SetZListItems('default_search', array('name', 'description', 'status', 'subject', 'language'));
}
// count the recipients found
@@ -324,6 +347,15 @@ class ActionEmail extends ActionNotification
try
{
$oSearch = DBObjectSearch::FromOQL($sOQL);
+ if ($this->Get('ignore_notify') === 'no') {
+ // In theory it is possible to notify *any* kind of object,
+ // as long as there is an email attribute in the class
+ // So let's not assume that the selected class is a Person
+ $sFirstSelectedClass = $oSearch->GetClass();
+ if (MetaModel::IsValidAttCode($sFirstSelectedClass, 'notify')) {
+ $oSearch->AddCondition('notify', 'yes');
+ }
+ }
$oSearch->AllowAllData();
}
catch (OQLException $e)
@@ -448,114 +480,27 @@ class ActionEmail extends ActionNotification
*/
protected function _DoExecute($oTrigger, $aContextArgs, &$oLog)
{
- $sPreviousUrlMaker = ApplicationContext::SetUrlMakerClass();
- try
- {
- $this->m_iRecipients = 0;
- $this->m_aMailErrors = array();
-
- // Determine recipients
- //
- $sTo = $this->FindRecipients('to', $aContextArgs);
- $sCC = $this->FindRecipients('cc', $aContextArgs);
- $sBCC = $this->FindRecipients('bcc', $aContextArgs);
-
- $sFrom = MetaModel::ApplyParams($this->Get('from'), $aContextArgs);
- $sFromLabel = MetaModel::ApplyParams($this->Get('from_label'), $aContextArgs);
- $sReplyTo = MetaModel::ApplyParams($this->Get('reply_to'), $aContextArgs);
- $sReplyToLabel = MetaModel::ApplyParams($this->Get('reply_to_label'), $aContextArgs);
-
- $sSubject = MetaModel::ApplyParams($this->Get('subject'), $aContextArgs);
- $sBody = MetaModel::ApplyParams($this->Get('body'), $aContextArgs);
-
- $oObj = $aContextArgs['this->object()'];
- $sMessageId = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_MESSAGE_ID);
- $sReference = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_REFERENCES);
- }
- catch (Exception $e) {
- /** @noinspection PhpUnhandledExceptionInspection */
- throw $e;
- }
- finally {
- ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker);
- }
-
- if (!is_null($oLog)) {
- // Note: we have to secure this because those values are calculated
- // inside the try statement, and we would like to keep track of as
- // many data as we could while some variables may still be undefined
- if (isset($sTo)) {
- $oLog->Set('to', $sTo);
- }
- if (isset($sCC)) {
- $oLog->Set('cc', $sCC);
- }
- if (isset($sBCC)) {
- $oLog->Set('bcc', $sBCC);
- }
- if (isset($sFrom)) {
- $oLog->Set('from', $sFrom);
- }
- if (isset($sSubject)) {
- $oLog->Set('subject', $sSubject);
- }
- if (isset($sBody)) {
- $oLog->Set('body', $sBody);
- }
- }
$sStyles = file_get_contents(APPROOT.'css/email.css');
$sStyles .= MetaModel::GetConfig()->Get('email_css');
-
+
$oEmail = new EMail();
-
- if ($this->IsBeingTested()) {
- $oEmail->SetSubject('TEST['.$sSubject.']');
- $sTestBody = $sBody;
- $sTestBody .= "\n";
- $sTestBody .= "
Testing email notification ".$this->GetHyperlink()."
\n";
- $sTestBody .= "
The email should be sent with the following properties\n";
- $sTestBody .= "
\n";
- $sTestBody .= "- TO: $sTo
\n";
- $sTestBody .= "- CC: $sCC
\n";
- $sTestBody .= "- BCC: $sBCC
\n";
- $sTestBody .= empty($sFromLabel) ? "- From: $sFrom
\n" : "- From: $sFromLabel <$sFrom>
\n";
- $sTestBody .= empty($sReplyToLabel) ? "- Reply-To: $sReplyTo
\n" : "- Reply-To: $sReplyToLabel <$sReplyTo>
\n";
- $sTestBody .= "- References: $sReference
\n";
- $sTestBody .= "
\n";
- $sTestBody .= "\n";
- $sTestBody .= "
\n";
- $oEmail->SetBody($sTestBody, 'text/html', $sStyles);
- $oEmail->SetRecipientTO($this->Get('test_recipient'));
- $oEmail->SetRecipientFrom($sFrom, $sFromLabel);
- $oEmail->SetReferences($sReference);
- $oEmail->SetMessageId($sMessageId);
- // Note: N°4849 We pass the "References" identifier instead of the "Message-ID" on purpose as we want notifications emails to group around the triggering iTop object, not just the users' replies to the notification
- $oEmail->SetInReplyTo($sReference);
- } else {
- $oEmail->SetSubject($sSubject);
- $oEmail->SetBody($sBody, 'text/html', $sStyles);
- $oEmail->SetRecipientTO($sTo);
- $oEmail->SetRecipientCC($sCC);
- $oEmail->SetRecipientBCC($sBCC);
- $oEmail->SetRecipientFrom($sFrom, $sFromLabel);
- $oEmail->SetRecipientReplyTo($sReplyTo, $sReplyToLabel);
- $oEmail->SetReferences($sReference);
- $oEmail->SetMessageId($sMessageId);
- // Note: N°4849 We pass the "References" identifier instead of the "Message-ID" on purpose as we want notifications emails to group around the triggering iTop object, not just the users' replies to the notification
- $oEmail->SetInReplyTo($sReference);
+
+ $aEmailContent = $this->PrepareMessageContent($aContextArgs, $oLog);
+ $oEmail->SetSubject($aEmailContent['subject']);
+ $oEmail->SetBody($aEmailContent['body'], 'text/html', $sStyles);
+ $oEmail->SetRecipientTO($aEmailContent['to']);
+ $oEmail->SetRecipientCC($aEmailContent['cc']);
+ $oEmail->SetRecipientBCC($aEmailContent['bcc']);
+ $oEmail->SetRecipientFrom($aEmailContent['from'], $aEmailContent['from_label']);
+ $oEmail->SetRecipientReplyTo($aEmailContent['reply_to'], $aEmailContent['reply_to_label']);
+ $oEmail->SetReferences($aEmailContent['references']);
+ $oEmail->SetMessageId($aEmailContent['message_id']);
+ $oEmail->SetInReplyTo($aEmailContent['in_reply_to']);
+
+ foreach($aEmailContent['attachments'] as $aAttachment) {
+ $oEmail->AddAttachment($aAttachment['data'], $aAttachment['filename'], $aAttachment['mime_type']);
}
-
- if (isset($aContextArgs['attachments']))
- {
- $aAttachmentReport = array();
- foreach($aContextArgs['attachments'] as $oDocument)
- {
- $oEmail->AddAttachment($oDocument->GetData(), $oDocument->GetFileName(), $oDocument->GetMimeType());
- $aAttachmentReport[] = array($oDocument->GetFileName(), $oDocument->GetMimeType(), strlen($oDocument->GetData()));
- }
- $oLog->Set('attachments', $aAttachmentReport);
- }
-
+
if (empty($this->m_aMailErrors))
{
if ($this->m_iRecipients == 0)
@@ -564,6 +509,7 @@ class ActionEmail extends ActionNotification
}
else
{
+ $aErrors = [];
$iRes = $oEmail->Send($aErrors, false, $oLog); // allow asynchronous mode
switch ($iRes)
{
@@ -588,13 +534,147 @@ class ActionEmail extends ActionNotification
}
}
+ /**
+ * @param array $aContextArgs
+ * @param \EventNotification $oLog
+ *
+ * @return array
+ * @throws \CoreException
+ * @throws \Exception
+ * @since 3.1.0 N°918
+ */
+ protected function PrepareMessageContent($aContextArgs, &$oLog): array
+ {
+ $aMessageContent = [
+ 'to' => '',
+ 'cc' => '',
+ 'bcc' => '',
+ 'from' => '',
+ 'from_label' => '',
+ 'reply_to' => '',
+ 'reply_to_label' => '',
+ 'subject' => '',
+ 'body' => '',
+ 'references' => '',
+ 'message_id' => '',
+ 'in_reply_to' => '',
+ 'attachments' => [],
+ ];
+ $sPreviousUrlMaker = ApplicationContext::SetUrlMakerClass();
+ $sPreviousLanguage = Dict::GetUserLanguage();
+ $aPreviousPluginProperties = ApplicationContext::GetPluginProperties('QueryLocalizerPlugin');
+ if ($this->Get('language') !== '') {
+ // If a language is specified for this action, force this language
+ // when rendering all placeholders inside this message
+ Dict::SetUserLanguage($this->Get('language'));
+ AttributeDateTime::LoadFormatFromConfig();
+ ApplicationContext::SetPluginProperty('QueryLocalizerPlugin', 'language_code', $this->Get('language'));
+ }
+
+ try
+ {
+ $this->m_iRecipients = 0;
+ $this->m_aMailErrors = array();
+
+ // Determine recipients
+ //
+ $aMessageContent['to'] = $this->FindRecipients('to', $aContextArgs);
+ $aMessageContent['cc'] = $this->FindRecipients('cc', $aContextArgs);
+ $aMessageContent['bcc'] = $this->FindRecipients('bcc', $aContextArgs);
+
+ $aMessageContent['from'] = MetaModel::ApplyParams($this->Get('from'), $aContextArgs);
+ $aMessageContent['from_label'] = MetaModel::ApplyParams($this->Get('from_label'), $aContextArgs);
+ $aMessageContent['reply_to'] = MetaModel::ApplyParams($this->Get('reply_to'), $aContextArgs);
+ $aMessageContent['reply_to_label'] = MetaModel::ApplyParams($this->Get('reply_to_label'), $aContextArgs);
+
+ $aMessageContent['subject'] = MetaModel::ApplyParams($this->Get('subject'), $aContextArgs);
+ $sBody = $this->BuildMessageBody(false);
+ $aMessageContent['body'] = MetaModel::ApplyParams($sBody, $aContextArgs);
+
+ $oObj = $aContextArgs['this->object()'];
+ $aMessageContent['message_id'] = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_MESSAGE_ID);
+ $aMessageContent['references'] = $this->GenerateIdentifierForHeaders($oObj, static::ENUM_HEADER_NAME_REFERENCES);
+ }
+ catch (Exception $e) {
+ /** @noinspection PhpUnhandledExceptionInspection */
+ throw $e;
+ }
+ finally {
+ ApplicationContext::SetUrlMakerClass($sPreviousUrlMaker);
+ Dict::SetUserLanguage($sPreviousLanguage);
+ AttributeDateTime::LoadFormatFromConfig();
+ ApplicationContext::SetPluginProperty('QueryLocalizerPlugin', 'language_code', $aPreviousPluginProperties['language_code'] ?? null);
+ }
+
+ if (!is_null($oLog)) {
+ // Note: we have to secure this because those values are calculated
+ // inside the try statement, and we would like to keep track of as
+ // many data as we could while some variables may still be undefined
+ if (isset($aMessageContent['to'])) {
+ $oLog->Set('to', $aMessageContent['to']);
+ }
+ if (isset($aMessageContent['cc'])) {
+ $oLog->Set('cc', $aMessageContent['cc']);
+ }
+ if (isset($aMessageContent['bcc'])) {
+ $oLog->Set('bcc', $aMessageContent['bcc']);
+ }
+ if (isset($aMessageContent['from'])) {
+ $oLog->Set('from', $aMessageContent['from']);
+ }
+ if (isset($aMessageContent['subject'])) {
+ $oLog->Set('subject', $aMessageContent['subject']);
+ }
+ if (isset($aMessageContent['body'])) {
+ $oLog->Set('body', HTMLSanitizer::Sanitize($aMessageContent['body']));
+ }
+ }
+ $sStyles = file_get_contents(APPROOT.'css/email.css');
+ $sStyles .= MetaModel::GetConfig()->Get('email_css');
+
+ if ($this->IsBeingTested()) {
+ $sTestBody = $aMessageContent['body'];
+ $sTestBody .= "\n";
+ $sTestBody .= "
Testing email notification ".$this->GetHyperlink()."
\n";
+ $sTestBody .= "
The email should be sent with the following properties\n";
+ $sTestBody .= "
\n";
+ $sTestBody .= "- TO: {$aMessageContent['to']}
\n";
+ $sTestBody .= "- CC: {$aMessageContent['cc']}
\n";
+ $sTestBody .= "- BCC: {$aMessageContent['bcc']}
\n";
+ $sTestBody .= empty($aMessageContent['from_label']) ? "- From: {$aMessageContent['from']}
\n" : "- From: {$aMessageContent['from_label']} <{$aMessageContent['from']}>
\n";
+ $sTestBody .= empty($aMessageContent['reply_to_label']) ? "- Reply-To: {$aMessageContent['reply_to']}
\n" : "- Reply-To: {$aMessageContent['reply_to_label']} <{$aMessageContent['reply_to']}>
\n";
+ $sTestBody .= "- References: {$aMessageContent['references']}
\n";
+ $sTestBody .= "
\n";
+ $sTestBody .= "\n";
+ $sTestBody .= "
\n";
+ $aMessageContent['subject'] = 'TEST['.$aMessageContent['subject'].']';
+ $aMessageContent['body'] = $sTestBody;
+ $aMessageContent['to'] = $this->Get('test_recipient');
+ }
+ // Note: N°4849 We pass the "References" identifier instead of the "Message-ID" on purpose as we want notifications emails to group around the triggering iTop object, not just the users' replies to the notification
+ $aMessageContent['in_reply_to'] = $aMessageContent['references'];
+
+ if (isset($aContextArgs['attachments']))
+ {
+ $aAttachmentReport = array();
+ foreach($aContextArgs['attachments'] as $oDocument)
+ {
+ $aMessageContent['attachments'][] = ['data' => $oDocument->GetData(), 'filename' => $oDocument->GetFileName(), 'mime_type' => $oDocument->GetMimeType()];
+ $aAttachmentReport[] = array($oDocument->GetFileName(), $oDocument->GetMimeType(), strlen($oDocument->GetData()));
+ }
+ $oLog->Set('attachments', $aAttachmentReport);
+ }
+
+ return $aMessageContent;
+ }
+
/**
* @param \DBObject $oObject
* @param string $sHeaderName {@see \ActionEmail::ENUM_HEADER_NAME_REFERENCES}, {@see \ActionEmail::ENUM_HEADER_NAME_MESSAGE_ID}
*
* @return string The formatted identifier for $sHeaderName based on $oObject
* @throws \Exception
- * @since 3.0.1 N°4849
+ * @since 3.1.0 N°4849
*/
protected function GenerateIdentifierForHeaders(DBObject $oObject, string $sHeaderName): string
{
@@ -629,4 +709,65 @@ class ActionEmail extends ActionNotification
]);
throw new Exception($sErrorMessage);
}
+
+ /**
+ * Compose the body of the message from the 'body' attribute and the HTML template (if any)
+ * @since 3.1.0 N°4849
+ * @param bool $bHighlightPlaceholders If true add some extra HTML around placeholders to highlight them
+ * @return string
+ */
+ protected function BuildMessageBody(bool $bHighlightPlaceholders = false): string
+ {
+ $sBody = $this->Get('body');
+ /** @var ormDocument $oHtmlTemplate */
+ $oHtmlTemplate = $this->Get('html_template');
+ if ($oHtmlTemplate && !$oHtmlTemplate->IsEmpty()) {
+ $sHtmlTemplate = $oHtmlTemplate->GetData();
+ if (false !== mb_strpos($sHtmlTemplate, static::TEMPLATE_BODY_CONTENT)) {
+ if ($bHighlightPlaceholders) {
+ // Highlight the $content$ block
+ $sBody = sprintf(static::CONTENT_HIGHLIGHT, $sBody);
+ }
+ $sBody = str_replace(static::TEMPLATE_BODY_CONTENT, $sBody, $oHtmlTemplate->GetData()); // str_replace is ok as long as both strings are well-formed UTF-8
+ } else {
+ $sBody = $oHtmlTemplate->GetData();
+ }
+ }
+ if($bHighlightPlaceholders) {
+ // Highlight all placeholders
+ $sBody = preg_replace('/\\$([^$]+)\\$/', static::FIELD_HIGHLIGHT, $sBody);
+ }
+ return $sBody;
+ }
+
+ /**
+ * @since 3.1.0 N°4849
+ * @inheritDoc
+ * @see cmdbAbstractObject::DisplayBareRelations()
+ */
+ public function DisplayBareRelations(WebPage $oPage, $bEditMode = false)
+ {
+ parent::DisplayBareRelations($oPage, false);
+ if (!$bEditMode) {
+ $oPage->SetCurrentTab('action_email__preview', Dict::S('ActionEmail:preview_tab'), Dict::S('ActionEmail:preview_tab+'));
+ $sBody = $this->BuildMessageBody(true);
+ TwigHelper::RenderIntoPage($oPage, APPROOT.'/', 'templates/datamodel/ActionEmail/email-notification-preview', ['iframe_content' => $sBody]);
+ }
+ }
+
+ /**
+ * @since 3.1.0
+ * @inheritDoc
+ * @see cmdbAbstractObject::DoCheckToWrite()
+ */
+ public function DoCheckToWrite()
+ {
+ parent::DoCheckToWrite();
+ $oHtmlTemplate = $this->Get('html_template');
+ if ($oHtmlTemplate && !$oHtmlTemplate->IsEmpty()) {
+ if (false === mb_strpos($oHtmlTemplate->GetData(), static::TEMPLATE_BODY_CONTENT)) {
+ $this->m_aCheckWarnings[] = Dict::Format('ActionEmail:content_placeholder_missing', static::TEMPLATE_BODY_CONTENT, Dict::S('Class:ActionEmail/Attribute:body'));
+ }
+ }
+ }
}
diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php
index 9dcbdd381..936d36c92 100644
--- a/core/attributedef.class.inc.php
+++ b/core/attributedef.class.inc.php
@@ -6214,7 +6214,7 @@ class AttributeDateTime extends AttributeDBField
/**
* Load the 3 settings: date format, time format and data_time format from the configuration
*/
- protected static function LoadFormatFromConfig()
+ public static function LoadFormatFromConfig()
{
$aFormats = MetaModel::GetConfig()->Get('date_and_time_format');
$sLang = Dict::GetUserLanguage();
@@ -8343,7 +8343,7 @@ class AttributeBlob extends AttributeDefinition
$aValues[$this->GetCode().'_data'] = '';
$aValues[$this->GetCode().'_mimetype'] = '';
$aValues[$this->GetCode().'_filename'] = '';
- $aValues[$this->GetCode().'_downloads_count'] = ''; // Note: Should this be set to \ormDocument::DEFAULT_DOWNLOADS_COUNT ?
+ $aValues[$this->GetCode().'_downloads_count'] = \ormDocument::DEFAULT_DOWNLOADS_COUNT;
}
return $aValues;
diff --git a/dictionaries/en.dictionary.itop.core.php b/dictionaries/en.dictionary.itop.core.php
index 11bd52d67..5538e7c0c 100644
--- a/dictionaries/en.dictionary.itop.core.php
+++ b/dictionaries/en.dictionary.itop.core.php
@@ -569,9 +569,22 @@ While editing, click on the magnifier to get pertinent examples',
'Class:ActionEmail/Attribute:importance/Value:normal+' => '',
'Class:ActionEmail/Attribute:importance/Value:high' => 'High',
'Class:ActionEmail/Attribute:importance/Value:high+' => '',
+ 'Class:ActionEmail/Attribute:language' => 'Language',
+ 'Class:ActionEmail/Attribute:language+' => 'Language to use for placeholders ($xxx$) inside the message (state, importance, priority, etc)',
+ 'Class:ActionEmail/Attribute:html_template' => 'HTML template',
+ 'Class:ActionEmail/Attribute:html_template+' => 'Optional HTML template wrapping around the content of the \'Body\' attribute below, useful for tailored email layouts (in the template, content of the \'Body\' attribute will replace the $content$ placeholder)',
+ 'Class:ActionEmail/Attribute:ignore_notify' => 'Ignore the Notify flag',
+ 'Class:ActionEmail/Attribute:ignore_notify+' => 'If set to \'Yes\' the \'Notify\' flag on Contacts has no effect.',
+ 'Class:ActionEmail/Attribute:ignore_notify/Value:no' => 'No',
+ 'Class:ActionEmail/Attribute:ignore_notify/value:yes' => 'Yes',
'ActionEmail:main' => 'Message',
'ActionEmail:trigger' => 'Triggers',
'ActionEmail:recipients' => 'Contacts',
+ 'ActionEmail:preview_tab' => 'Preview',
+ 'ActionEmail:preview_tab+' => 'Preview of the eMail template',
+ 'ActionEmail:preview_warning' => 'The actual eMail may look different in the eMail client than this preview in your browser.',
+ 'ActionEmail:preview_more_info' => 'For more information about the CSS features supported by the different eMail clients, refer to %1$s',
+ 'ActionEmail:content_placeholder_missing' => 'The placeholder "%1$s" was not found in the HTML template. The content of the field "%2$s" will not be included in the generated emails.',
));
//
diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php
index eac9f864c..605852be0 100644
--- a/dictionaries/fr.dictionary.itop.core.php
+++ b/dictionaries/fr.dictionary.itop.core.php
@@ -569,10 +569,22 @@ En édition, cliquez sur la loupe pour obtenir des exemples pertinents.',
'Class:ActionEmail/Attribute:importance/Value:normal+' => '',
'Class:ActionEmail/Attribute:importance/Value:high' => 'Haute',
'Class:ActionEmail/Attribute:importance/Value:high+' => '',
+ 'Class:ActionEmail/Attribute:language' => 'Langue',
+ 'Class:ActionEmail/Attribute:language+' => 'Langue utilisée pour les placeholders ($xxx$) dans le message (statut, importance, priorité, etc)',
+ 'Class:ActionEmail/Attribute:html_template' => 'Modèle HTML',
+ 'Class:ActionEmail/Attribute:html_template+' => 'Optionnel, modèle HTML encapsulant le contenu du champ \'Message\' ci-dessous, utile pour des mises en page sur mesure (dans le modèle, le contenu du champ \'Message\' remplacera le mot-clé $content$)',
+ 'Class:ActionEmail/Attribute:ignore_notify' => 'Ignorer la préférence \'Notification\'',
+ 'Class:ActionEmail/Attribute:ignore_notify+' => 'Si \'Oui\', le champ \'Notification\' des Contacts est ignoré.',
+ 'Class:ActionEmail/Attribute:ignore_notify/Value:no' => 'Non',
+ 'Class:ActionEmail/Attribute:ignore_notify/Value:yes' => 'Oui',
'ActionEmail:main' => 'Message',
'ActionEmail:trigger' => 'Conditions de déclenchement',
'ActionEmail:recipients' => 'Contacts',
-
+ 'ActionEmail:preview_tab' => 'Aperçu',
+ 'ActionEmail:preview_tab+' => 'Aperçu du modèle de mèl',
+ 'ActionEmail:preview_warning' => 'Le mèl peut s\'afficher différement dans les clients mèl par rapport à cet aperçu dans votre navigateur.',
+ 'ActionEmail:preview_more_info' => 'Pour plus d\'informations sur les fonctionnalités CSS supportées par les différents client mèl, consultez %1$s.',
+ 'ActionEmail:content_placeholder_missing' => 'The mot-clé "%1$s" ne figure pas dans le modèle HTML. Le contenu du champ "%2$s" ne sera pas intégré dans les mèls générés.',
));
//
diff --git a/templates/datamodel/ActionEmail/email-notification-preview.html.twig b/templates/datamodel/ActionEmail/email-notification-preview.html.twig
new file mode 100644
index 000000000..717659f6b
--- /dev/null
+++ b/templates/datamodel/ActionEmail/email-notification-preview.html.twig
@@ -0,0 +1,13 @@
+{% apply spaceless %}
+{% UIAlert ForWarning{sTitle:'', sContent: '', sId: null} %}
+
+
+
+
{{ 'ActionEmail:preview_warning'|dict_s }}
{{ 'ActionEmail:preview_more_info'|dict_format('
www.canimeail.com')|raw }}
+
+
+{% EndUIAlert %}
+
+
+
+{% endapply %}
\ No newline at end of file
diff --git a/tests/php-unit-tests/unitary-tests/core/ActionEmailTest.php b/tests/php-unit-tests/unitary-tests/core/ActionEmailTest.php
index 3f78df997..f8a7ed97d 100644
--- a/tests/php-unit-tests/unitary-tests/core/ActionEmailTest.php
+++ b/tests/php-unit-tests/unitary-tests/core/ActionEmailTest.php
@@ -7,6 +7,7 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
use MetaModel;
use utils;
+use Dict;
/**
* @runTestsInSeparateProcesses
@@ -23,11 +24,22 @@ class ActionEmailTest extends ItopDataTestCase
/** @var \ActionEmail|null Temp ActionEmail created for tests */
protected static $oActionEmail = null;
+
+ /** @var \ormDocument|null Temp ormDocument for tests */
+ protected static $oDocument = null;
+
+ /** @var \UserRequest|null Temp ormDocument for tests */
+ protected static $oUserRequest = null;
+
+ /** @var string[] Dict formatted message, because the Dict class is not available in providers */
+ protected static $aWarningMessages;
protected function setUp(): void
{
parent::setUp();
+ $this->RequireOnceItopFile('application/Html2Text.php');
+
static::$oActionEmail = MetaModel::NewObject('ActionEmail', [
'name' => 'Test action',
'status' => 'disabled',
@@ -35,7 +47,27 @@ class ActionEmailTest extends ItopDataTestCase
'subject' => 'Test subject',
'body' => 'Test body',
]);
+
+ $sHtml =
+<<
+
+ | Formatted eMail |
+ | \$content\$ |
+
+HTML
+ ;
+
static::$oActionEmail->DBInsert();
+ static::$oDocument = new \ormDocument($sHtml, 'text/html', 'sample.html');
+ static::$oUserRequest = MetaModel::NewObject('UserRequest', [
+ 'title' => 'Test UserRequest',
+ 'description' => 'Multi-line
description
'
+ ]);
+
+ static::$aWarningMessages = [
+ 'warning-missing-content' => Dict::Format('ActionEmail:content_placeholder_missing', '$content$', Dict::S('Class:ActionEmail/Attribute:body'))
+ ];
}
/**
@@ -77,7 +109,6 @@ class ActionEmailTest extends ItopDataTestCase
$this->assertEquals($sExpectedIdentifier, $sTestedIdentifier);
break;
}
-
}
public function GenerateIdentifierForHeadersProvider()
@@ -88,4 +119,206 @@ class ActionEmailTest extends ItopDataTestCase
'IncorrectHeaderName' => ['IncorrectHeaderName'],
];
}
+
+ /**
+ * @dataProvider prepareMessageContentProvider
+ */
+ public function testPrepareMessageContent($sCurrentUserLanguage, $aActionFields, $aFieldsToCheck)
+ {
+ \Dict::SetUserLanguage($sCurrentUserLanguage);
+ $aContext = ['this->object()' => static::$oUserRequest];
+ $oActionEmail = MetaModel::NewObject('ActionEmail', [
+ 'name' => 'Test action',
+ 'status' => 'disabled',
+ 'from' => 'unit-test@openitop.org',
+ 'subject' => 'Test subject',
+ 'body' => 'Test body',
+ ]);
+ foreach($aActionFields as $sCode => $value) {
+ if ($sCode === 'html_template') {
+ // special case since the data provider cannot create ormDocument objects
+ $oActionEmail->Set($sCode, static::$oDocument);
+ } else {
+ $oActionEmail->Set($sCode, $value);
+ }
+
+ }
+ $oActionEmail->DBInsert();
+
+ $oOrg = $this->CreateOrganization('testPrepareMessageContent');
+
+ $oContact1 = MetaModel::NewObject('Person', [
+ 'name' => 'Person 1',
+ 'first_name' => 'PrepareMessageContent',
+ 'org_id' => $oOrg->GetKey(),
+ 'email' => 'some.valid@email.com',
+ 'notify' => 'yes',
+ ]);
+ $oContact1->DBInsert();
+
+
+ $oContact2 = MetaModel::NewObject('Person', [
+ 'name' => 'Person 2',
+ 'first_name' => 'PrepareMessageContent',
+ 'org_id' => $oOrg->GetKey(),
+ 'email' => 'some.valid2@email.com',
+ 'notify' => 'no',
+ ]);
+ $oContact2->DBInsert();
+
+ $oLog = null;
+
+ $aEmailContent = $this->InvokeNonPublicMethod('\ActionEmail', 'PrepareMessageContent', $oActionEmail, [$aContext, &$oLog]);
+ // Normalize the content of the body to simplify the comparison, useful when status == test
+ $aEmailContent['body'] = preg_replace('/title="[^"]+"/', 'title="****"', $aEmailContent['body']);
+ $aEmailContent['body'] = preg_replace('/class="object-ref-link" href="[^"]+"/', 'class="object-ref-link" href="****"', $aEmailContent['body']);
+ $aEmailContent['body'] = preg_replace('/References: <[^>]+>/', 'References: ****', $aEmailContent['body']);
+ foreach($aFieldsToCheck as $sCode => $expectedValue) {
+ $this->assertEquals($expectedValue, $aEmailContent[$sCode]);
+ }
+ }
+
+ public function prepareMessageContentProvider()
+ {
+ return [
+ 'subject-no-placeholder' => [
+ 'EN US',
+ ['subject' => 'This is a test'],
+ ['subject' => 'This is a test'],
+ ],
+ 'subject-with-placeholder' => [
+ 'EN US',
+ ['subject' => 'Ticket "$this->title$" created'],
+ ['subject' => 'Ticket "Test UserRequest" created'],
+ ],
+ 'simple-to-oql' => [
+ 'EN US',
+ ['to' => "SELECT Person WHERE email='some.valid@email.com'"],
+ ['to' => 'some.valid@email.com'],
+ ],
+ 'simple-to-oql_ignoring_ignore_notify' => [
+ 'EN US',
+ ['to' => "SELECT Person WHERE email='some.valid2@email.com'"],
+ ['to' => 'some.valid2@email.com'], // contact2 has 'notify' set to 'no' BUT by default when don't care
+ ],
+ 'simple-to-oql-not-bypassing-notify' => [
+ 'EN US',
+ ['to' => "SELECT Person WHERE email='some.valid2@email.com'", 'ignore_notify' => 'no'],
+ ['to' => ''], // contact2 has 'notify' set to 'no'
+ ],
+ 'subject-with-localized-placeholder (default behavior)' => [
+ 'EN US',
+ ['subject' => 'Ticket in state "$this->label(status)$" created'],
+ ['subject' => 'Ticket in state "New" created'],
+ ],
+ 'subject-with-localized-placeholder (default behavior 2)' => [
+ 'FR FR',
+ ['subject' => 'Ticket in state "$this->label(status)$" created'],
+ ['subject' => 'Ticket in state "Nouveau" created'],
+ ],
+ 'subject-with-localized-placeholder (new behavior)' => [
+ 'FR FR',
+ ['subject' => 'Ticket in state "$this->label(status)$" created', 'language' => 'EN US'],
+ ['subject' => 'Ticket in state "New" created'],
+ ],
+ 'simple-body-with-placeholder' => [
+ 'EN US',
+ ['body' => 'Ticket "$this->title$" created.
'],
+ ['body' => 'Ticket "Test UserRequest" created.
'],
+ ],
+ 'simple-body-with-placeholder-TEST-mode' => [
+ 'EN US',
+ ['body' => 'Ticket "$this->title$" created.
', 'status' => 'test'],
+ ['body' =>
+<<Ticket "Test UserRequest" created.
+
Testing email notification Test action
+
The email should be sent with the following properties
+
+- TO:
+- CC:
+- BCC:
+- From: unit-test@openitop.org
+- Reply-To:
+- References: ****
+
+
+
+
+HTML
+ ,
+ 'subject' => 'TEST[Test subject]',
+ ],
+ ],
+ 'more-complex-body-and-title-with-placeholder' => [
+ 'EN US',
+ ['subject' => 'Ticket "$this->title$" created'],
+ ['subject' => 'Ticket "Test UserRequest" created'],
+ ['body' => 'Ticket "$this->title$" created.
Description: $this->html(description)
'],
+ ['body' => 'Ticket "Test UserRequest" created.
Description:
Multi-line
description
'],
+ ],
+ 'simple-body-with-placeholder_and_template' => [
+ 'EN US',
+ ['body' => 'Ticket "$this->title$" created.
', 'html_template' => true],
+ ['body' =>
+<<
+
+ | Formatted eMail |
+ Ticket "Test UserRequest" created. |
+
+HTML
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider doCheckToWriteProvider
+ * @param string $sBody
+ * @param string $sHtmlTemplate
+ * @param string[] $aExpectedWarnings
+ */
+ public function testDoCheckToWrite(string $sBody, ?string $sHtmlTemplate, $expectedWarnings)
+ {
+ $oActionEmail = new ActionEmail();
+ // Set mandatory fields
+ $oActionEmail->Set('name', 'test');
+ $oActionEmail->Set('subject', 'Ga Bu Zo Meu');
+ // Set the fields for testing
+ $oActionEmail->Set('body', $sBody);
+ if ($sHtmlTemplate !== null) {
+ $oDoc = new \ormDocument($sHtmlTemplate, 'text/html', 'template.html');
+ $oActionEmail->Set('html_template', $oDoc);
+ }
+ $oActionEmail->DoCheckToWrite();
+ $aWarnings = $this->GetNonPublicProperty($oActionEmail, 'm_aCheckWarnings');
+ if ($expectedWarnings === null) {
+ $this->assertEquals($aWarnings, $expectedWarnings);
+ } else {
+ // The warning messages are localized, but the provider functions does not
+ // have access to the Dict class, so let's replace the value given by the
+ // provider by a statically precomputed and localized message
+ foreach($expectedWarnings as $index => $sMessageKey) {
+ $expectedWarnings[$index] = static::$aWarningMessages[$sMessageKey];
+ }
+ $this->assertEquals($aWarnings, $expectedWarnings);
+ }
+ }
+
+ public function doCheckToWriteProvider()
+ {
+ return [
+ 'no warnings' => [
+ 'Some text here
',
+ '$content$
',
+ null
+ ],
+ '$content$ missing' => [
+ 'Some text here
',
+ 'no placeholder
',
+ [ 'warning-missing-content' ]
+ ],
+ ];
+ }
}
\ No newline at end of file