Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NotificationMailling): add 'References' header for mail grouping (Gmail) #14296

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The present file will list all changes made to the project; according to the
### API changes

#### Added
- `CommonDBTM::getMessageReferenceEvent()` method that can be overridden to tweak notifications grouping in mail clients.

#### Changes

Expand Down
46 changes: 46 additions & 0 deletions install/migrations/update_10.0.6_to_10.0.7/queuednotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2023 Teclib' and contributors.
* @copyright 2003-2014 by the INDEPNET Development Team.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

/**
* @var DB $DB
* @var Migration $migration
*/

$default_key_sign = DBConnection::getDefaultPrimaryKeySignOption();

/* Add `event` to some glpi_queuednotifications */
if (!$DB->fieldExists('glpi_queuednotifications', 'event')) {
$migration->addField('glpi_queuednotifications', 'event', 'varchar(255) DEFAULT NULL', ['value' => null]);
}
1 change: 1 addition & 0 deletions install/mysql/glpi-empty.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6076,6 +6076,7 @@ CREATE TABLE `glpi_queuednotifications` (
`messageid` text,
`documents` text,
`mode` varchar(20) NOT NULL COMMENT 'See Notification_NotificationTemplate::MODE_* constants',
`event` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `item` (`itemtype`,`items_id`,`notificationtemplates_id`),
KEY `is_deleted` (`is_deleted`),
Expand Down
25 changes: 25 additions & 0 deletions src/CommonDBTM.php
Original file line number Diff line number Diff line change
Expand Up @@ -6579,4 +6579,29 @@ public function isGlobal(): bool

return $is_global;
}

/**
* Return reference event name for given event.
*
* @param string $event
*
* @since 10.0.7
*/
public static function getMessageReferenceEvent(string $event): ?string
{
switch ($event) {
case 'new':
case 'update':
case 'delete':
case 'user_mention':
// Add the CRUD actions and the `user_mention` notifications to thread instanciated by `new` event
$reference_event = 'new';
break;
default:
// Other actions should have their own thread
$reference_event = null;
break;
}
return $reference_event;
}
}
6 changes: 6 additions & 0 deletions src/CommonITILObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -9416,4 +9416,10 @@ public function prepareInputForClone($input)
unset($input['actiontime']);
return $input;
}

public static function getMessageReferenceEvent(string $event): ?string
{
// All actions should be attached to thread instanciated by `new` event
return 'new';
}
}
118 changes: 80 additions & 38 deletions src/MailCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2128,16 +2128,15 @@ public function getItemFromHeaders(Message $message): ?CommonDBTM
return null;
}

$pattern = $this->getMessageIdExtractPattern();

