From dfe7b27e8e2448efefc19865aefe76c311ef3ea6 Mon Sep 17 00:00:00 2001 From: Megachip Date: Fri, 24 Jul 2026 15:44:30 +0200 Subject: [PATCH 01/13] Add Location class to document_types #BUG On location view, I can assign documents to a location, on document view there is no option to choose a location --- src/autoload/CFG_GLPI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autoload/CFG_GLPI.php b/src/autoload/CFG_GLPI.php index ba4c2f268fab..3f873c721df0 100644 --- a/src/autoload/CFG_GLPI.php +++ b/src/autoload/CFG_GLPI.php @@ -230,7 +230,7 @@ SoftwareLicense::class, Supplier::class, Ticket::class, User::class, Certificate::class, Cluster::class, ITILFollowup::class, ITILSolution::class, ChangeTask::class, ProblemTask::class, TicketTask::class, Appliance::class, - DatabaseInstance::class, Rack::class, + DatabaseInstance::class, Rack::class, Location::class, ]; $CFG_GLPI['consumables_types'] = [Group::class, User::class]; From bcd817b2656ec5a32b7ef89ede846ff9c215425f Mon Sep 17 00:00:00 2001 From: Megachip Date: Thu, 20 Aug 2026 20:02:26 +0200 Subject: [PATCH 02/13] Fix regex pattern for ticket matching in MailCollector Fix: mail collector attaches reply to the wrong ticket when subject holds a foreign reference --- src/MailCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MailCollector.php b/src/MailCollector.php index 25569bd38790..0b9f3b19fc49 100644 --- a/src/MailCollector.php +++ b/src/MailCollector.php @@ -2191,7 +2191,7 @@ public function getItemFromHeaders(Message $message): ?CommonDBTM $ticket = new Ticket(); if ( - preg_match('/\[.+#(\d+)\]/', $subject, $matches) === 1 + preg_match('/\[[^\]]*\s#(\d+)\]/', $subject, $matches) === 1 && $ticket->getFromDB($matches[1]) ) { return $ticket; From 2e9a6db1192e449756699e9ef0d39fdbe3033fe6 Mon Sep 17 00:00:00 2001 From: Megachip Date: Thu, 20 Aug 2026 20:08:42 +0200 Subject: [PATCH 03/13] Add test case for GLPI tag in email subject Added a test case for GLPI subject fallback handling. --- tests/imap/MailCollectorTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/imap/MailCollectorTest.php b/tests/imap/MailCollectorTest.php index 4ac8789b5046..aa90f728f9b1 100644 --- a/tests/imap/MailCollectorTest.php +++ b/tests/imap/MailCollectorTest.php @@ -628,6 +628,15 @@ public static function itemReferenceHeaderProvider() 'expected_items_id' => null, 'accepted' => false, ], + // Subject fallback - GLPI tag found, ignoring a foreign `[Ticket#...]` reference + [ + 'headers' => [ + 'subject' => "Re: [GLPI #{$ticket_id}] [Ticket#2026072803024161] Foo", + ], + 'expected_itemtype' => Ticket::class, + 'expected_items_id' => $ticket_id, + 'accepted' => true, + ], ]; } From 317e3d167c9847e6973b677aea25f65197f62d1e Mon Sep 17 00:00:00 2001 From: Megachip Date: Fri, 21 Aug 2026 16:15:31 +0200 Subject: [PATCH 04/13] Update tests/imap/MailCollectorTest.php Co-authored-by: Romain B. <8530352+Rom1-B@users.noreply.github.com> --- tests/imap/MailCollectorTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/imap/MailCollectorTest.php b/tests/imap/MailCollectorTest.php index aa90f728f9b1..86d16372788d 100644 --- a/tests/imap/MailCollectorTest.php +++ b/tests/imap/MailCollectorTest.php @@ -637,6 +637,15 @@ public static function itemReferenceHeaderProvider() 'expected_items_id' => $ticket_id, 'accepted' => true, ], + // Subject fallback - GLPI tag found, ignoring a foreign `[Case #xxxx]` reference placed before it + [ + 'headers' => [ + 'subject' => "[Case #4711] Re: [GLPI #{$ticket_id}] Foo", + ], + 'expected_itemtype' => Ticket::class, + 'expected_items_id' => $ticket_id, + 'accepted' => true, + ], ]; } From 7ba728d9e4f6754c20da5dc09f10033fe3aaa4f2 Mon Sep 17 00:00:00 2001 From: Megachip Date: Fri, 21 Aug 2026 16:38:05 +0200 Subject: [PATCH 05/13] Refactor subject pattern matching in MailCollector Match ticket id against configured notification subject tags. --- src/MailCollector.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/MailCollector.php b/src/MailCollector.php index 0b9f3b19fc49..64ab677abf71 100644 --- a/src/MailCollector.php +++ b/src/MailCollector.php @@ -2189,9 +2189,20 @@ public function getItemFromHeaders(Message $message): ?CommonDBTM $subject = $message->getHeader('subject')->getFieldValue(); $matches = []; + global $DB; $ticket = new Ticket(); + $tags = array_unique(array_merge(['GLPI'], array_column( + iterator_to_array($DB->request([ + 'SELECT' => 'notification_subject_tag', + 'DISTINCT' => true, + 'FROM' => Entity::getTable(), + 'WHERE' => ['notification_subject_tag' => ['<>', '']], + ])), + 'notification_subject_tag' + ))); + $pattern = '/\[(?:' . implode('|', array_map(static fn($tag) => preg_quote($tag, '/'), $tags)) . ')\s#(\d+)\]/'; if ( - preg_match('/\[[^\]]*\s#(\d+)\]/', $subject, $matches) === 1 + preg_match($pattern, $subject, $matches) === 1 && $ticket->getFromDB($matches[1]) ) { return $ticket; From 43ba1ebf4aaa91c40b23476aee57a283a6de79d9 Mon Sep 17 00:00:00 2001 From: Megachip Date: Wed, 9 Sep 2026 08:48:28 +0200 Subject: [PATCH 06/13] Enhance notification subject tag processing Refactor notification subject handling to improve tag matching and retrieval logic. --- src/MailCollector.php | 82 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/src/MailCollector.php b/src/MailCollector.php index 64ab677abf71..3dfbcb5de9fb 100644 --- a/src/MailCollector.php +++ b/src/MailCollector.php @@ -77,6 +77,11 @@ class MailCollector extends CommonDBTM * IMAP / POP connection */ private ?AbstractStorage $storage = null; + /** + * Notification subject tags used across entities, memoized for the current run. + * @var ?string[] + */ + private ?array $notification_subject_tags = null; /** * UID of the current message * @var int @@ -2187,31 +2192,74 @@ public function getItemFromHeaders(Message $message): ?CommonDBTM // Check in subject if ($message->getHeaders()->has('subject')) { $subject = $message->getHeader('subject')->getFieldValue(); - $matches = []; - global $DB; $ticket = new Ticket(); - $tags = array_unique(array_merge(['GLPI'], array_column( - iterator_to_array($DB->request([ - 'SELECT' => 'notification_subject_tag', - 'DISTINCT' => true, - 'FROM' => Entity::getTable(), - 'WHERE' => ['notification_subject_tag' => ['<>', '']], - ])), - 'notification_subject_tag' - ))); - $pattern = '/\[(?:' . implode('|', array_map(static fn($tag) => preg_quote($tag, '/'), $tags)) . ')\s#(\d+)\]/'; - if ( - preg_match($pattern, $subject, $matches) === 1 - && $ticket->getFromDB($matches[1]) - ) { - return $ticket; + + // GLPI prefixes ITIL notification subjects with `[ #]`, where the + // id is zero-padded to at least 7 digits and always preceded by a space + // (see NotificationTargetCommonITILObject::getSubjectPrefix()). Requiring + // that shape prevents foreign references such as `[Ticket#123]` from being + // mistaken for a ticket id. + if (preg_match_all('/\[([^\]]*)\s#(\d{7,})\]/', $subject, $matches, PREG_SET_ORDER) > 0) { + $candidate = null; + if (count($matches) === 1) { + // Single match: trust it, even if the entity tag has been edited + // since the notification was sent. + $candidate = $matches[0][2]; + } else { + // Multiple matches: prefer the one whose prefix matches a known + // notification subject tag, and fall back to the last match. + $known_tags = $this->getNotificationSubjectTags(); + foreach ($matches as $match) { + if (in_array(trim($match[1]), $known_tags, true)) { + $candidate = $match[2]; + break; + } + } + if ($candidate === null) { + $last = end($matches); + $candidate = $last[2]; + } + } + + if ($candidate !== null && $ticket->getFromDB($candidate)) { + return $ticket; + } } } return null; } + /** + * Get the notification subject tags (`[ #]`) configured across the + * entities, plus the default `GLPI` tag. Computed once per collector instance. + * + * @return string[] + */ + private function getNotificationSubjectTags(): array + { + if ($this->notification_subject_tags === null) { + /** @var \DBmysql $DB */ + global $DB; + + $tags = ['GLPI']; + $iterator = $DB->request([ + 'SELECT' => 'notification_subject_tag', + 'DISTINCT' => true, + 'FROM' => Entity::getTable(), + 'WHERE' => ['notification_subject_tag' => ['<>', '']], + ]); + foreach ($iterator as $row) { + $tags[] = trim((string) $row['notification_subject_tag']); + } + + $this->notification_subject_tags = array_values(array_unique(array_filter($tags))); + } + + return $this->notification_subject_tags; + } + /** * Retrieve the message ID from headers. * If multiple matching headers are found, the first one parsed as a {@link MessageId} is returned. From 02a748aee70dea59b94f6a34271d91a1a1a7f28d Mon Sep 17 00:00:00 2001 From: Megachip Date: Wed, 9 Sep 2026 08:51:01 +0200 Subject: [PATCH 07/13] Update ticket ID formatting in MailCollectorTest --- tests/imap/MailCollectorTest.php | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/imap/MailCollectorTest.php b/tests/imap/MailCollectorTest.php index 86d16372788d..db1e657d9db6 100644 --- a/tests/imap/MailCollectorTest.php +++ b/tests/imap/MailCollectorTest.php @@ -415,6 +415,7 @@ public static function itemReferenceHeaderProvider() $root_ent_id = getItemByTypeName('Entity', '_test_root_entity', true); $ticket_id = getItemByTypeName('Ticket', '_ticket01', true); + $padded_ticket_id = sprintf('%07d', $ticket_id); $ticket_notif = new NotificationTargetTicket($root_ent_id, 'test_event', getItemByTypeName('Ticket', '_ticket01')); $soft_id = getItemByTypeName('SoftwareLicense', '_test_softlic_1', true); @@ -628,19 +629,37 @@ public static function itemReferenceHeaderProvider() 'expected_items_id' => null, 'accepted' => false, ], - // Subject fallback - GLPI tag found, ignoring a foreign `[Ticket#...]` reference + // Subject fallback - single GLPI tag, foreign `[Ticket#...]` (no space) ignored [ 'headers' => [ - 'subject' => "Re: [GLPI #{$ticket_id}] [Ticket#2026072803024161] Foo", + 'subject' => "Re: [GLPI #{$padded_ticket_id}] [Ticket#2026072803024161] Foo", ], 'expected_itemtype' => Ticket::class, 'expected_items_id' => $ticket_id, 'accepted' => true, ], - // Subject fallback - GLPI tag found, ignoring a foreign `[Case #xxxx]` reference placed before it + // Subject fallback - a too-short foreign `[Case #4711]` reference is ignored (< 7 digits) [ 'headers' => [ - 'subject' => "[Case #4711] Re: [GLPI #{$ticket_id}] Foo", + 'subject' => "[Case #4711] Re: [GLPI #{$padded_ticket_id}] Foo", + ], + 'expected_itemtype' => Ticket::class, + 'expected_items_id' => $ticket_id, + 'accepted' => true, + ], + // Subject fallback - multiple matches: the configured tag (GLPI) wins over a foreign one + [ + 'headers' => [ + 'subject' => "[Foreign #1234567] Re: [GLPI #{$padded_ticket_id}] Foo", + ], + 'expected_itemtype' => Ticket::class, + 'expected_items_id' => $ticket_id, + 'accepted' => true, + ], + // Subject fallback - multiple matches, none with a known tag: falls back to the last one + [ + 'headers' => [ + 'subject' => "[Foreign #7777777] Re: [Unknown #{$padded_ticket_id}] Foo", ], 'expected_itemtype' => Ticket::class, 'expected_items_id' => $ticket_id, From 2ccf51ff54eb7cada851d6b4577ac88560ee4c97 Mon Sep 17 00:00:00 2001 From: Megachip Date: Thu, 10 Sep 2026 13:32:45 +0200 Subject: [PATCH 08/13] Improve candidate selection for notification tags Refactor candidate selection logic for notification tags. --- src/MailCollector.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/MailCollector.php b/src/MailCollector.php index f93a22cb3ab5..a0d27446a255 100644 --- a/src/MailCollector.php +++ b/src/MailCollector.php @@ -2280,15 +2280,13 @@ public function getItemFromHeaders(Message $message): ?CommonDBTM // notification subject tag, and fall back to the last match. $known_tags = $this->getNotificationSubjectTags(); foreach ($matches as $match) { + // Keep the last seen id as fallback, but stop as soon as a + // known notification subject tag is found. + $candidate = $match[2]; if (in_array(trim($match[1]), $known_tags, true)) { - $candidate = $match[2]; break; } } - if ($candidate === null) { - $last = end($matches); - $candidate = $last[2]; - } } if ($candidate !== null && $ticket->getFromDB($candidate)) { From 85073511ad7c31c95a09e7ea0f63429cb4929f27 Mon Sep 17 00:00:00 2001 From: Megachip Date: Thu, 10 Sep 2026 13:35:47 +0200 Subject: [PATCH 09/13] Clean up ignored errors in phpstan baseline Removed an ignored error related to offset access on an array. --- .phpstan-baseline.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.phpstan-baseline.php b/.phpstan-baseline.php index a0cc591a7644..b55a0c1afe1e 100644 --- a/.phpstan-baseline.php +++ b/.phpstan-baseline.php @@ -14671,12 +14671,6 @@ 'count' => 1, 'path' => __DIR__ . '/src/MailCollector.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Offset 1 might not exist on array\\{\\}\\|array\\{non\\-falsy\\-string, numeric\\-string\\}\\.$#', - 'identifier' => 'offsetAccess.notFound', - 'count' => 1, - 'path' => __DIR__ . '/src/MailCollector.php', -]; $ignoreErrors[] = [ 'message' => '#^Parameter \\#1 \\$datetime of function Safe\\\\strtotime expects string, array\\|ArrayIterator\\|Laminas\\\\Mail\\\\Header\\\\HeaderInterface\\|string given\\.$#', 'identifier' => 'argument.type', From 2fc57c82de312b2032128e5df6d8ac3e49bd409f Mon Sep 17 00:00:00 2001 From: Megachip Date: Thu, 10 Sep 2026 13:43:44 +0200 Subject: [PATCH 10/13] Add preg_match_all function import --- src/MailCollector.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MailCollector.php b/src/MailCollector.php index a0d27446a255..bfbbb57160cf 100644 --- a/src/MailCollector.php +++ b/src/MailCollector.php @@ -57,6 +57,7 @@ use function Safe\iconv; use function Safe\mb_convert_encoding; use function Safe\preg_match; +use function Safe\preg_match_all; use function Safe\preg_replace; use function Safe\strtotime; From 47a45174a3a89fdc5ad6894e43becef730e8eac6 Mon Sep 17 00:00:00 2001 From: Megachip Date: Fri, 18 Sep 2026 15:25:22 +0200 Subject: [PATCH 11/13] Zero-pad ticket ID in email subject Update the email subject to include zero-padded ticket ID. --- tests/functional/TicketTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/functional/TicketTest.php b/tests/functional/TicketTest.php index 622345f9ca7a..899dfe4be521 100644 --- a/tests/functional/TicketTest.php +++ b/tests/functional/TicketTest.php @@ -7483,12 +7483,14 @@ public function testMailCollectorFollowupSetAssignee(string $from_user, int $set '_skip_auto_assign' => true, ]); $ticket_id = $ticket->getID(); + // GLPI zero-pads the ticket id to at least 7 digits in notification subjects. + $padded_ticket_id = sprintf('%07d', $ticket_id); // Build a raw email from the sender replying to the ticket (linked via subject line) $raw = implode("\r\n", [ "From: {$from_user} <{$sender_email}>", "To: helpdesk@glpi.com", - "Subject: Re: [GLPI #{$ticket_id}]", + "Subject: Re: [GLPI #{$padded_ticket_id}]", "Message-ID: ", "Date: Mon, 01 Jan 2024 12:00:00 +0000", "", From 996124cf9798a4fb583c5a5e0fe39255d342d874 Mon Sep 17 00:00:00 2001 From: Megachip Date: Fri, 18 Sep 2026 15:26:15 +0200 Subject: [PATCH 12/13] Fix PHPDoc type hint for DB variable --- src/MailCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MailCollector.php b/src/MailCollector.php index bfbbb57160cf..e8f5d6fa38d1 100644 --- a/src/MailCollector.php +++ b/src/MailCollector.php @@ -2308,7 +2308,7 @@ public function getItemFromHeaders(Message $message): ?CommonDBTM private function getNotificationSubjectTags(): array { if ($this->notification_subject_tags === null) { - /** @var \DBmysql $DB */ + /** @var DBmysql $DB */ global $DB; $tags = ['GLPI']; From 8f7379803bdb0a4cedaa7e03bc723edef211abaf Mon Sep 17 00:00:00 2001 From: Megachip Date: Fri, 18 Sep 2026 15:27:37 +0200 Subject: [PATCH 13/13] Fix comment formatting in MailCollectorTest.php --- tests/imap/MailCollectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/imap/MailCollectorTest.php b/tests/imap/MailCollectorTest.php index 405c4cb85f10..411d7f9d2610 100644 --- a/tests/imap/MailCollectorTest.php +++ b/tests/imap/MailCollectorTest.php @@ -634,7 +634,7 @@ public static function itemReferenceHeaderProvider() 'expected_items_id' => null, 'accepted' => false, ], - // Subject fallback - single GLPI tag, foreign `[Ticket#...]` (no space) ignored + // Subject fallback - single GLPI tag, foreign `[Ticket#...]` (no space) ignored [ 'headers' => [ 'subject' => "Re: [GLPI #{$padded_ticket_id}] [Ticket#2026072803024161] Foo",