Skip to content
Open
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
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Changelog

## [4.1.0] — 2026-07-03

- Removed legacy `inc/config.class.php` and `inc/mailcollector.class.php`, fully superseded by `src/Config.php` and `src/MailCollectorWrapper.php`
- `src/Config.php` now actually renders `templates/pages/config.html.twig` via `TemplateRenderer` instead of duplicating the form as inline HTML
- Removed dead `Config::configUpdate()` (never called: the form posts to `front/save_config.php`, not through core's `Config::update()`/`config_class` flow)
- Added `locales/mailanalyzer.pot` translation template
- Translated remaining non-English code comments
- Rebased onto upstream master (4.0.1/4.0.2), carrying forward:
- `message_id` column widened from `VARCHAR(255)` to `VARCHAR(512)`
- Schema upgrade steps in `plugin_mailanalyzer_install()` now gated behind a stored `dbversion`, so they only run once instead of on every activation/update
- Fixed a version-bounds check in `plugin_mailanalyzer_check_prerequisites()` that could never trigger (`&&` of two mutually exclusive conditions, now `||`)
- Default `use_threadindex` config is now only seeded when unset, so upgrading no longer silently resets a saved value back to 0

## [4.0.0] — 2026-03-16

### GLPI 11 compatibility

#### Breaking changes
- **Minimum GLPI version raised to 11.0.0** (use 3.2.x for GLPI 10)
- PHP minimum raised to **8.1**

#### Architecture
- Classes migrated from `inc/` to `src/` with PSR-4 namespace `GlpiPlugin\Mailanalyzer`
- `inc/config.class.php` → `src/Config.php` (`GlpiPlugin\Mailanalyzer\Config`)
- `inc/mailcollector.class.php` → `src/MailCollectorWrapper.php` (`GlpiPlugin\Mailanalyzer\MailCollectorWrapper`)
- `hook.php` class `PluginMailAnalyzer` → `src/MailAnalyzer.php` (`GlpiPlugin\Mailanalyzer\MailAnalyzer`)
- Added `composer.json` with PSR-4 autoload configuration

#### setup.php
- Hook keys now use `Glpi\Plugin\Hooks` constants (`Hooks::PRE_ITEM_ADD`, `Hooks::ITEM_ADD`, etc.)
- Hook callbacks reference `MailAnalyzer::class` instead of bare strings
- Version range updated: `11.0.0` → `12.0.0`
- Removed broken `plugin_mailanalyzer_check_prerequisites()` compatibility check (used `Toolbox::addslashes_deep()` removed in GLPI 11)
- Added `Plugin::isPluginActive()` guard before registering hooks

#### hook.php
- Replaced `$DB->query()` / `or die()` with `Migration::addField()` and `Migration::addKey()` (raw SQL queries are prohibited in GLPI 11)
- Upgrade path preserved: detects and renames legacy `mailgate_id` → `mailcollectors_id`
- `plugin_mailanalyzer_uninstall()` remains intentionally empty (table preserved by design)

#### Config UI
- Replaced raw HTML (`echo "<table class='tab_cadre_fixe'>..."`) with `TemplateRenderer::getInstance()->display()`
- New Twig template: `templates/pages/config.html.twig`
- UI uses GLPI 11 native Tabler/Bootstrap 5 components: `card`, `form-switch`, `btn btn-primary`
- No custom CSS file — zero added assets
- CSRF token added via `{{ csrf_token() }}`

#### Hook callbacks (MailAnalyzer class)
- `plugin_pre_item_add_mailanalyzer` → `MailAnalyzer::preItemAdd(Ticket $item)`
- `plugin_item_add_mailanalyzer` → `MailAnalyzer::itemAdd(Ticket $item)`
- `plugin_item_purge_mailanalyzer` → `MailAnalyzer::itemPurge(Ticket $item)`
- `$DB->request('table', [...])` two-arg syntax → `$DB->request(['FROM' => ..., 'WHERE' => ...])` (two-arg deprecated in GLPI 11)
- Removed `Toolbox::addslashes_deep()` calls (auto-sanitization removed in GLPI 11; data is always raw)

#### front/config.form.php
- Fixed include path: uses `__DIR__` relative path instead of hardcoded `../../../inc/includes.php`
- `Session::setActiveTab` argument updated to match new namespaced class `GlpiPlugin\Mailanalyzer\Config$1`

---

## [3.2.2] — 2025-xx-xx

- Last release compatible with GLPI 10.0.18+

## [3.2.0] — GLPI 10.0.0+

## [2.1.0] — GLPI 9.5.x
114 changes: 97 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,97 @@
# mailanalyzer
Mail Analyzer GLPI Plugin aims at keeping track of email conversations.
It will combine emails from the same conversation into one Ticket.

It is currently tested with GLPI 0.83.8, 0.85.5, 0.90, 9.1, 9.2, 9.3, 9.4, 9.5, 10.0 and 11.0

Must be copied into *glpifolder*/plugins/mailanalyzer, or it can be downloaded from the GLPI plugin marketplace.

To be installed and enabled via the plugins configuration page in GLPI.

It creates a new table in the DB with the purpose of storing email guid (generated by email servers) in order to be able (if possible) to match emails in mailgate which have been sent using 'CC' and 'Reply to all'.
It cannot keeps track of forwarded emails and replies to them.

This solves the problem of duplicated Tickets when an email is sent to GLPI and CC (carbon copy) to others. And when those CC users use 'Reply to All', GLPI by default will create a new ticket. See: http://glpi.userecho.com/topic/1090667-new-rule-criteria-for-mail-receivers/


Please report any question/problem in the issue section.
# Mail Analyzer — GLPI Plugin

> Automatically combines CC/reply emails into a single Ticket instead of creating duplicates.

[![License: GPL v2+](https://img.shields.io/badge/License-GPLv2%2B-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)

---

## What it does

When an email is sent to GLPI **and** CC'd to other recipients, and those CC recipients use **Reply to All**, GLPI would normally create a new ticket for each reply. This plugin prevents that by:

1. **Deduplicating** — if the same `Message-ID` arrives again, the email is silently moved to the refused folder.
2. **Threading** — if an email references a previous ticket (via `References` or `Thread-Index` headers), it becomes a **followup** on that ticket instead of a new one.
3. **Linking** — if the referenced ticket is closed, a new ticket is created and linked to it.

---

## Compatibility

| Plugin version | GLPI version |
|---|---|
| **4.1.x** | **11.0.x** |
| 3.2.x | 10.0.18+ |
| 2.1.x | 9.5.x |

---

## Installation

### From marketplace / zip

1. Download the release zip and extract into `<glpi>/plugins/mailanalyzer/`
2. Run `composer install --no-dev --optimize-autoloader` inside the plugin directory
3. Go to **Setup → Plugins** and install + activate **Mail Analyzer**

### From source (development)

```bash
cd /var/www/glpi/plugins
git clone https://github.com/tomolimo/mailanalyzer.git mailanalyzer
cd mailanalyzer
git checkout main # or dev/glpi-11 for the GLPI 11 branch
composer install --no-dev
```

Then install via **Setup → Plugins**.

---

## Configuration

After activation, go to **Setup → General → Mail Analyzer** tab.

| Option | Description |
|---|---|
| **Use Thread-Index** | When enabled, the Microsoft `Thread-Index` header is used in addition to `References` to detect email threads. Useful for Outlook/Exchange environments. Requires an additional connection to the mail server per collected email (slight performance impact). |

---

## How threading works

```
Email A → GLPI creates Ticket #100, stores Message-ID in DB
Email B (Reply-All to A) → References header contains Message-ID of A
→ Plugin finds Ticket #100 → adds as Followup → moves mail to accepted folder
Email B again (duplicate) → Same Message-ID already in DB → refused folder
```

---

## GLPI 11 Migration notes (v3.x → v4.x)

- **PSR-4 autoloading**: classes moved from `inc/` to `src/` with `namespace GlpiPlugin\Mailanalyzer`
- **Hook constants**: now uses `Glpi\Plugin\Hooks::PRE_ITEM_ADD` etc.
- **Database**: `$DB->query()` replaced with `Migration` API + `$DB->request()` / `insert()` / `update()`
- **Config UI**: raw HTML replaced with `TemplateRenderer` + Twig (Bootstrap 5/Tabler)
- **No custom CSS**: UI uses GLPI 11 native Tabler/Bootstrap classes

---

## CLI utility

Decode a raw `Thread-Index` header value:

```bash
php plugins/mailanalyzer/scripts/threadindex.php AQHbR1gAJkY7...
# → 3a1f8b2c4d5e6f7a8b9c0d1e2f3a4b5c
```

---

## License

GPL v2 or later — see [LICENSE](LICENSE).

Original author: Olivier Moron / Raynet SAS — A.Raymond Network.
24 changes: 24 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "giovanny07/mailanalyzer",
"description": "MailAnalyzer plugin for GLPI — automatically combines CC/reply emails into existing tickets",
"type": "glpi-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Olivier Moron",
"role": "Original author"
}
],
"homepage": "https://github.com/giovanny07/mailanalyzer",
"require": {
"php": ">=8.1"
},
"autoload": {
"psr-4": {
"GlpiPlugin\\Mailanalyzer\\": "src/"
}
},
"config": {
"optimize-autoloader": true
}
}
33 changes: 7 additions & 26 deletions front/config.form.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,14 @@
-------------------------------------------------------------------------
MailAnalyzer plugin for GLPI
Copyright (C) 2011-2026 by Raynet SAS a company of A.Raymond Network.

