├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── README.md ├── front └── config.form.php ├── hook.php ├── inc ├── config.class.php └── mailcollector.class.php ├── mailanalyzer.xml ├── plugin.png ├── scripts └── threadindex.php └── setup.php /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto !eol 2 | /README.md -text 3 | /hook.php -text 4 | /mailanalyzer.xml -text 5 | /plugin.png -text 6 | /setup.php -text 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 24 | 25 | **Describe the bug** 26 | 27 | A clear and concise description of what the bug is. 28 | 29 | **Page(s) URL** 30 | 31 | If applicable, page(s) URL where the bug happens. 32 | 33 | **To reproduce** 34 | 35 | Steps to reproduce the behavior: 36 | 1. Go to '...' 37 | 2. Click on '....' 38 | 3. Scroll down to '....' 39 | 4. See error 40 | 41 | **Expected behavior** 42 | 43 | A clear and concise description of what you expected to happen. 44 | 45 | **Logs** 46 | 47 | ``` 48 | paste logs here 49 | Find them in *-error.log files under glpi/files/_log/ 50 | ``` 51 | 52 | **Screenshots** 53 | 54 | If applicable, add screenshots to help explain your problem. 55 | 56 | 57 | **Your GLPI setup (you can find it in Setup > General menu, System tab)** 58 | 59 | ``` 60 | paste here 61 | ``` 62 | 63 | **Additional context** 64 | 65 | Add any other context about the problem here. 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | .vshistory 6 | .vs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mailanalyzer 2 | Mail Analyzer GLPI Plugin aims at keeping track of email conversations. 3 | It will combine emails from the same conversation into one Ticket. 4 | 5 | It is currently tested with GLPI 0.83.8, 0.85.5, 0.90, 9.1, 9.2, 9.3, 9.4, 9.5 and 10.0 6 | 7 | Must be copied into *glpifolder*/plugins/mailanalyzer, or it can be downloaded from the GLPI plugin marketplace. 8 | 9 | To be installed and enabled via the plugins configuration page in GLPI. 10 | 11 | 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'. 12 | It cannot keeps track of forwarded emails and replies to them. 13 | 14 | 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/ 15 | 16 | 17 | Please report any question/problem in the issue section. 18 | -------------------------------------------------------------------------------- /front/config.form.php: -------------------------------------------------------------------------------- 1 | . 26 | -------------------------------------------------------------------------- 27 | */ 28 | 29 | include ( "../../../inc/includes.php"); 30 | 31 | Session::setActiveTab('Config', 'PluginMailanalyzerConfig$1'); 32 | Html::redirect($CFG_GLPI["root_doc"]."/front/config.form.php"); 33 | -------------------------------------------------------------------------------- /hook.php: -------------------------------------------------------------------------------- 1 | . 26 | -------------------------------------------------------------------------- 27 | */ 28 | /** 29 | * Summary of plugin_mailanalyzer_install 30 | * @return boolean 31 | */ 32 | function plugin_mailanalyzer_install() { 33 | global $DB; 34 | 35 | if (!$DB->tableExists("glpi_plugin_mailanalyzer_message_id")) { 36 | $query = "CREATE TABLE `glpi_plugin_mailanalyzer_message_id` ( 37 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 38 | `message_id` VARCHAR(255) NOT NULL DEFAULT '0', 39 | `tickets_id` INT UNSIGNED NOT NULL DEFAULT '0', 40 | `mailcollectors_id` int UNSIGNED NOT NULL DEFAULT '0', 41 | PRIMARY KEY (`id`), 42 | UNIQUE INDEX `message_id` (`message_id`,`mailcollectors_id`), 43 | INDEX `tickets_id` (`tickets_id`) 44 | ) 45 | COLLATE='utf8mb4_unicode_ci' 46 | ENGINE=innoDB; 47 | "; 48 | 49 | $DB->query($query) or die("error creating glpi_plugin_mailanalyzer_message_id " . $DB->error()); 50 | } else { 51 | if (count($DB->listTables('glpi_plugin_mailanalyzer_message_id', ['engine' => 'MyIsam'])) > 0) { 52 | $query = "ALTER TABLE glpi_plugin_mailanalyzer_message_id ENGINE = InnoDB"; 53 | $DB->query($query) or die("error updating ENGINE in glpi_plugin_mailanalyzer_message_id " . $DB->error()); 54 | } 55 | } 56 | if ($DB->fieldExists("glpi_plugin_mailanalyzer_message_id","mailgate_id")) 57 | { 58 | //STEP - UPDATE MAILGATE_ID INTO MAILCOLLECTORS_ID 59 | $query = "ALTER TABLE `glpi_plugin_mailanalyzer_message_id` 60 | CHANGE COLUMN `mailgate_id` `mailcollectors_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `message_id`, 61 | DROP INDEX `message_id`, 62 | ADD UNIQUE INDEX `message_id` (`message_id`, `mailcollectors_id`) USING BTREE;"; 63 | $DB->query($query) or die("error updating ENGINE in glpi_plugin_mailanalyzer_message_id " . $DB->error()); 64 | } 65 | if (!$DB->fieldExists("glpi_plugin_mailanalyzer_message_id","mailcollectors_id")) 66 | { 67 | //STEP - ADD mailcollectors_id 68 | $query = "ALTER TABLE glpi_plugin_mailanalyzer_message_id ADD COLUMN `mailcollectors_id` int UNSIGNED NOT NULL DEFAULT 0 AFTER `message_id`"; 69 | $DB->query($query) or die("error updating ENGINE in glpi_plugin_mailanalyzer_message_id " . $DB->error()); 70 | 71 | //STEP - REMOVE UNICITY CONSTRAINT 72 | $query = "ALTER TABLE glpi_plugin_mailanalyzer_message_id DROP INDEX `message_id`"; 73 | $DB->query($query) or die("error updating ENGINE in glpi_plugin_mailanalyzer_message_id " . $DB->error()); 74 | //STEP - ADD NEW UNICITY CONSTRAINT 75 | $query = "ALTER TABLE glpi_plugin_mailanalyzer_message_id ADD UNIQUE KEY `message_id` (`message_id`,`mailcollectors_id`);"; 76 | $DB->query($query) or die("error updating ENGINE in glpi_plugin_mailanalyzer_message_id " . $DB->error()); 77 | } 78 | 79 | if (!$DB->fieldExists('glpi_plugin_mailanalyzer_message_id', 'tickets_id')) { 80 | // then we must change the name and the length of id and ticket_id to 11 81 | $query = "ALTER TABLE `glpi_plugin_mailanalyzer_message_id` 82 | CHANGE COLUMN `id` `id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST, 83 | CHANGE COLUMN `ticket_id` `tickets_id` INT UNSIGNED NOT NULL DEFAULT '0' AFTER `message_id`, 84 | DROP INDEX `ticket_id`, 85 | ADD INDEX `ticket_id` (`tickets_id`);"; 86 | $DB->query($query) or die('Cannot alter glpi_plugin_mailanalyzer_message_id table! ' . $DB->error()); 87 | } 88 | 89 | return true; 90 | } 91 | 92 | 93 | /** 94 | * Summary of plugin_mailanalyzer_uninstall 95 | * @return boolean 96 | */ 97 | function plugin_mailanalyzer_uninstall() { 98 | 99 | // nothing to uninstall 100 | // do not delete table 101 | 102 | return true; 103 | } 104 | 105 | 106 | /** 107 | * Summary of PluginMailAnalyzer 108 | */ 109 | class PluginMailAnalyzer { 110 | 111 | /** 112 | * Create default mailgate 113 | * @param int $mailcollectors_id is the id of the mail collector in GLPI DB 114 | * @return bool|MailCollector 115 | */ 116 | static function openMailgate($mailcollectors_id) : PluginMailanalyzerMailCollector { 117 | 118 | $mailgate = new PluginMailanalyzerMailCollector(); 119 | $mailgate->getFromDB($mailcollectors_id); 120 | $mailgate->uid = -1; 121 | $mailgate->connect(); 122 | 123 | return $mailgate; 124 | } 125 | 126 | 127 | /** 128 | * Summary of plugin_pre_item_add_mailanalyzer 129 | * @param mixed $parm 130 | * @return void 131 | */ 132 | public static function plugin_pre_item_add_mailanalyzer($parm) { 133 | global $DB, $mailgate; 134 | 135 | $mailgateId = $parm->input['_mailgate'] ?? false; 136 | if ($mailgateId) { 137 | // this ticket have been created via email receiver. 138 | // Analyzes emails to establish conversation 139 | 140 | // search for 'Thread-Index'? 141 | $config = Config::getConfigurationValues('plugin:mailanalyzer'); 142 | $use_threadindex = isset($config['use_threadindex']) && $config['use_threadindex']; 143 | 144 | if (isset($mailgate)) { 145 | // mailgate has been open by web page call, then use it 146 | $local_mailgate = $mailgate; 147 | // if use of threadindex is true then must open a new mailgate 148 | // to be able to get the threadindex of the email 149 | if ($use_threadindex) { 150 | $local_mailgate = PluginMailAnalyzer::openMailgate($mailgateId); 151 | } 152 | } else { 153 | // mailgate is not open. Called by cron 154 | // then locally create a mailgate 155 | $local_mailgate = PluginMailAnalyzer::openMailgate($mailgateId); 156 | if ($local_mailgate === false) { 157 | // can't connect to the mail server, then cancel ticket creation 158 | $parm->input = false;// []; // empty array... 159 | return; 160 | } 161 | } 162 | 163 | if ($use_threadindex) { 164 | $local_message = $local_mailgate->getMessage($parm->input['_uid']); 165 | $threadindex = $local_mailgate->getThreadIndex($local_message); 166 | if ($threadindex) { 167 | // add threadindex to the '_head' of the input 168 | $parm->input['_head']['threadindex'] = $threadindex; 169 | } 170 | } 171 | 172 | 173 | // we must check if this email has not been received yet! 174 | // test if 'message-id' is in the DB 175 | $messageId = html_entity_decode($parm->input['_head']['message_id']); 176 | $uid = $parm->input['_uid']; 177 | $res = $DB->request( 178 | 'glpi_plugin_mailanalyzer_message_id', 179 | [ 180 | 'AND' => 181 | [ 182 | 'tickets_id' => ['!=', 0], 183 | 'message_id' => $messageId, 184 | 'mailcollectors_id' => $mailgateId 185 | ] 186 | ] 187 | ); 188 | if ($row = $res->current()) { 189 | // email already received 190 | // must prevent ticket creation 191 | $parm->input = false; //[ ]; 192 | 193 | // as Ticket creation is cancelled, then email is not deleted from mailbox 194 | // then we need to set deletion flag to true to this email from mailbox folder 195 | $local_mailgate->deleteMails($uid, MailCollector::REFUSED_FOLDER); // NOK Folder 196 | 197 | return; 198 | } 199 | 200 | // search for 'Thread-Index' and 'References' 201 | $messages_id = self::getMailReferences( 202 | $parm->input['_head']['threadindex'] ?? '', 203 | html_entity_decode($parm->input['_head']['references'] ?? '') 204 | ); 205 | 206 | if (count($messages_id) > 0) { 207 | $res = $DB->request( 208 | 'glpi_plugin_mailanalyzer_message_id', 209 | ['AND' => 210 | [ 211 | 'tickets_id' => ['!=',0], 212 | 'message_id' => $messages_id, 213 | 'mailcollectors_id' => $mailgateId 214 | ], 215 | 'ORDER' => 'tickets_id DESC' 216 | ] 217 | ); 218 | if ($row = $res->current()) { 219 | // TicketFollowup creation only if ticket status is not closed 220 | $locTicket = new Ticket(); 221 | $locTicket->getFromDB((integer)$row['tickets_id']); 222 | if ($locTicket->fields['status'] != CommonITILObject::CLOSED) { 223 | $ticketfollowup = new ITILFollowup(); 224 | $input = $parm->input; 225 | $input['items_id'] = $row['tickets_id']; 226 | $input['users_id'] = $parm->input['_users_id_requester']; 227 | $input['add_reopen'] = 1; 228 | $input['itemtype'] = 'Ticket'; 229 | 230 | unset($input['urgency']); 231 | unset($input['entities_id']); 232 | unset($input['_ruleid']); 233 | 234 | $ticketfollowup->add($input); 235 | 236 | // add message id to DB in case of another email will use it 237 | $DB->insert( 238 | 'glpi_plugin_mailanalyzer_message_id', 239 | [ 240 | 'message_id' => $messageId, 241 | 'tickets_id' => $input['items_id'], 242 | 'mailcollectors_id' => $mailgateId 243 | ] 244 | ); 245 | 246 | // prevent Ticket creation. Unfortunately it will return an error to receiver when started manually from web page 247 | $parm->input = false; // []; // empty array... 248 | 249 | // as Ticket creation is cancelled, then email is not deleted from mailbox 250 | // then we need to set deletion flag to true to this email from mailbox folder 251 | $local_mailgate->deleteMails($uid, MailCollector::ACCEPTED_FOLDER); // OK folder 252 | 253 | return; 254 | 255 | } else { 256 | // ticket creation, but linked to the closed one... 257 | $parm->input['_link'] = ['link' => '1', 'tickets_id_1' => '0', 'tickets_id_2' => $row['tickets_id']]; 258 | } 259 | } 260 | } 261 | 262 | // can't find ref into DB, then this is a new ticket, in this case insert refs and message_id into DB 263 | $messages_id[] = $messageId; 264 | 265 | // this is a new ticket 266 | // then add references and message_id to DB 267 | foreach ($messages_id as $ref) { 268 | $res = $DB->request('glpi_plugin_mailanalyzer_message_id', ['message_id' => $ref, 'mailcollectors_id' => $mailgateId]); 269 | if (count($res) <= 0) { 270 | $DB->insert('glpi_plugin_mailanalyzer_message_id', ['message_id' => $ref, 'mailcollectors_id' => $mailgateId]); 271 | } 272 | } 273 | } 274 | } 275 | 276 | 277 | /** 278 | * Summary of plugin_item_add_mailanalyzer 279 | * @param mixed $parm 280 | */ 281 | public static function plugin_item_add_mailanalyzer($parm) { 282 | global $DB; 283 | if (isset($parm->input['_mailgate'])) { 284 | // this ticket have been created via email receiver. 285 | // update the ticket ID for the message_id only for newly created tickets (tickets_id == 0) 286 | 287 | // Are 'Thread-Index' or 'Refrences' present? 288 | $messages_id = self::getMailReferences( 289 | $parm->input['_head']['threadindex'] ?? '', 290 | html_entity_decode($parm->input['_head']['references'] ?? '') 291 | ); 292 | $messages_id[] = html_entity_decode($parm->input['_head']['message_id']); 293 | 294 | $DB->update( 295 | 'glpi_plugin_mailanalyzer_message_id', 296 | [ 297 | 'tickets_id' => $parm->fields['id'] 298 | ], 299 | [ 300 | 'WHERE' => 301 | [ 302 | 'AND' => 303 | [ 304 | 'tickets_id' => 0, 305 | 'message_id' => $messages_id 306 | ] 307 | ] 308 | ] 309 | ); 310 | } 311 | } 312 | 313 | 314 | /** 315 | * Summary of getMailReferences 316 | * @param string $threadindex 317 | * @param string $references 318 | * @return string[] 319 | */ 320 | private static function getMailReferences(string $threadindex, string $references) { 321 | 322 | $messages_id = []; // by default 323 | 324 | if (!empty($threadindex)) { 325 | $messages_id[] = $threadindex; 326 | } 327 | 328 | // search for 'References' 329 | if (!empty($references)) { 330 | // we may have a forwarded email that looks like reply-to 331 | if (preg_match_all('/<.*?>/', $references, $matches)) { 332 | $messages_id = array_merge($messages_id, $matches[0]); 333 | } 334 | } 335 | 336 | // clean $messages_id array 337 | return array_filter($messages_id, function($val) {return $val != trim('', '< >');}); 338 | } 339 | 340 | 341 | /** 342 | * Summary of plugin_item_purge_mailanalyzer 343 | * @param mixed $item 344 | */ 345 | static function plugin_item_purge_mailanalyzer($item) { 346 | global $DB; 347 | // the ticket is purged, then we are going to purge the matching rows in glpi_plugin_mailanalyzer_message_id table 348 | // DELETE FROM glpi_plugin 349 | $DB->delete('glpi_plugin_mailanalyzer_message_id', ['tickets_id' => $item->getID()]); 350 | } 351 | } 352 | 353 | -------------------------------------------------------------------------------- /inc/config.class.php: -------------------------------------------------------------------------------- 1 | . 26 | -------------------------------------------------------------------------- 27 | */ 28 | 29 | class PluginMailanalyzerConfig extends CommonDBTM { 30 | 31 | /** 32 | * Summary of getTypeName 33 | * @param mixed $nb plural 34 | * @return mixed 35 | */ 36 | static function getTypeName($nb = 0) { 37 | return __('Mail Analyzer setup', 'mailanalyzer'); 38 | } 39 | 40 | /** 41 | * Summary of getName 42 | * @param mixed $with_comment with comment 43 | * @return mixed 44 | */ 45 | function getName($with_comment = 0) { 46 | return __('MailAnalyzer', 'mailanalyzer'); 47 | } 48 | 49 | 50 | /** 51 | * Summary of showConfigForm 52 | * @param mixed $item is the config 53 | * @return boolean 54 | */ 55 | static function showConfigForm($item) { 56 | $config = Config::getConfigurationValues('plugin:mailanalyzer'); 57 | 58 | echo "