new: [mail] HTML alert emails

pull/6967/head
Jakub Onderka 2021-02-05 15:40:22 +01:00
parent 79e78b817f
commit e2b1ba18a3
5 changed files with 152 additions and 165 deletions

View File

@ -581,7 +581,7 @@ class SendEmail
$email->replyTo(Configure::read('MISP.email_reply_to'));
}
$email->from(Configure::read('MISP.email'));
$email->from(Configure::read('MISP.email'), Configure::read('MISP.email_from_name'));
$email->returnPath(Configure::read('MISP.email')); // TODO?
$email->to($user['User']['email']);
$email->subject($subject);

View File

@ -3219,7 +3219,7 @@ class Event extends AppModel
])[0];
if ($this->UserSetting->checkPublishFilter($user, $eventForUser)) {
$body = $this->__buildAlertEmailBody($eventForUser, $user, $oldpublish);
$body = $this->renderAlertEmail($eventForUser, $user, $oldpublish);
$this->User->sendEmail(['User' => $user], $body, $bodyNoEnc, $subject);
}
if ($jobId) {
@ -3233,136 +3233,41 @@ class Event extends AppModel
return true;
}
/**
* @param string $body
* @param string $bodyTempOther
* @param array $objects
* @param int|null $oldpublish Timestamp of latest publish
*/
private function __buildAlertEmailObject(&$body, &$bodyTempOther, array $objects, $oldpublish)
{
foreach ($objects as $object) {
if (isset($oldpublish) && isset($object['timestamp']) && $object['timestamp'] > $oldpublish) {
$body .= '* ';
} else {
$body .= ' ';
}
$body .= $object['name'] . '/' . $object['meta-category'] . "\n";
if (!empty($object['Attribute'])) {
$body .= $this->__buildAlertEmailAttribute($body, $bodyTempOther, $object['Attribute'], $oldpublish, ' ');
}
}
}
/**
* @param string $body
* @param string $bodyTempOther
* @param array $attributes
* @param int|null $oldpublish Timestamp of latest publish
* @param string $indent
*/
private function __buildAlertEmailAttribute(&$body, &$bodyTempOther, array $attributes, $oldpublish, $indent = ' ')
{
$appendlen = 20;
foreach ($attributes as $attribute) {
$ids = $attribute['to_ids'] ? ' (IDS)' : '';
// Defanging URLs (Not "links") emails domains/ips in notification emails
$value = $attribute['value'];
if ('url' == $attribute['type'] || 'uri' == $attribute['type']) {
$value = str_ireplace("http", "hxxp", $value);
$value = str_ireplace(".", "[.]", $value);
} elseif (in_array($attribute['type'], array('email-src', 'email-dst', 'whois-registrant-email', 'dns-soa-email', 'email-reply-to'))) {
$value = str_replace("@", "[at]", $value);
} elseif (in_array($attribute['type'], array('hostname', 'domain', 'ip-src', 'ip-dst', 'domain|ip'))) {
$value = str_replace(".", "[.]", $value);
}
$strRepeatCount = $appendlen - 2 - strlen($attribute['type']);
$strRepeat = ($strRepeatCount > 0) ? str_repeat(' ', $strRepeatCount) : '';
if (isset($oldpublish) && isset($attribute['timestamp']) && $attribute['timestamp'] > $oldpublish) {
$line = '* ' . $indent . $attribute['category'] . '/' . $attribute['type'] . $strRepeat . ': ' . $value . $ids . " *\n";
} else {
$line = $indent . $attribute['category'] . '/' . $attribute['type'] . $strRepeat . ': ' . $value . $ids . "\n";
}
if (!empty($attribute['AttributeTag'])) {
$tags = [];
foreach ($attribute['AttributeTag'] as $aT) {
$tags[] = $aT['Tag']['name'];
}
$line .= ' - Tags: ' . implode(', ', $tags) . "\n";
}
if ('other' == $attribute['type']) { // append the 'other' attribute types to the bottom.
$bodyTempOther .= $line;
} else {
$body .= $line;
}
}
}
/**
* @param array $event
* @param array $user
* @param int|null $oldpublish Timestamp of latest publish
* @return string
* @param int|null $oldpublish Timestamp of previous publishing.
* @param bool $contactAlert
* @return CakeEmailBody
* @throws CakeException
*/
private function __buildAlertEmailBody(array $event, array $user, $oldpublish)
private function renderAlertEmail(array $event, array $user, $oldpublish = null, $contactAlert = false)
{
// The mail body, h() is NOT needed as we are sending plain-text mails.
$body = "";
$body .= '==============================================' . "\n";
$body .= 'URL : ' . $this->__getAnnounceBaseurl() . '/events/view/' . $event['Event']['id'] . "\n";
$body .= 'Event ID : ' . $event['Event']['id'] . "\n";
$body .= 'Date : ' . $event['Event']['date'] . "\n";
if (Configure::read('MISP.showorg')) {
$body .= 'Reported by : ' . $event['Orgc']['name'] . "\n";
$body .= 'Local owner of the event : ' . $event['Org']['name'] . "\n";
$View = new View();
$View->helpers = ['TextColour'];
$View->loadHelpers();
$View->set('event', $event);
$View->set('user', $user);
$View->set('oldPublishTimestamp', $oldpublish);
$View->set('baseurl', $this->__getAnnounceBaseurl());
$View->set('distributionLevels', $this->distributionLevels);
$View->set('analysisLevels', $this->analysisLevels);
$View->set('contactAlert', $contactAlert);
try {
$View->viewPath = $View->layoutPath = 'Emails' . DS . 'html';
$html = $View->render('alert');
} catch (MissingViewException $e) {
$html = null; // HTMl template is optional
}
$body .= 'Distribution: ' . $this->distributionLevels[$event['Event']['distribution']] . "\n";
if ($event['Event']['distribution'] == 4) {
$body .= 'Sharing Group: ' . $event['SharingGroup']['name'] . "\n";
}
$tags = [];
foreach ($event['EventTag'] as $tag) {
$tags[] = $tag['Tag']['name'];
}
$body .= 'Tags: ' . implode(', ', $tags) . "\n";
$body .= 'Threat Level: ' . $event['ThreatLevel']['name'] . "\n";
$body .= 'Analysis : ' . $this->analysisLevels[$event['Event']['analysis']] . "\n";
$body .= 'Description : ' . $event['Event']['info'] . "\n";
if (!empty($event['RelatedEvent'])) {
$body .= '==============================================' . "\n";
$body .= 'Related to: '. "\n";
foreach ($event['RelatedEvent'] as $relatedEvent) {
$body .= $this->__getAnnounceBaseurl() . '/events/view/' . $relatedEvent['Event']['id'] . ' (' . $relatedEvent['Event']['date'] . ') ' ."\n";
}
$body .= '==============================================' . "\n";
}
$bodyTempOther = "";
if (!empty($event['Attribute'])) {
$body .= 'Attributes (* indicates a new or modified attribute):' . "\n";
$this->__buildAlertEmailAttribute($body, $bodyTempOther, $event['Attribute'], $oldpublish);
}
if (!empty($event['Object'])) {
$body .= 'Objects (* indicates a new or modified object):' . "\n";
$this->__buildAlertEmailObject($body, $bodyTempOther, $event['Object'], $oldpublish);
}
if (!empty($bodyTempOther)) {
$body .= "\n";
}
$body .= $bodyTempOther; // append the 'other' attribute types to the bottom.
$body .= '==============================================' . "\n";
$body .= sprintf(
"You receive this e-mail because the e-mail address %s is set to receive publish alerts on the MISP instance at %s.%s%s",
$user['email'],
$this->__getAnnounceBaseurl(),
PHP_EOL,
PHP_EOL
);
$body .= "If you would like to unsubscribe from receiving such alert e-mails, simply\ndisable publish alerts via " . $this->__getAnnounceBaseurl() . '/users/edit' . PHP_EOL;
$body .= '==============================================' . "\n";
return $body;
$View->hasRendered = false;
$View->viewPath = $View->layoutPath = 'Emails' . DS . 'text';
$text = $View->render('alert');
require_once APP . '/Lib/Tools/SendEmail.php'; // TODO
return new CakeEmailBody($text, $html);
}
/**
@ -3423,13 +3328,13 @@ class Event extends AppModel
$subject = "[" . Configure::read('MISP.org') . " MISP] Need info about event $id - " . strtoupper($tplColorString);
$result = true;
foreach ($orgMembers as $reporter) {
list($bodyevent, $body) = $this->__buildContactEventEmailBody($user, $message, $event);
list($bodyevent, $body) = $this->__buildContactEventEmailBody($user, $reporter, $message, $event);
$result = $this->User->sendEmail($reporter, $bodyevent, $body, $subject, $user) && $result;
}
return $result;
}
private function __buildContactEventEmailBody(array $user, $message, array $event)
private function __buildContactEventEmailBody(array $user, array $reporter, $message, array $event)
{
// The mail body, h() is NOT needed as we are sending plain-text mails.
$body = "";
@ -3451,40 +3356,13 @@ class Event extends AppModel
$body .= "\n";
$body .= "The event is the following: \n";
// print the event in mail-format
// LATER place event-to-email-layout in a function
$body .= 'URL : ' . $this->__getAnnounceBaseurl() . '/events/view/' . $event['Event']['id'] . "\n";
$bodyevent = $body;
$bodyevent .= 'Event ID : ' . $event['Event']['id'] . "\n";
$bodyevent .= 'Date : ' . $event['Event']['date'] . "\n";
if (Configure::read('MISP.showorg')) {
$bodyevent .= 'Reported by : ' . $event['Orgc']['name'] . "\n";
}
$bodyevent .= 'Risk : ' . $event['ThreatLevel']['name'] . "\n";
$bodyevent .= 'Analysis : ' . $this->analysisLevels[$event['Event']['analysis']] . "\n";
$full = $body;
$body .= $this->__getAnnounceBaseurl() . '/events/view/' . $event['Event']['id'] . "\n";
foreach ($event['RelatedEvent'] as $relatedEvent) {
$bodyevent .= 'Related to : ' . $this->__getAnnounceBaseurl() . '/events/view/' . $relatedEvent['Event']['id'] . ' (' . $relatedEvent['Event']['date'] . ')' . "\n";
}
$rendered = $this->renderAlertEmail($event, $reporter);
$full .= $rendered->text;
$bodyevent .= 'Info : ' . "\n";
$bodyevent .= $event['Event']['info'] . "\n";
$bodyTempOther = "";
if (!empty($event['Attribute'])) {
$bodyevent .= 'Attributes:' . "\n";
$this->__buildAlertEmailAttribute($bodyevent, $bodyTempOther, $event['Attribute'], null);
}
if (!empty($event['Object'])) {
$bodyevent .= 'Objects:' . "\n";
$this->__buildAlertEmailObject($bodyevent, $bodyTempOther, $event['Object'], null);
}
if (!empty($bodyTempOther)) {
$bodyevent .= "\n";
}
$bodyevent .= $bodyTempOther; // append the 'other' attribute types to the bottom.
return array($bodyevent, $body);
return array($body, $full);
}
public function captureSGForElement($element, $user, $server=false)

View File

@ -4739,6 +4739,14 @@ class Server extends AppModel
'test' => 'testBool',
'type' => 'boolean',
),
'email_from_name' => [
'level' => 2,
'description' => __('Notification e-mail sender name.'),
'value' => '',
'errorMessage' => '',
'test' => 'testForEmpty',
'type' => 'string',
],
'taxii_sync' => array(
'level' => 3,
'description' => __('This setting is deprecated and can be safely removed.'),

View File

@ -757,7 +757,7 @@ class User extends AppModel
'conditions' => $conditions,
'recursive' => -1,
'fields' => array('id', 'email', 'gpgkey', 'certif_public', 'org_id', 'disabled'),
'contain' => ['Role' => ['fields' => ['perm_site_admin', 'perm_audit']], 'Organisation' => ['fields' => ['id']]],
'contain' => ['Role' => ['fields' => ['perm_site_admin', 'perm_audit']], 'Organisation' => ['fields' => ['id', 'name']]],
));
foreach ($users as $k => $user) {
$user = $user['User'];
@ -786,8 +786,8 @@ class User extends AppModel
* the remaining two parameters are the e-mail subject and a secondary user object which will be used as the replyto address if set. If it is set and an encryption key for the replyTo user exists, then his/her public key will also be attached
*
* @param array $user
* @param string $body
* @param string|false $bodyNoEnc
* @param CakeEmailBody|string $body
* @param CakeEmailBody|string|false $bodyNoEnc
* @param string $subject
* @param array|false $replyToUser
* @return bool
@ -810,8 +810,8 @@ class User extends AppModel
$encrypted = $sendEmail->sendToUser(
$user,
$subject,
new CakeEmailBody($body),
$bodyNoEnc ? new CakeEmailBody($bodyNoEnc): null,
is_string($body) ? new CakeEmailBody($body) : $body,
$bodyNoEnc ? (is_string($bodyNoEnc) ? new CakeEmailBody($bodyNoEnc) : $bodyNoEnc): null,
$replyToUser ?: []
);

View File

@ -0,0 +1,101 @@
<?php
$renderAttributes = function(array $attributes, $indent = ' ') use ($oldPublishTimestamp) {
$appendlen = 20;
foreach ($attributes as $attribute) {
$ids = $attribute['to_ids'] ? ' (IDS)' : '';
// Defanging URLs (Not "links") emails domains/ips in notification emails
$value = $attribute['value'];
if ('url' === $attribute['type'] || 'uri' === $attribute['type']) {
$value = str_ireplace("http", "hxxp", $value);
$value = str_ireplace(".", "[.]", $value);
} elseif (in_array($attribute['type'], ['email-src', 'email-dst', 'whois-registrant-email', 'dns-soa-email', 'email-reply-to'], true)) {
$value = str_replace("@", "[at]", $value);
} elseif (in_array($attribute['type'], ['hostname', 'domain', 'ip-src', 'ip-dst', 'domain|ip'], true)) {
$value = str_replace(".", "[.]", $value);
}
$strRepeatCount = $appendlen - 2 - strlen($attribute['type']);
$strRepeat = ($strRepeatCount > 0) ? str_repeat(' ', $strRepeatCount) : '';
if (isset($oldPublishTimestamp) && isset($attribute['timestamp']) && $attribute['timestamp'] > $oldPublishTimestamp) {
$line = '* ' . $indent . $attribute['category'] . '/' . $attribute['type'] . $strRepeat . ': ' . $value . $ids . " *\n";
} else {
$line = $indent . $attribute['category'] . '/' . $attribute['type'] . $strRepeat . ': ' . $value . $ids . "\n";
}
if (!empty($attribute['AttributeTag'])) {
$tags = [];
foreach ($attribute['AttributeTag'] as $aT) {
$tags[] = $aT['Tag']['name'];
}
$line .= ' - Tags: ' . implode(', ', $tags) . "\n";
}
echo $line;
}
};
$renderObjects = function(array $objects) use ($renderAttributes, $oldPublishTimestamp) {
foreach ($objects as $object) {
$body = '';
if (isset($oldPublishTimestamp) && isset($object['timestamp']) && $object['timestamp'] > $oldPublishTimestamp) {
$body .= '* ';
} else {
$body .= ' ';
}
$body .= $object['name'] . '/' . $object['meta-category'] . "\n";
if (!empty($object['Attribute'])) {
$body .= $renderAttributes($object['Attribute'], ' ');
}
echo $body;
}
};
$tags = [];
foreach ($event['EventTag'] as $tag) {
$tags[] = $tag['Tag']['name'];
}
?>
==============================================
URL : <?= $baseurl ?>/events/view/<?= $event['Event']['id'] . PHP_EOL ?>
Event ID : <?= $event['Event']['id'] . PHP_EOL ?>
Date : <?= $event['Event']['date'] . PHP_EOL ?>
<?php if (Configure::read('MISP.showorg')): ?>
Reported by : <?= $event['Orgc']['name'] . PHP_EOL ?>
Local owner of the event : <?= $event['Org']['name'] . PHP_EOL ?>
<?php endif; ?>
Distribution: <?= $distributionLevels[$event['Event']['distribution']] . PHP_EOL ?>
<?php if ($event['Event']['distribution'] == 4): ?>
Sharing Group: <?= $event['SharingGroup']['name'] . PHP_EOL ?>
<?php endif; ?>
Tags: <?= implode(", ", $tags) . PHP_EOL ?>
Threat Level: <?= $event['ThreatLevel']['name'] . PHP_EOL ?>
Analysis : <?= $analysisLevels[$event['Event']['analysis']] . PHP_EOL ?>
Description : <?= $event['Event']['info'] . PHP_EOL ?>
<?php if (!empty($event['RelatedEvent'])): ?>
==============================================
Related to:
<?php
foreach ($event['RelatedEvent'] as $relatedEvent) {
echo $baseurl . '/events/view/' . $relatedEvent['Event']['id'] . ' (' . $relatedEvent['Event']['date'] . ') ' . "\n";
}
?>
==============================================
<?php endif; ?>
<?php if (!empty($event['Attribute'])): ?>
Attributes<?= isset($oldPublishTimestamp) ? ' (* indicates a new or modified attribute since last update):' : ':' ?>
<?= $renderAttributes($event['Attribute']) ?>
<?php endif; ?>
<?php if (!empty($event['Object'])): ?>
Objects<?= isset($oldPublishTimestamp) ? ' (* indicates a new or modified object since last update):' : ':' ?>
<?= $renderObjects($event['Object']) ?>
<?php endif; ?>
==============================================
You receive this e-mail because the e-mail address <?= $user['email'] ?> is set
to receive <?= $contactAlert ? 'contact' : 'publish' ?> alerts on the MISP instance at <?= $baseurl ?>.
If you would like to unsubscribe from receiving such alert e-mails, simply
disable <?= $contactAlert ? 'contact' : 'publish' ?> alerts via <?= $baseurl ?>/users/edit
==============================================