foreach (['in_reply_to', 'references'] as $header_name) {
$matches = [];
if (
$message->getHeaders()->has($header_name)
&& preg_match($pattern, $message->getHeader($header_name)->getFieldValue(), $matches)
) {
$itemtype = $matches['itemtype'] ?? '';
$items_id = $matches['items_id'] ?? '';
if (!$message->getHeaders()->has($header_name)) {
continue;
}

$matches = $this->extractValuesFromRefHeader($message->getHeader($header_name)->getFieldValue());
if ($matches !== null) {
$itemtype = $matches['itemtype'];
$items_id = $matches['items_id'];

// Handle old format MessageId where itemtype was not in header
if (empty($itemtype) && !empty($items_id)) {
Expand Down Expand Up @@ -2170,21 +2169,19 @@ public function getItemFromHeaders(Message $message): ?CommonDBTM
*/
public function isMessageSentByGlpi(Message $message): bool
{
$pattern = $this->getMessageIdExtractPattern();

if (!$message->getHeaders()->has('message-id')) {
// Messages sent by GLPI now have always a message-id header.
return false;
}

$message_id = $message->getHeader('message_id')->getFieldValue();
$matches = [];
if (!preg_match($pattern, $message_id, $matches)) {
$matches = $this->extractValuesFromRefHeader($message_id);
if ($matches === null) {
// message-id header does not match GLPI format.
return false;
}

$uuid = $matches['uuid'] ?? '';
$uuid = $matches['uuid'];
if (empty($uuid)) {
// message-id corresponds to old format, without uuid.
// We assume that in most environments this message have been sent by this instance of GLPI,
Expand All @@ -2208,16 +2205,14 @@ public function isMessageSentByGlpi(Message $message): bool
*/
public function isResponseToMessageSentByAnotherGlpi(Message $message): bool
{
$pattern = $this->getMessageIdExtractPattern();

$has_uuid_from_another_glpi = false;
$has_uuid_from_current_glpi = false;
foreach (['in-reply-to', 'references'] as $header_name) {
$matches = [];
if (
$message->getHeaders()->has($header_name)
&& preg_match($pattern, $message->getHeader($header_name)->getFieldValue(), $matches)
) {
if (!$message->getHeaders()->has($header_name)) {
continue;
}
$matches = $this->extractValuesFromRefHeader($message->getHeader($header_name)->getFieldValue());
if ($matches !== null) {
if (empty($matches['uuid'])) {
continue;
}
Expand All @@ -2236,28 +2231,75 @@ public function isResponseToMessageSentByAnotherGlpi(Message $message): bool
}

/**
* Get pattern that can be used to extract information from a GLPI MessageId (uuid, itemtype and items_id).
* Extract information from a `Message-Id` or `Reference` header.
* Headers mays contains `uuid`, `itemtype`, `items_id` and `event` values.
*
* @see NotificationTarget::getMessageID()
* @see NotificationTarget::getMessageIdForEvent()
*
* @return string
*/
private function getMessageIdExtractPattern(): string
private function extractValuesFromRefHeader(string $header): ?array
{
// old format for tickets: GLPI-{$items_id}.{$time}.{$rand}@{$uname}
// old format without related item: GLPI.{$time}.{$rand}@{$uname}
// old format with related item: GLPI-{$itemtype}-{$items_id}.{$time}.{$rand}@{$uname}
// new format without related item: GLPI_{$uuid}.{$time}.{$rand}@{$uname}
// new format with related item: GLPI_{$uuid}-{$itemtype}-{$items_id}.{$time}.{$rand}@{$uname}

return '/GLPI'
. '(_(?<uuid>[a-z0-9]+))?' // uuid was not be present in old format
. '(-(?<itemtype>[a-z]+))?' // itemtype is not present if notification is not related to any object and was not present in old format
. '(-(?<items_id>[0-9]+))?' // items_id is not present if notification is not related to any object
. '\.[0-9]+' // time()
. '\.[0-9]+' // rand()
. '@\w*' // uname
. '/i'; // insensitive
$defaults = [
'uuid' => null,
'itemtype' => null,
'items_id' => null,
'event' => null,
];

$values = [];

// Message-Id generated in GLPI >= 10.0.7
// - without related item: GLPI_{$uuid}/{$event}.{$time}.{$rand}@{$uname}
// - with related item (reference event): GLPI_{$uuid}-{$itemtype}-{$items_id}/{$event}@{$uname}
// - with related item (other events): GLPI_{$uuid}-{$itemtype}-{$items_id}/{$event}.{$time}.{$rand}@{$uname}
$pattern = '/'
. 'GLPI'
. '_(?<uuid>[a-z0-9]+)' // uuid
. '(-(?<itemtype>[a-z]+)-(?<items_id>[0-9]+))?' // optional itemtype + items_id (only when related to an item)
. '\/(?<event>[a-z_]+)' // event
. '(\.[0-9]+\.[0-9]+)?' // optional time + rand (only when NOT related to an item OR when event is not the reference one)
. '@.+' // uname
. '/i';
if (preg_match($pattern, $header, $values) === 1) {
$values += $defaults;
return $values;
}

// Message-Id generated by GLPI >= 10.0.0 < 10.0.7
// - without related item: GLPI_{$uuid}.{$time}.{$rand}@{$uname}
// - with related item: GLPI_{$uuid}-{$itemtype}-{$items_id}.{$time}.{$rand}@{$uname}
$pattern = '/'
. 'GLPI'
. '_(?<uuid>[a-z0-9]+)' // uuid
. '(-(?<itemtype>[a-z]+)-(?<items_id>[0-9]+))?' // optionnal itemtype + items_id
. '\.[0-9]+' // time()
. '\.[0-9]+' // rand()
. '@.+' // uname
. '/i';
if (preg_match($pattern, $header, $values) === 1) {
$values += $defaults;
return $values;
}

// Message-Id generated by GLPI < 10.0.0
// - for tickets: GLPI-{$items_id}.{$time}.{$rand}@{$uname}
// - without related item: GLPI.{$time}.{$rand}@{$uname}
// - with related item: GLPI-{$itemtype}-{$items_id}.{$time}.{$rand}@{$uname}
$pattern = '/'
. 'GLPI'
. '(-(?<itemtype>[a-z]+))?' // optionnal itemtype
. '(-(?<items_id>[0-9]+))?' // optionnal items_id
. '\.[0-9]+' // time()
. '\.[0-9]+' // rand()
. '@.+' // uname
. '/i';
if (preg_match($pattern, $header, $values) === 1) {
$values += $defaults;
return $values;
}

return null;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/NotificationAjax.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public static function testNotification()
'fromname' => 'TEST',
'subject' => 'Test notification',
'content_text' => "Hello, this is a test notification.",
'to' => Session::getLoginUserID()
'to' => Session::getLoginUserID(),
'event' => 'test_notification'
]);
}

Expand All @@ -86,6 +87,8 @@ public function sendNotification($options = [])
$data['body_text'] = $options['content_text'];
$data['recipient'] = $options['to'];

$data['event'] = $options['event'] ?? null; // `event` has been added in GLPI 10.0.7

$data['mode'] = Notification_NotificationTemplate::MODE_AJAX;

$queue = new QueuedNotification();
Expand Down
1 change: 1 addition & 0 deletions src/NotificationEventAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public static function raise(
: 0;
$send_data['_entities_id'] = $entity;
$send_data['mode'] = $data['mode'];
$send_data['event'] = $event;

Notification::send($send_data);
} else {
Expand Down
30 changes: 15 additions & 15 deletions src/NotificationEventMailing.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,22 +131,22 @@ public static function send(array $data)
}
}

// Add custom header for mail grouping in reader
$mmail->AddCustomHeader(
str_replace(
[
'%uuid',
'%itemtype',
'%items_id'
],
[
Config::getUuid('notification'),
if (is_a($current->fields['itemtype'], CommonDBTM::class, true)) {
$reference_event = $current->fields['itemtype']::getMessageReferenceEvent($current->fields['event']);
if ($reference_event !== null && $reference_event !== $current->fields['event']) {
// Add `In-Reply-To` and `References` for mail grouping in reader when:
// - there is a reference event (i.e. we want to add current notification to a thread)
// - event is not the reference event (i.e. the thread has already be initiated).
// see https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.4
$email_ref = NotificationTarget::getMessageIdForEvent(
$current->fields['itemtype'],
$current->fields['items_id']
],
"In-Reply-To: <GLPI-%uuid-%itemtype-%items_id>"
)
);
$current->fields['items_id'],
$reference_event
);
$mmail->AddCustomHeader("In-Reply-To: <{$email_ref}>");
$mmail->AddCustomHeader("References: <{$email_ref}>");
}
}

$mmail->SetFrom($current->fields['sender'], $current->fields['sendername']);

Expand Down
2 changes: 2 additions & 0 deletions src/NotificationMailing.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ public function sendNotification($options = [])
$data['sender'] = $options['from'];
$data['sendername'] = $options['fromname'];

$data['event'] = $options['event'] ?? null; // `event` has been added in GLPI 10.0.7

if (isset($options['replyto']) && $options['replyto']) {
$data['replyto'] = $options['replyto'];
if (isset($options['replytoname'])) {
Expand Down
Loading