https://www.araymond.com/
-------------------------------------------------------------------------

LICENSE

This file is part of MailAnalyzer plugin for GLPI.

This file 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 2 of the License, or
(at your option) any later version.

This plugin 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 plugin. If not, see <http://www.gnu.org/licenses/>.
LICENSE: GPLv2+
--------------------------------------------------------------------------
*/

include("../../../inc/includes.php");

/** @var array $CFG_GLPI */
global $CFG_GLPI;

Session::setActiveTab('Config', 'PluginMailanalyzerConfig$1');
Html::redirect($CFG_GLPI["root_doc"] . "/front/config.form.php");
/**
* GLPI 11: redirect to the Mail Analyzer tab on Setup > General.
* This file only exists to satisfy the Hooks::CONFIG_PAGE hook target.
*/
Session::setActiveTab('Config', 'GlpiPlugin\Mailanalyzer\Config$1');
Html::redirect(Toolbox::getItemTypeFormURL('Config'));
54 changes: 54 additions & 0 deletions front/save_config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
/*
-------------------------------------------------------------------------
MailAnalyzer plugin for GLPI
Copyright (C) 2011-2026 by Raynet SAS a company of A.Raymond Network.
-------------------------------------------------------------------------
LICENSE: GPLv2+
--------------------------------------------------------------------------
*/

/**
* Endpoint under front/ that saves the plugin configuration.
*
* GLPI 11 CSRF rules:
* - front/ and report/ → GLPI automatically verifies $_POST['_glpi_csrf_token']
* BEFORE this file is executed (in the GLPI Firewall). The Twig template
* renders that token via csrf_token(), so the form is compatible.
*
* - ajax/ → GLPI looks for the 'X-Glpi-Csrf-Token' HTTP header instead
* (not $_POST) → incompatible with a regular form POST.
*
* That's why this endpoint lives in front/ and not in ajax/.
*/

if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['update'])) {
Html::redirect(Toolbox::getItemTypeFormURL('Config'));
return;
}

// CSRF was already validated by the GLPI Firewall before reaching here.
// Only the user's rights need to be checked.
if (!Session::haveRight('config', UPDATE)) {
Html::displayRightError();
return;
}

// Save configuration
$use_threadindex = isset($_POST['use_threadindex']) ? (int) $_POST['use_threadindex'] : 0;

Config::setConfigurationValues('plugin:mailanalyzer', [
'use_threadindex' => $use_threadindex,
]);

Session::addMessageAfterRedirect(
__('Configuration updated', 'mailanalyzer'),
true,
INFO
);

Html::redirect(
Toolbox::getItemTypeFormURL('Config')
. '?forcetab='
. urlencode('GlpiPlugin\Mailanalyzer\Config$1')
);
Loading