Project

General

Profile

Plugins Directory » Redmine expert Helpdesk

Author: Dennis Buehring
Website: https://redmine-plugins.expert.de
Code repository: https://github.com/expertZentrale/redmine_expert_helpdesk
Registered on: 2026-07-29 (3 months ago)
Current version: 0.20.4
Compatible with: Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x
User ratings:   (0)

What it does

Turns mailboxes into a Redmine helpdesk. Incoming mail becomes a ticket, replies are matched onto the existing ticket, and agents answer the customer straight from the ticket page. Contacts, SLA tracking, phishing detection and a rules engine sit on top.

Reply matching is delegated to Redmine's own MailHandler, so In-Reply-To and [#id] subject patterns behave exactly as they do for Redmine's built-in mail handling, attachments included.

Mail providers

Each mailbox chooses its own backend:

  • Microsoft 365 via the Microsoft Graph API (application-only).
  • Generic IMAP/SMTP for Google Workspace, Exchange on-premises, self-hosted servers (Dovecot, Zimbra) and any ordinary hoster.

Authentication is OAuth2/XOAUTH2 by default, with three grants — client_credentials (app-only), authorization_code (one-time consent) and jwt_bearer (service account with domain-wide delegation). Username/password over TLS remains available for servers without OAuth2. Per-mailbox secrets are stored encrypted.

Features

  • Per-project mailboxes — source and target folders, defaults for tracker, priority and status, handling of unknown senders.
  • Autoresponder — configurable confirmation mail for new tickets.
  • Status on a customer reply — per mailbox, an inbound reply moves the ticket to a status of its own: one for a closed ticket (the classic reopen) and a separate one for a ticket that is still open, so a waiting for customer status does not have to be closed to move on. Both are optional. An age limit can force a new ticket for a reply to a long-closed one.
  • Customer replies from the ticket page, with header/footer templates, inline images (CID), attachments and CC/BCC. Sent through the mailbox itself, so a copy lands in its Sent folder. A mailbox routed over Redmine's own SMTP settings can optionally send under a different address, for a distribution list that became a helpdesk mailbox.
  • Address autocomplete — To/CC/BCC suggest matching project contacts.
  • Original recipients — behind To and CC, a button offers the addresses the mail that opened the ticket was sent to, each with a checkbox, so everyone who was on the original thread can be put back on the answer with a click. The helpdesk's own address and the customer are filtered out.
  • Quoting and answer templates — next to the formatting icons of the note field, a Quote button inserts the original mail, the complete conversation or only the mail exchange (private notes never among them), and a Templates button inserts a stored answer with its macros already resolved. Templates are maintained globally and per project.
  • Template macros — ticket fields (status, priority, tracker, author, assignee, category, version, dates, progress), the replying agent's name, login and address, and the project. Issue custom fields can be used too, each one enabled by an administrator and still subject to its own Redmine visibility, so an internal field cannot reach a customer through a shared template.
  • Embedded images — inline images of an incoming mail are shown in the ticket where the mail showed them, instead of leaving a [cid:…] marker behind, and images referenced in a reply are sent to the customer as inline parts.
  • Contacts — senders are saved automatically; customer list per project, a customer panel with previous tickets on the ticket page, and sortable Customer / Customer email issue-list columns with a filter matching name or email.
  • Black-/whitelists — sender and domain filters per mailbox.
  • Marked customer replies — the mail fields, the note field and the signature preview form one block in the ticket form, with a warning border while a reply is armed, so the field whose text goes to the customer cannot be mistaken for an internal note. Both colours are configurable centrally and per project.
  • Blocked attachments — a signature logo, icon or tracking pixel is blocked once from the ticket page; every copy in the project is removed and later mails arrive without it. Matching is the file content (SHA-256), not the file name, and an allow list of file types plus a size limit decide which attachments the button is offered for at all, so an .eml, a PDF or a large screenshot is never one misclick from deletion.
  • SLA — reaction and solution targets in business minutes, per-priority overrides, traffic-light display, sortable and filterable SLA columns in the issue list, breach notification via a cron endpoint, plus an "SLA statistics" tab with charts.
  • Ticket statistics — a per-project tab for every helpdesk project, SLA or not, behind its own member permission: ticket volume, first response and resolution times, time in status, reopens, conversation length, per-agent and per-customer tables, busiest hours/weekdays; calendar time or the project's business hours.
  • Phishing detection — optional check of incoming links against a local mirror of PhishTank and the Phishing.Database feed, Microsoft SafeLinks decoded locally. On a hit the links are neutralised or the mail is quarantined, per project.
  • AI summaries and knowledge base (optional) — per-project mail summaries via OpenAI, Anthropic or an OpenAI-compatible endpoint, and a RAG knowledge base built from resolved tickets (Qdrant or pgvector), optionally with a cross-encoder reranking stage for markedly more precise retrieval. A per-project Knowledge base tab lets reviewers check, correct and curate the extracted entries (re-embedded on save). Off by default.
  • AI answer drafts (optional) — a toolbar button that drafts the reply to the customer for the ticket at hand, grounded in that project's knowledge base and never sent automatically. Without a sufficiently similar knowledge base entry no draft is produced, private notes and attachments are never sent to the model, and the entries it works from are passed without their ticket numbers. Off by default.
  • Completeness check (optional) — evaluates the first mail of a new ticket and, when it does not carry enough to start working, automatically mails the customer for the missing details, notes it on the ticket and can set a "waiting for customer" status. Two modes per project: rule-based (minimum length, attachment required, expected terms, threshold) or AI-powered. Senders that cannot answer — backup reports, cron mails, monitoring alerts — are excluded by a per-project list, by a flag agents set on the customer straight from the ticket, and by automatic detection of machine-generated mail headers. Off by default; the AI mode fails closed, so a garbled model response never mails a customer.
  • Awaiting response — a ticket is flagged when a customer replies by mail or a reply reopens a closed ticket, and unflagged when an agent answers publicly or closes it. Sortable column and filter in the issue list, a marker on the row, a sidebar counter and a My page block.
  • Ticket assignment — new tickets from mail go to a user or a group chosen per project, a matching rule overrides that for the mail it applies to, and the "assign to me when replying" option preselects the replying agent, and only on a ticket nobody holds yet.
  • Rules — automation by subject or sender: set priority, tracker, category, assignee (user or group), or ignore the mail.
  • Initial mail — assign a customer contact to any ticket and optionally send the first outbound mail.
  • REST API (JSON and XML) following Redmine's own conventions, authenticated with X-Redmine-API-Key and scoped by the existing permissions: contacts, tickets, per-project settings and mailboxes. Mailboxes are fully configurable over the API — backend, IMAP/SMTP hosts, OAuth2 credentials, folders — with a test_connection call to verify one. Secrets can be written but are never returned.

Requirements

  • Redmine 5.1.x, 6.0.x, 6.1.x or 7.0.x
  • Two gems: base64 (required — no longer a Ruby default gem as of Ruby 4.0, which the official redmine:7 image ships) and pdf-reader (optional — PDF text for AI summaries, skipped when absent)

A third gem, pg, is needed only by the optional pgvector backend of the knowledge base; it is deliberately not in PluginGemfile, so the default Qdrant build does not drag in libpq.

Links

License

GNU General Public License, version 2 or later — the same license Redmine itself uses.

Installation notes

Requirements

  • Redmine 5.1.x, 6.0.x, 6.1.x or 7.0.x
  • Two gems: base64 (required — no longer a Ruby default gem as of Ruby 4.0, which the official redmine:7 image ships) and pdf-reader (optional — PDF text for AI summaries, skipped when absent)

Every release is tested against Redmine 5.1, 6.0, 6.1 and 7.0 on a clean MariaDB, and additionally booted inside the official redmine Docker images for the same versions.

A third gem, pg, is needed only by the optional pgvector backend of the knowledge base; it is deliberately not in PluginGemfile, so the default Qdrant build does not drag in libpq. Both backends are off unless you enable them.

1. Install the plugin

Download redmine_expert_helpdesk-<version>.zip (or .tar.gz) from the Releases page. The archive already contains a top-level redmine_expert_helpdesk/ folder, so extract it directly into your Redmine plugins/ directory:

cd /path/to/redmine/plugins
unzip redmine_expert_helpdesk-<version>.zip

Alternatively, from source:

cd /path/to/redmine/plugins
git clone https://github.com/expertZentrale/redmine_expert_helpdesk.git

The directory must be named redmine_expert_helpdesk — Redmine derives the plugin id from it, and the plugin's own asset and settings paths depend on it.

2. Run the migrations

cd /path/to/redmine
bundle exec rake redmine:plugins:migrate NAME=redmine_expert_helpdesk RAILS_ENV=production

Then restart Redmine so the plugin is loaded — touch tmp/restart.txt for Passenger, or restart the Puma/Unicorn service.

3. Enable the module and grant permissions

Enable expert Helpdesk per project under Project settings → Modules, then assign the permissions under Administration → Roles and permissions:

Permission Grants
Manage helpdesk mailboxes Mailbox configuration, folders, rules, answer templates, blocked attachments
Fetch helpdesk mails The "Fetch mails now" button
Send customer replies Reply form, contact autocomplete, initial mail
View helpdesk info Info bar and sidebar on the ticket page
Manage contacts Customer list and customer profile
View SLA statistics The project's "SLA statistics" tab
View ticket statistics The project's "Ticket statistics" tab (member roles only, e.g. a "helpdesk manager" role)
View knowledge base The project's "Knowledge base" tab: list and detail of the RAG entries
Edit knowledge base entries Correct, create, approve and reject entries (re-embeds on save); also the knowledge-base buttons in the ticket sidebar
Manage knowledge base Delete entries and rebuild the project's vector index

View AI statistics is the one global permission — it is granted once to a role and then applies across all projects without project membership, because it exposes the token spend of the AI features. The tab it unlocks is shown only where the AI features or the knowledge base are enabled; with both switched off it stays hidden and the page answers 403.

4. Central configuration

Under Administration → Plugins → Redmine expert Helpdesk, or via the expert Helpdesk entry in the Administration menu, which links there directly.

Setting Purpose
Tenant ID / Client ID / Client Secret The Azure app registration, used by Microsoft 365 mailboxes and shared by IMAP/SMTP mailboxes that authenticate with OAuth2
Default credentials for IMAP/SMTP Preset, grant, authorization and token URL, scope, plus default IMAP/SMTP host, port and encryption
API key (mail fetch) Secures the global helpdesk/fetch_all endpoint
API key (SLA check) Secures the helpdesk/sla_check endpoint
Entries per page Page size of the customer list (default 25)
Max. tickets in customer profile Tickets shown in the customer detail view (default 10)
Mark tickets awaiting a response Flags tickets whose customer replied by mail and clears the flag when an agent answers (on by default)
Answer templates Global canned responses offered in every helpdesk project, in addition to a project's own ones
Embedded images Rewrites the [cid:…] markers of incoming mails to the stored image attachment (on by default)
Reply block The two colours of the customer-facing block in the ticket form: its background and the warning border shown while a reply is armed. Hex colours, each overridable per project; a colour picker and a live sample sit next to the fields
Blocked attachments The two guards on the Block button: the file types it may be offered for (default png, gif, jpg, jpeg, bmp, webp, tif, tiff, ico; * allows all) and the largest file it may be offered for (default 100 KB, 0 disables the limit). Both can be overridden per project; they gate the button only and never drop anything from incoming mail
Custom fields as macros Issue custom fields that may be used as {{issue.cf.…}} in templates. Nothing is enabled by default; a field's own Redmine visibility still applies on top
Completeness check Master switch for the automatic follow-up when an incoming mail carries too little information, plus the central texts and check prompt. Off by default, and each project additionally chooses its own mode
AI answer drafts Master switch for the customer-facing draft button, plus its prompt, minimum match, output budget and timeout. Off by default, and each project opts in separately. Requires the AI settings, and the knowledge base for every variant except "ask for missing details"
Knowledge base reranker Optional second retrieval stage: a cross-encoder re-scores the shortlist the vector store returns, which is markedly more precise than similarity alone. Off by default; endpoint and key fall back to the embeddings ones, since both models usually share a base URL. Its own minimum score replaces the similarity one while it is on, and both want retuning — the two are different scales

Nothing here is required for a mailbox that carries its own credentials — see the next step.

5. Add a mailbox

Under Project settings → Helpdesk, add a mailbox and pick its Mail provider:

Provider Incoming Outgoing Typical use
Microsoft 365 (Graph) Graph API Graph sendMail Microsoft 365 / Exchange Online
IMAP / SMTP IMAP SMTP Google Workspace, Exchange on-premises, Dovecot, Zimbra, any hoster

Credentials is an explicit switch, not a fallback chain. From plugin settings uses the central app registration above; Individual for this mailbox uses only the mailbox's own fields. A mailbox uses one source entirely — blank fields are deliberately not filled in from the other, so a half-configured mailbox cannot silently authenticate against the wrong tenant.

Use Test connection before saving. It performs a real login, lists the folders and probes the outgoing route, so a wrong port, encryption mode or credential surfaces immediately instead of on the first fetch.

Reply transport decides how outgoing mail leaves: through the mailbox's own backend, through Microsoft Graph, or through Redmine's global SMTP settings. Only the last one offers Sender override (From), an optional address to send under instead of the mailbox address — meant for a distribution list that became a helpdesk mailbox. The relay must be allowed to send for that address. Graph and the mailbox's own SMTP server authenticate as the mailbox and reject a foreign sender, so the field is hidden for them.

For Microsoft 365 the app registration needs the application permissions Mail.ReadWrite and Mail.Send, granted with admin consent.

6. Schedule the fetch

There is no built-in scheduler. Mail is fetched either with the Fetch mails now button in the project settings, or through an endpoint meant for cron:

curl "https://redmine.example.com/helpdesk/fetch_all?key=FETCH-API-KEY" 

That processes every active mailbox across all projects and piggybacks the phishing-feed sync. The SLA breach check is deliberately separate, so it can run on its own schedule:

curl "https://redmine.example.com/helpdesk/sla_check?key=SLA-API-KEY" 

Both keys come from the central configuration. Endpoints without a key configured refuse the request rather than running unauthenticated.

REST API

The plugin's own REST API (contacts, tickets, project settings, mailboxes) is separate from the two cron endpoints above and uses Redmine's normal API authentication. Enable Administration → Settings → API → Enable REST web service; requests then authenticate with an X-Redmine-API-Key header and are authorized by the same permissions as the UI. Mailbox endpoints require manage_helpdesk for reading as well as writing, because the configuration exposes mail hosts, usernames and OAuth client ids. Full reference: API.md

Upgrading

Replace the plugin directory with the new version, then run the migration command from step 2 again and restart. Migrations are additive and existing mailboxes keep their configuration; if a release needs anything beyond that it is called out in that version's notes.

Downgrading is not supported — roll back the database alongside the plugin if you need to.

Uninstalling

bundle exec rake redmine:plugins:migrate NAME=redmine_expert_helpdesk VERSION=0 RAILS_ENV=production
rm -rf plugins/redmine_expert_helpdesk

This drops the plugin's tables and all helpdesk data — contacts, message log, SLA tracking. Tickets created from mail are ordinary Redmine issues and stay.

Changelog

0.20.4 (2026-10-06)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Detail level for knowledge-base extraction. Entries used to be generalised, which dropped exactly what decides whether a past fix applies: application names, versions, error codes, paths, systems. A new setting offers three levels: General (previous behaviour, still the default), Specific (keeps application and product names, versions, error codes and messages) and Most specific (also paths, server and share names, configuration values and the exact order of steps). At every level the model is instructed to leave out credentials and personal data.
  • Per-project extraction settings. Projects can set their own detail level and extraction prompt (use central / extend / override, like the other AI prompts) in the knowledge-base section of the Helpdesk tab and via the REST API.
  • Re-extraction of existing entries. "Re-extract entries" on the Knowledge base tab (permission Manage knowledge base) and rake redmine_expert_helpdesk:kb_reextract re-run extraction at the current level. Entries keep their status, a run without a solution leaves the entry unchanged, and entries curated by a person are never touched. Each entry costs one AI call; the confirmation shows the count.

Changed

  • The AI summary compares application, version, error code, paths and systems of matching knowledge-base cases with the ticket and names differences when several fit.
  • The customer-facing answer draft is instructed not to quote internal details (hosts, IPs, network paths, accounts, other customers) from knowledge-base cases.

Fixed

  • Knowledge-base extraction dropped answers containing Windows paths. A path copied verbatim (\\SRV01\Share\Config) is not valid JSON, so the entry was silently lost; paths like C:\new\test were stored with a line break and a tab instead. Paths are now repaired before parsing, with the ticket text deciding ambiguous cases.

Upgrade notes

Migration 065 adds the detail-level columns - run the migration command. Nothing changes until a level is raised: the default stays General, and a stored copy of an older default extraction prompt counts as not customised, so the level selection takes effect. Raise the level centrally or per project, then use "Re-extract entries" on the Knowledge base tab to bring existing entries to it.

0.20.3 (2026-10-01)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • An attachment read as text could take down the whole AI summary. Text attachments were read as raw bytes, which could not be joined with the prompt - any text file with an umlaut, or a .zip that Outlook labelled text/plain, made the summary fail and the ticket got none. Attachment text is now read as UTF-8 (Windows-1252 as fallback), and content that is actually binary is skipped instead of being sent to the model. A skipped attachment is written to the log ([helpdesk][ai][debug] attachment-text ... skipped=binary, level per AI log level).

Upgrade notes

No migrations. Tickets whose summary failed this way can get one with the sidebar's regenerate button.

0.20.2 (2026-10-01)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • The sidebar could recommend a different ticket than the AI summary. Both start from the same similar solved tickets, but the summary reads problem and solution and only cites the case that fits, while the sidebar showed them in plain score order. The sidebar now follows the summary: the case it cites comes first, marked in the AI summary, the others are greyed out, and if none fits the sidebar says so. This applies when a project shows proposed solutions in both places.
  • The knowledge base was searched with the whole mail, signature included, so a short request under a long signature could match the wrong entries. The search now uses the subject and what the customer wrote, without quoted history, closing greeting, signature, links, addresses, phone numbers and images. The AI summary still sees the full mail.

Added

  • If a configured reranker could not be reached, the sidebar now says that the order is by text similarity only.

Upgrade notes

Migration 064 adds two columns to helpdesk_kb_proposals. Run plugin migrations. Existing tickets show the new sidebar after their AI summary is regenerated.

0.20.1 (2026-10-01)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • Deleting a ticket left its helpdesk data behind. Deleting an issue through Redmine itself (the issue page, bulk delete or deleting the project) kept its helpdesk ticket info and its mail messages. They are now deleted together with the issue.
  • Deleting a project left its helpdesk contacts behind, invisible in every list and impossible to delete through the REST API, not even by an administrator. A project's contacts are now deleted with it.
  • Deleting a ticket that had mail through the REST API failed (DELETE /helpdesk/tickets/:id answered 500). It works now, and the ticket's messages are deleted with it.

Changed

  • The release workflow now refreshes the plugin website (redmine-plugins.expert.de) right after a release.

Upgrade notes

Migration 063 runs once and removes what earlier deletes left behind: ticket infos and messages of tickets that no longer exist, and contacts of projects that no longer exist. Nothing it removes is reachable any more. On a database of about 180,000 tickets it took a few seconds. Run plugin migrations.

0.20.0 (2026-09-30)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Recipients of the initial mail on the ticket page. When a ticket is created from an email that was also addressed to other people, the info bar now shows a pill such as An +1 | CC +2 next to the sender, counting everyone besides the mailbox that received the mail. Hovering or clicking it (or pressing Enter) opens a list of every To and CC address, with your own mailbox marked. Agents see at a glance whether the customer wrote to several support mailboxes or copied colleagues in. Previously the addresses were only a tooltip on the envelope icon.

Changed

  • Incoming replies use the same pill. The separate An +1 / CC +1 tags in the journal headers of incoming mails are replaced by the pill above. The counting changes with it: +N is now the number of people besides your mailbox, so a CC line with two addresses reads CC +2 (was CC +1).

Upgrade notes

No migrations and no new settings.

0.19.0 (2026-09-29)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Maintenance mode for upgrades and scaling down. Knowing whether a pod was in the middle of processing mail used to mean reading the logs and guessing. A switch under Administration → Plugins → expert Helpdesk → Maintenance mode now pauses every mail fetch: the cron endpoint helpdesk/fetch_all answers {"maintenance": true} without touching a mailbox, the project's Fetch mails now button only shows a notice, and a fetch that is already running finishes the message at hand and stops. Unprocessed mails stay in the mailbox and are picked up after the switch is turned off.
  • Scriptable, with a status that covers every replica. POST /helpdesk/maintenance?key=FETCH-API-KEY&enabled=1 (or 0) switches it, GET on the same URL lists the fetches still in progress on any pod — mailbox, host name, messages done, heartbeat — and reports "safe_to_stop": true once the mode is on and none is left. A pod killed mid-fetch drops out of the list after 10 minutes without a heartbeat. Background jobs a fetch already queued (AI summary, completeness check) are not part of this status.

Upgrade notes

Migration 062 adds the table helpdesk_fetch_runs. Run plugin migrations. The mode is off after the upgrade and the fetch API key is reused, so nothing needs configuring; the endpoint refuses requests while no fetch key is set. This upgrade itself still runs without the pause — from the next one on, switch it on first and wait for "safe_to_stop": true.

0.18.1 (2026-09-28)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • Ticket statistics listed agents who do not work in the project. The per-agent table added a row for everyone who had been assigned an in-scope ticket, wrote a public note on one or closed one — including users who are not members of the project at all, such as an administrator who once commented, former members or deleted users. The table now shows current project members only; users who belong to the project through a group still count, and member groups keep their row. The tickets of the hidden users stay in every other figure on the page, and the "unassigned" row is unchanged.

Documentation

  • Reranker tuning, with measured numbers. The knowledge-base section of the README now has a worked example that measures the reranker threshold on synthetic data, with seed data and the measuring script shipped in docs/kb_reranker_demo/ so installations can repeat the measurement with their own models.

Upgrade notes

No migrations, no configuration changes and no new settings. Replace the plugin directory and restart. The Assigned column of the per-agent table may no longer add up to the ticket total when tickets are assigned to someone who has left the project.

0.18.0 (2026-09-25)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Knowledge base tab. The problem/solution pairs the knowledge base extracts from resolved tickets can now be checked and corrected in Redmine, per project. Editing a payload in the vector database never helped — the vector stays the one computed from the old text — so the tab edits the entry and re-embeds it on save. Entries can be approved, rejected, created by hand for a ticket of the project and deleted, and the project's vector index can be rebuilt in the background. Works with Qdrant and pgvector.
  • Three new permissions in the helpdesk module: View knowledge base, Edit knowledge base entries and Manage knowledge base, meant for roles such as KB viewer, KB editor and KB admin.

Changed

  • Corrections are kept. An entry that a person edited, approved or rejected is no longer overwritten when its ticket is reopened and closed again. The Add to knowledge base button in the ticket sidebar still re-extracts it on request.

Fixed

  • An entry that lost its approval on re-ingest (no solution found, or pending in manual mode) stayed searchable. Its vector is now removed.

Upgrade notes

Migration 061 adds two columns to helpdesk_knowledge_entries (who checked an entry and when). Run the plugin migrations.

The new tab is hidden until a role has View knowledge base; grant the three permissions under Administration → Roles and permissions. The ticket-sidebar buttons keep working for roles with Send customer replies.

0.17.0 (2026-09-25)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Original mails can be given back to the RedmineUP helpdesk. The EML repair moves each message.eml from its redmine_contacts_helpdesk ticket onto the Redmine issue. That is right for projects that left RedmineUP, but a project that still works with the RedmineUP helpdesk then no longer finds its original mails, because RedmineUP only looks on its own ticket. The new action Give back to RedmineUP reverses the repair for the selected projects. It moves only unambiguous cases: the issue has exactly one RedmineUP ticket and exactly one message.eml, and that ticket holds no file yet. Mails archived by this plugin (original_mail_*.eml) never match, and its "Original-Mail" link keeps working either way. Offered only while redmine_contacts_helpdesk is installed.

Changed

  • The EML repair works per project, like the contact import. The settings button now opens a project list showing, per project, how many mails can be attached to the issue and — with RedmineUP installed — how many can be given back to it. Nothing is preselected; previously the repair always ran over every project. Both directions run as background jobs with progress.

Fixed

  • Unrepairable legacy attachments no longer keep the repair button visible. Mails whose redmine_contacts_helpdesk ticket no longer exists were counted as misplaced, so the button stayed on the settings page after every repair while doing nothing for them.

Upgrade notes

No migration runs in this release.

If you already ran the EML repair and a project still works with the RedmineUP helpdesk: open Manage EML attachments in the plugin settings, tick only that project and choose Give back to RedmineUP. In later repairs, leave such projects out.

The selection page counts restorable mails with one grouped scan over all legacy tickets; on a large dataset it takes several seconds to load.

0.16.0 (2026-09-25)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Changed

  • The legacy contact import and the EML attachment repair now run in the background. Both buttons in the plugin settings (shown when the database still holds the tables of the RedmineUP plugins redmine_contacts / redmine_contacts_helpdesk) used to do all their work inside the admin's request. On a real dataset that takes far longer than a browser or reverse proxy waits — about 17 minutes for 26,000 contacts and 186,000 tickets — so the page timed out while the run carried on, or was cut off, unseen. The buttons now queue a background job and open a status page that shows the current phase, a progress bar and, once finished, the same result summary as before. The page can be left; the settings page links back to the live or most recent run.
  • Only one run at a time, enforced by the database: a second click, or a second admin, lands on the live run's status page instead of starting a competing one.
  • A run that reports no progress for an hour — its worker was restarted, for example — is shown as interrupted and simply started again; the import is idempotent. Should the old worker still be alive, it stops before its next row instead of writing alongside the new run.

Upgrade notes

Migration 060 creates the table helpdesk_legacy_import_runs; run the migration command as usual. Nothing else changes for installations without legacy data — the section only appears when those tables exist.

The job runs through Redmine's ActiveJob queue. With the default in-process adapter a run lives in the Redmine process that received the click, so restarting Redmine during a run interrupts it; the status page then reports it as interrupted after an hour, and starting it again is safe.

0.15.0 (2026-09-24)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • The knowledge base can rerank its hits with a cross-encoder. Vector search alone is a bi-encoder: it ranks on whole-text proximity, so a ticket that merely shares vocabulary with the query could outrank the one describing the same fault. That cost in both directions — the answer draft refuses without a hit above its threshold, so weak ranking became "no matching knowledge-base entry" for tickets that did have an answer, while a hit clearing the bar for the wrong reason became a customer-facing draft grounded in an unrelated ticket. With a reranker configured, the vector store is asked for a shortlist of kb_rerank_candidates entries (default 20) instead of Top-K, a cross-encoder re-scores that shortlist, and the best Top-K above kb_rerank_min_score are kept. It applies to both places the knowledge base is used: the proposals shown to agents and the customer-facing answer draft. Switched off by default.
  • Only the problem text of an entry is re-scored — the same text that was embedded. Passing the solution as well pulled up entries whose fix happened to share the words of the query while the fault was a different one, which is precisely the mistake the stage exists to prevent.
  • Endpoint and API key fall back to the embeddings configuration when left empty. With most providers the embedding and the reranker model sit on the same base URL, so switching the feature on is usually the only thing to set. Reranker calls appear in the AI statistics as kb_rerank.
  • Rerank scores are normalised to 0..1. The raw output of a cross-encoder is a logit and not every runtime squashes it — one measured provider returns values from +5.97 (identical text) down to -10.99 (unrelated). A response carrying anything outside 0..1 is put through a sigmoid, so the minimum score is always a 0..1 value; the transform is monotonic, so the ranking is untouched.

Changed

  • While reranking is on, the relevance thresholds are read against the rerank score. Cosine similarity and cross-encoder relevance are different scales. kb_rerank_min_score replaces kb_min_score as the gate, and ai_answer_min_score — the stricter, customer-facing bar of the answer draft — is applied to the rerank score instead of the similarity. Scores stored on knowledge-base proposals and shown in the ticket sidebar follow the same change.
  • The connect phase of every AI provider call is now bounded by that call's own time budget rather than by a fixed 15 seconds. A host that accepts no connection spends its time connecting, not reading, so a call given five seconds could previously block for twenty — long enough to outlive the lease that stops a second answer draft starting for the same ticket while the first is still running.

Fixed

  • The minimum score for reranking is parsed strictly. A typo in the field would otherwise have been read as 0 — the value that removes the only threshold on the proposals and lets every candidate through — and a value such as 50% as 50, which rejects everything. Anything that is not a plain 0..1 number now falls back to the shipped default.

Upgrade notes

No migration runs for this release.

Nothing changes for an existing installation until the reranker is switched on: it is off by default, and with it off retrieval behaves exactly as in 0.14.0.

After switching it on, expect to retune two thresholds. kb_rerank_min_score (default 0.2) replaces kb_min_score for the agent-facing proposals, and ai_answer_min_score (default 0.65) is applied to the rerank score rather than to the similarity — under a cross-encoder that value admits only a near-exact match, so the answer draft will refuse more often than before, and will be better grounded when it does produce one. Both scales are documented in the README.

Data protection: with the reranker on, every search additionally sends the current ticket text together with the problem texts of the candidate entries — content from other tickets of the same project — to the reranker provider in a single request. Nothing is stored there, but it is a second recipient, and a separate one when the reranker endpoint is set to something other than the embeddings endpoint. For a fully on-premise flow, use self-hosted endpoints for embeddings and reranking.

If the reranker fails or times out, the search falls back to the similarity ranking and the similarity thresholds, so a broken or unreachable reranker costs the ordering, never the search itself.

0.14.0 (2026-09-23)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Changed

  • The customer reply is now one clearly marked block in the ticket form. A reply is written in Redmine's ordinary note field — the same field, looking exactly the same, whether the text stays internal or is mailed to a customer. The mail-specific part sat above it as a separate panel, and the signature preview sat inside that panel, above the text it is appended to. Recipients, note field and signature preview are now a single block: continuous background, the signature preview after the body it belongs to, and — while the reply is switched on — a diagonal warning border around the whole thing. The border appears only while a reply is armed; an internal note looks exactly as it did before, because a warning that is always on stops being read.
  • Both colours are configurable under Administration → Plugins → Redmine expert Helpdesk and can be overridden per project; leaving a project field empty inherits the central value. Each field offers a colour picker next to the hex input and a live sample of the block, so the colours can be chosen without saving and opening a ticket to see the result. The hex field is what is submitted, because an empty value has to keep meaning "inherit" and a colour input always holds a colour. Values are validated as hex colours and anything else falls back to the default — they are written into a stylesheet, where an unchecked string would not be a colour but a style injection.

Added

  • The attachment-block guards are now part of the project settings REST API. blacklist_types and blacklist_max_kb — which file types, and up to what size, the Block button is offered for — could be set in the web forms but were missing from the endpoint that carries every other per-project setting, so an instance configured over the API could not reach them. Both are readable and writable now, and an empty value clears the project override and inherits the central setting, exactly as in the form.

Fixed

  • blacklist_max_kb is parsed strictly rather than coerced, and is bounded by its column. A typo would otherwise have become 0 — precisely the value that switches the size guard off and would have offered the Block button on every file in the project — and a value beyond the integer column would have failed while saving instead of being reported as a validation error. Both the web form and the REST endpoint are covered by the same rule, because it lives in the model.

Upgrade notes

One migration runs on upgrade (059). It adds the two colour columns to the per-project settings; they are empty, so every project inherits the central colours and nothing changes for an existing installation beyond the new look of the reply block.

No new permission and no new setting is required to keep working as before. The reply block is marked for everyone who already has Send customer replies; there is nothing to switch on.

The colours must be hex (#rgb or #rrggbb). A value that is not one is rejected in the forms and over the API, and falls back to the default where an older value was already stored.

0.13.0 (2026-09-23)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Signature logos and icons can be blocked once instead of deleted forever. A business mail carries the sender's signature logo, a row of social-media icons and a tracking pixel, and Redmine's mail handler stores every single one as a real attachment. Ten mails in a thread therefore leave thirty files on the ticket, none of them evidence, and the one screenshot the customer actually sent is buried among them. Every attachment row on a ticket now carries a Block button. One click blocks that file for the project and deletes every copy already attached to a ticket of that project — the confirmation names the real number first ("3 attachments are deleted"), which the server counts before it asks. From then on, incoming mail carrying the same file arrives without it.
  • The match is the file content, not the file name. Outlook numbers embedded images per mail, so image001.png is whatever that sender's client happened to put first: blocking by name would hit a different customer's logo on the next ticket and, sooner or later, a screenshot. Each entry is the SHA-256 of the bytes, so a blocked file is blocked exactly when it is byte-for-byte the same one. A logo that a mail client re-encodes for every mail is deliberately not caught — a filter that guesses may lose a logo, never a screenshot.
  • Deleting the file is only half of it. By the time an agent sees the ticket, the plugin has turned the mail's inline-image references into image markup pointing at the attachment, so removing the file alone would trade a signature logo for a broken image. Every removal therefore also strips the markup that named it, and writes the text past the callbacks: the cleanup produces no journal entry, no "edited" marker and no notification for text the customer wrote.
  • The button is not offered on everything. Blocking deletes a file from every ticket in the project, so it must not sit next to the evidence. Two settings decide which attachments an agent is shown the button for at all: an allow list of file types (extensions such as gif, or MIME types such as image/gif and image/*; * allows all) and an upper size limit, defaulting to the image formats logos arrive in and 100 KB. An .eml, a .msg, a PDF or a log therefore never gets the button, and neither does any image above the limit — which is where screenshots normally sit. The guards bound what the button can reach; they do not classify content, so a small screenshot saved as a PNG is still eligible and the confirmation is the last check. Both are set centrally and can be overridden per project, and both are enforced again on the server, so a page left open since the settings changed cannot bypass them.
  • Blocked files are listed under Project settings → expert Helpdesk → Blocked attachments with the name and size of the blocked copy, who blocked it, and how often the filter has dropped a file since — so an entry that never earns its keep is visible and can be removed again. Blocking requires the Manage helpdesk permission; the project is always taken from the ticket the attachment hangs on, never from a parameter.

Fixed

  • The embedded images of a mail no longer all show the same picture. Outlook names every embedded image image.png, so a mail whose signature carries a logo, a phone icon, a mail icon and four social icons arrives as seven parts with seven content IDs and a single file name between them. Two things then went wrong at once. The parts were matched to the stored attachments by file name, so all of them were handed the same file; and the markup written into the ticket was the bare name, which Redmine does not treat as a reference but as a lookup — it resolves the name against every attachment of the ticket and returns the newest match. A ticket that showed nine different pictures in the mail showed the same screenshot nine times over. Parts are now matched by their content — the file name narrows the field, then the part's byte count and, if that still ties, its SHA-256 pick the right file, and each part claims its attachment so two parts can never take the same one — and the markup names the download path, which carries the attachment id and therefore means exactly one file.
  • The same fault in the outgoing direction. When an agent quoted such a mail in a reply, the outgoing image resolution also matched on the file name, so the first candidate claimed every reference and the customer received one picture as many times as the mail had images. References carrying a download path are now resolved by the attachment id; a bare file name still matches a freshly pasted upload, which has no id yet.
  • A slower failure is fixed with it, and it did not need duplicate names: a reply's attachments are appended to the ticket, and the description is rendered against those, so the next mail carrying an image.png quietly took over the markers of the first — a ticket's description could start showing a later reply's pictures. Sub-URI installations are handled too: the image path now includes the application's URL root, which the previous fallback omitted.

Upgrade notes

Two migrations run on upgrade (057–058). They create the table of blocked attachment contents and add the two guard settings to the per-project settings; nothing is blocked until an agent blocks something, so no existing ticket, mailbox or attachment is touched by the upgrade itself.

No new permission is introduced. The Block button follows Manage helpdesk, the permission that already covers mailboxes, rules and answer templates.

The defaults for the button guards are the image formats signature logos arrive in (png, gif, jpg, jpeg, bmp, webp, tif, tiff, ico) and 100 KB. Both can be changed under Administration → Plugins → Redmine expert Helpdesk and overridden per project; leave a project field empty to inherit the central value.

The inline-image fix applies to mail received from this version on. Text written earlier keeps whatever its markers resolved to at the time; tickets already showing the wrong picture are not rewritten, because repairing them would mean bulk-editing description and note text on existing tickets.

0.12.0 (2026-09-17)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • The AI can now draft the answer to the customer, not just the summary for the agent. Every AI feature so far pointed inward: the summary condenses the customer's mail for the agent, the completeness check judges it, and the knowledge base files a repair note written for a colleague. The one customer-facing feature, answer templates, is static and knows nothing about the ticket in front of you — so agents kept writing every real answer by hand, even when the identical case had been solved and filed last month. A third button, AI answer, now sits next to Quote and Templates in the toolbar of the note field. It drafts a reply addressed to the customer for this ticket — what they should do, in order — grounded in that project's own knowledge base, and appends it to the note field, which is also the body of the outgoing mail. Four variants: a standard answer, step-by-step instructions, a short version, and one that asks for the missing details. Nothing is ever sent automatically: the draft is inserted for the agent to read, edit and send through the normal reply form.
  • Without a matching knowledge base entry, no draft is produced. An ungrounded reply is exactly the artefact that invents a repair date, so the three variants that propose a fix refuse and name the answer templates as the alternative. Ask for missing details is the exception — it proposes nothing, needs no grounding, and therefore works from the first day against an empty knowledge base. How similar a match has to be is configurable centrally and per project (ai_answer_min_score, default 0.65), deliberately separate from the knowledge base's own minimum score: tightening what reaches customers must not also suppress the proposals agents see inside a summary. When a draft is refused, the message names the near miss — "best match 86%, 95% required" — so the threshold can be calibrated from real cases instead of guessed.
  • Internal information stays internal. Unlike the knowledge base extractor, whose output never leaves the project, the drafter is never given private notes or attachments, and the knowledge base cases it works from are passed without their ticket numbers — a foreign ticket number in a customer mail discloses the existence, and by inference the content, of another customer's ticket. The prompt additionally forbids prices, dates, deadlines, supplier and colleague names, internal tooling, and any promise of repair, replacement, goodwill or warranty that is not already in the ticket. Which tickets a draft was built on is shown to the agent in the toolbar status line, never written into the mail.
  • No double salutation, and nothing inserted silently. The mailbox's reply header and footer are rendered and handed to the model, which is told they will be placed around its text, so the customer does not receive two greetings or two signatures. Inserted text is marked by a warning bar above the note field, and sending or saving a draft that has not been edited asks for confirmation first. Any note that came from a draft is kept out of knowledge base ingestion, so the model never re-reads its own prose as a solution.

Fixed

  • The plugin's secrets no longer appear in the request log. Every secret the plugin stores was written to the Rails parameter log in clear text whenever an administrator saved the settings form: the AI provider key, the embedding key, the Qdrant key, the keys securing the mail-fetch and SLA endpoints — and, on Redmine 5.1 and 6.0, the Azure Client Secret as well, which is only covered by the application's own filter list on 6.1 and 7.0. On a container installation that log goes wherever standard output goes, with a retention that has nothing to do with the ticket data. The plugin now registers its own filters instead of inheriting any, so the values appear as [FILTERED] on every supported version, while ordinary settings such as the model name stay readable.

Upgrade notes

Five migrations run on upgrade (052–056). They add the per-project settings for the answer drafts, record which agent triggered an AI call, and mark replies and notes that came from a draft. Nothing changes behaviour on upgrade: the feature is off centrally and off in every project, so no ticket, mailbox or existing AI setting is affected until it is switched on twice — once under Administration → Plugins → Redmine expert Helpdesk, then per project under Settings → expert Helpdesk.

The button additionally requires a linked customer on the ticket, a configured AI provider, and — for every variant except ask for missing details — a knowledge base with a sufficiently similar entry. No new permission is introduced: the button follows Send helpdesk reply, the same permission that gates the reply form.

Unlike every other AI call in the plugin this one runs synchronously, because an agent is waiting for it. It therefore has its own timeout (default 20 seconds) that should stay well below the timeout of any reverse proxy in front of Redmine, its own output budget, and a throttle per user and per ticket.

If your logs were retained, rotate the affected keys. The fix prevents new entries; it cannot remove those already written.

0.11.0 (2026-09-16)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • The original recipients of a customer mail, one click away in the reply form. A customer mail is often addressed to more than one person — a colleague, a shared team address, an account manager in Cc — but the reply form only ever prefilled the customer in To and left CC empty. Putting the rest of the thread back on the answer meant opening the original mail and retyping the addresses, so in practice they were dropped: the reply reached one person while nobody else on the original thread heard it. Behind To and CC there is now an original recipients button, listing the addresses the mail that opened the ticket was sent to, each with a checkbox — ticking one adds it to the field, unticking takes it back out. To offers the original To: and CC the original Cc:, so the customer's own addressing is preserved rather than guessed at. The helpdesk's own mailbox address and the customer itself are filtered out, an address that appeared in both headers is offered once, and the button is absent altogether when nothing is left to offer. BCC has no button: an inbound Bcc is stripped by the sending server and was never available to begin with.

Upgrade notes

No migration and no new setting. The recipients of every incoming mail have been recorded in the message log all along, so the button works on existing tickets as soon as the plugin directory is replaced — including tickets that arrived long before this release.

Two details worth knowing. The addresses are filtered against the mailbox that actually received the mail, read from the message itself rather than from the mailbox the reply happens to go out through; a ticket whose original mailbox has since been disabled therefore still has the helpdesk's own address filtered out instead of offered back as a recipient. And an address that the agent has already put in the field by hand is recognised even in its "Name" <address> form, so ticking it in the list cannot add the same person twice.

0.10.0 (2026-09-14)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • A separate status for a reply to an open ticket. A mailbox had one status for inbound customer replies, and it only ever applied to a closed ticket — that assumes the ticket is closed while the helpdesk waits for the customer. Where a ticket instead stays open under a status such as waiting for customer or on hold, the reply had no way to move it on. Helpdesk → Mailbox → Status on customer reply now offers two fields: Status (closed issue), the previous reopening status, and Status (open issue), applied when the mail lands on a ticket that is still open. The ticket's state when the mail arrives decides which one is used. Both are optional and independent of each other. Like the reopening, the change is written onto the same history entry that carries the customer's reply, so the ticket history shows one entry and nobody is notified twice. Readable and writable over the REST API as open_reply_status_id on helpdesk_mailbox.

Upgrade notes

Migration 051 adds one column, open_reply_status_id on helpdesk_mailboxes. Run the usual redmine:plugins:migrate after replacing the plugin directory.

No behaviour changes without configuration: the new field is empty on every existing mailbox, and an empty field leaves the status of an open ticket alone, exactly as before. The reopening status and its age limit are untouched.

One detail worth knowing if you read the ticket statistics: only a closed ticket that ends up in an open status counts as a reopening, both for the Reopened figures and for the Reopened label on the awaiting-response flag. A status change on an already open ticket is an ordinary reply, and so is a reopening status that is itself closed — it is applied, the ticket simply never becomes open.

0.9.3 (2026-09-13)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • The "Reply" button did not preselect the status or the assignee. Ticking Send as email to customer by hand applied the status configured under Project settings → expert Helpdesk → Reply settings and, if enabled, assigned the ticket to you. The Reply button above the ticket — which ticks that very box for you — did not: it set the checkbox from script, which fires no change event, and the preselection is what hangs off that event. The mail fields opened, but the ticket kept its old status and stayed unassigned, so an agent replying through the button silently lost both settings. Both routes now go through the same handler and end in the same place.

Upgrade notes

No migrations, no configuration changes and no new settings. The behaviour is what Reply settings already promised — if you configured a reply status or Assign ticket to me when replying and only ever saw it apply when you ticked the checkbox yourself, it now applies via the button too. Replace the plugin directory and restart.

0.9.2 (2026-09-13)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • The documentation said the plugin needs no gems. It does. Both READMEs and this listing stated "no additional gems — Ruby standard library only" in six places, which stopped being true when PluginGemfile gained base64 and pdf-reader. base64 is required: it is no longer a Ruby default gem as of Ruby 4.0, and the official redmine:7 Docker image ships Ruby 4.0, so the plugin does not load there without it. pdf-reader is optional and only extracts text from PDF attachments for the AI summaries; the call is guarded, so a missing gem just means those attachments are skipped. Anyone sizing up a deployment from the README was told they needed nothing and would then have hit a missing base64 on Redmine 7.
  • The installation steps never ran bundle install. They went from unzipping the archive straight to the migration, which fails on Redmine 7 for exactly the reason above. Redmine picks a plugin's PluginGemfile up on its own, so there is nothing to add to your deployment's own Gemfile — but the bundle has to be installed before the migration runs.
  • The release workflow accepted tags that are not versions. Its semver check let a fourth numeric component through (v1.2.3.4), accepted empty identifiers (1.2.3-a..b) and allowed leading zeroes (v01.2.3, v1.2.3-01). Any of those would have been packaged and published as a release.

Upgrade notes

No migrations, no configuration changes and no behaviour changes — this release is documentation and one workflow. Replace the plugin directory and restart. If you are installing rather than upgrading, note the corrected step order: unzip, bundle install, rake redmine:plugins:migrate, restart.

0.9.1 (2026-09-11)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Changed

  • "Status when replying" is a preselection now, not a rule. The status configured under Project settings → expert Helpdesk → Reply settings used to be written into the ticket the moment the mail went out, one step before the form was saved — every reply landed on it, and the agent never saw the decision, let alone got to change it. It is now filled into the ticket's own status field as soon as Send as email to customer is ticked, while the reply is still being written. A status the agent picks instead is kept; unticking the box restores the original one only while they have not picked anything themselves. That difference matters for the answer that ends in a question: a ticket waiting on the customer belongs out of the list of open tickets, in a status such as Feedback closed, for however many weeks the answer takes — or forever, if it never comes. Assign ticket to me when replying was moved to the same moment and is just as reversible.

Fixed

  • A reply no longer blanks the status field when the workflow forbids the configured status. The status set in the reply settings is not filtered by the workflow, so it is easy to configure one the agent's role may not move the ticket to. The old code assigned it anyway; with no matching entry in the status list the field simply went empty, and the reply was saved with whatever Redmine made of that. The configured status is now skipped when the workflow does not offer it, and the ticket keeps the status it had.
  • The preselection survives editing other ticket fields. Changing status, tracker or priority makes Redmine re-render the attribute block of the form. The reply block held on to the old fields across that and wrote into elements that were no longer on the page, so the preselection quietly stopped working for the rest of the edit.

Upgrade notes

No migration and no configuration change. The setting, its database column and its REST API field are unchanged, and existing configurations keep working — what changed is when and how the value is applied: it is offered to the agent instead of being written for them. Anything reading reply_status_id over the REST API should note that it now describes a preselection in the ticket form, not a value the plugin applies after a reply; API.md has been updated accordingly.

0.9.0 (2026-09-10)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • "Never ask this customer for more information" per customer. The completeness check already had a per-project list of senders it must never follow up on. That list needs an administrator and an address known in advance — but the Veeam job report, the monitoring alert or the cron mail is usually recognised by whoever is looking at the ticket it just produced. There is now a flag on the customer itself, and the ticket header carries it: one click on never ask next to the customer name, and you are back on the ticket you were reading. The same flag is a checkbox on the customer form and writable through the REST API as info_request_opt_out on helpdesk_contact. Flagged customers are marked no follow-up in the ticket header and in the customer list, with ask again next to it — a follow-up that never went out therefore never looks like a broken check. Tickets from those senders are still created and still evaluated; only the mail to the customer is suppressed, in the rule-based and the AI mode alike. Setting the flag requires the Manage customers permission; the marker is visible to everyone.

Fixed

  • "Never ask these senders" is editable in the AI mode again. The field sat in the rule-based part of the project settings, which the mode switch hides as soon as the check runs on AI. The list still applied — the check reads it in both modes — it was simply invisible and could not be maintained there, which is worse than a missing field: entries made in the rule-based mode kept working with nothing on screen to say so. The field has moved next to the follow-up templates, which both modes share.

Upgrade notes

One migration runs on upgrade (050). It adds info_request_opt_out to helpdesk_contacts with the default false, so no customer changes behaviour and no configuration is required: every customer keeps being asked exactly as before until somebody flags one.

The never ask link in the ticket header is shown to members with the Manage customers permission, and only while the completeness check is actually running in that project — no new permission is introduced.

0.8.1 (2026-09-09)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Sortable customer list and statistics tables. The project's customer list sorts by every column (name, email, company, phone, ticket count, last ticket) through the column headers, server-side and across pages and searches. The three tables of the ticket statistics (time in status, per agent, per customer) sort in the browser by clicking a header or pressing Enter/Space on it; durations and percentages sort as numbers, empty cells last.

Upgrade notes

No migration and no configuration change.

0.8.0 (2026-09-09)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • "Ticket statistics" tab per project. A third statistics page next to SLA and AI statistics, available for every helpdesk project — with or without SLA — and unlocked by the new member permission View ticket statistics (meant for a "helpdesk manager" role; without it the tab is hidden and the page answers 403). It covers the helpdesk tickets created in the selected period: ticket volume, average and median first response and resolution, time in status (only statuses that actually occur, completed stays plus the tickets currently sitting there), reopens, tickets awaiting a response, conversation length (incoming mails per ticket, agent replies, automatic mails, share of tickets solved with a single reply), a per-agent table and a per-customer table, plus busiest hours and weekdays. Durations are calendar time; with SLA enabled a time-basis switch shows the same figures in the project's business hours. Charts use the bundled Chart.js, no CDN.

Changed

  • The first reaction is recorded for every helpdesk ticket, not only under SLA. A public agent note or a customer reply mail sets the first-response timestamp regardless of the project's SLA switch, together with the acting user; only the business-minute figure, the deadlines, the breach mails and the SLA chips remain SLA features. Without SLA no ticket-info row is created for this — the timestamp only lands on tickets that already are helpdesk tickets.

Fixed

  • SLA statistics no longer count reopened tickets as closed. Redmine keeps the closing timestamp after a reopen; the page now derives "closed" from the current status.

Upgrade notes

  • Run the plugin migrations (bundle exec rake redmine:plugins:migrate NAME=redmine_expert_helpdesk RAILS_ENV=production): migration 049 adds the column for the first-response user and backfills it from the note written at the recorded moment where one exists.
  • Grant the new permission View ticket statistics to the roles that should see the tab — it is not granted to anyone by default.
  • No backfill of the first-response timestamp itself: tickets of projects without SLA that were answered before this version show no first-response figure in the statistics.

0.7.2 (2026-09-08)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • The settings REST API writes the completeness-check fields. PUT /projects/:id/helpdesk/settings accepted every info_request_* key documented in the API reference without error and then ignored it — the response echoed the unchanged value and the caller only noticed by reading it back. All of them are applied now, partially like the other fields: mode, rule values, expected terms, sender list, threshold, prompt mode and prompt, follow-up subject and body, note visibility and the status after a follow-up. Two rules the project settings form silently corrects answer 422 on the API instead: a threshold below 1, and a status that is closed or does not exist. A closed status is refused because an automatic follow-up must never close a ticket — every SLA reader treats a closed ticket as both reaction-done and solution-done.

Upgrade notes

No migration and no behaviour change for the web UI. Scripts that already send info_request_* keys and relied on them being ignored will now change the project's settings — check them before upgrading.

0.7.1 (2026-09-08)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • The completeness check reads the subject line. Both modes evaluated the mail body alone, so a ticket opened as "Drucker HP 4050 im EG druckt nicht" with a one-line body was asked which device was affected — the customer had already said so in the subject. The subject now counts towards the rule-based length and expected-term checks and is shown to the AI model as the first line of its input (Betreff: …), with the default prompt told to treat it as information. It is joined after the quoted-history stripping, so a subject that looks like a forwarded header is never cut, and in AI mode it is added after the body truncation, so a long thread cannot push it out.
  • The AI summary and the knowledge base read the subject line too. The summary job built its model input from the body alone, so an informative subject over a one-line body was skipped as "too short" or summarised without the device and error the subject named; the knowledge extractor embedded the same tickets without it. Both now start the text with Betreff: …, the subject counts towards the minimum input length, and both default prompts explain the line. The archived .eml is also parsed only once per summary run instead of up to three times.

Upgrade notes

No migration. Two behaviour changes worth knowing:

  • Rule-based completeness thresholds (minimum characters / words, expected terms) are now met by subject and body together. A project that tuned these values to the body alone may see fewer follow-ups; lower the values if that is unwanted.
  • Minimum input characters for the AI summary now include the subject, so some mails that were previously skipped as too short are summarised.

0.7.0 (2026-09-03)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • The AI summary no longer pays vision tokens for signature logos. With Attachments → Images enabled, every image of a mail was sent to the model — including the logos, social icons and tracking pixels that hang off nearly every business mail. With only four image slots per request, a three-image signature could push the actual screenshot out of the batch entirely. Images are now filtered first: below the new per-project minimum image size (default 15 KB, 0 disables it), below 64x64 pixels, or embedded inline and byte-identical to an image on other tickets — a signature graphic, whereas a pasted screenshot is inline but unique. Every stage keeps the image when its signal is unavailable (unknown size, unreadable header, unknown format), so the filter can only lose the logo, never the screenshot. Image dimensions are read from the file header, so no additional gem and no ImageMagick is required.
  • The completeness check no longer asks robots for more information. Veeam job reports, cron mails and monitoring alerts are legitimate tickets, but nobody behind them can answer a follow-up — the request bounced or, with a chatty mailer, looped. Two gates now run before the rule evaluation, so the AI mode spends no token on them either: the new per-project "Never ask these senders" list (one entry per line: full address, bare domain or "@domain"), and automatic detection of mail that marks itself machine-generated in its headers (Auto-Submitted, Precedence: bulk|list|junk, X-Auto-Response-Suppress, X-MS-Exchange-Generated-Message-Source and the X-Autoreply/X-Autorespond/X-Autoresponder headers). Delivery failures (NDR) are deliberately excluded — a bounce carries the same headers but is something the plugin has to act on.

Changed

  • A two-line mail with a signature logo is summarized no more. The "too short to be worth an AI call" short-circuit only fires when no image is attached, so any signature logo defeated it and sent the mail to the provider anyway. With the logos filtered out beforehand, the skip works as documented. Filtered images are reported in the AI debug log.
  • Machine-mail detection and the image size test each live in one place now. The header list was previously known only to the incoming-mail processor, which uses it to drop auto-replies before they become tickets; the completeness check needs the same answer to "did a human write this" for the opposite reason. The image size floor was previously known only to the completeness check. Both are shared now, so the features cannot drift apart.

Upgrade notes

Two migrations run on upgrade (047, 048). Both add a per-project setting with a safe default, so no configuration is required:

  • Minimum image size defaults to 15 KB. If a project relies on the AI summary seeing very small images, set the value to 0 in Project → Settings → expert Helpdesk to switch the floor off. Images smaller than 64x64 pixels are skipped regardless of this value.
  • "Never ask these senders" starts empty. The header-based detection is active immediately, so tickets from mailers that identify themselves as automated stop receiving follow-ups even without an entry.

Both settings are exposed through the REST API (ai_min_image_kb, info_request_sender_blacklist).

0.6.0 (2026-09-02)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Incoming mail is checked for sufficient information, and the customer can be asked for the rest automatically. A ticket that arrives as "the printer is broken" — no screenshot, no error message, no system name — costs an agent the first cycle just to write "please tell us more", and that cycle runs against the SLA. The plugin now evaluates the first mail of a new ticket and, when it does not carry enough to start working, sends the customer a templated follow-up listing what is missing, records a journal note, and can move the ticket to a "waiting for customer" status. Off by default.
  • Two modes, chosen per project. Rule-based works without AI: minimum length in characters and/or words, "an attachment is required", a minimum image size, a list of expected terms, and a threshold saying how many of those rules must fail before the customer is asked. AI-powered lets the model return a verdict plus the concrete details it found missing, which go straight into the follow-up mail; it uses the AI provider already configured for the summaries and appears in the AI statistics as its own completeness request type.
  • The AI prompt asks for the right kind of evidence. A screenshot when the problem is software (an application, a web portal, an error dialog) and a photo when it is hardware (a device, printer, till, display, cabling — ideally showing the type plate or the display). When it cannot tell which of the two is meant, it asks for the affected system instead of guessing. The model is also told which files are already attached, so it never asks for a screenshot the customer has already sent.
  • Quoted history and signature logos do not count as content. Quoted conversations, forward headers and signatures are stripped before anything is measured, so a two-word reply under a long thread does not pass as a detailed report. Images below a configurable size (15 KB by default) are ignored as evidence, because signature logos and tracking pixels hang off nearly every mail and would otherwise satisfy "an attachment is required" every single time. That size floor applies to images only — a small log file or PDF is evidence of a different kind and still counts.
  • Editable texts. Subject and body of the follow-up are templates using the usual macros plus {{missing_info}}, which inserts the rendered list of missing details. They are configured centrally and can be overridden per project, as can the check prompt (inherit, extend or replace, exactly like the summary prompt). The journal note recording the follow-up is public by default — the customer received the same text by mail — and can be switched to an internal note per project.

Fixed

  • An automatic note can no longer clear the "awaiting response" flag. The plugin writes several notes by itself — the autoresponder, the phishing warning and the new follow-up — all authored by the anonymous user. Clearing the flag was gated on the author holding Send helpdesk reply, which is normally enough, but a project that granted that permission to the Anonymous role turned every one of those notes into "an agent has answered". The phishing note was the sharp edge: it runs moments after the flag is set during the same mail fetch, so a waiting ticket disappeared from the queue immediately. Anonymous is now excluded outright; real agent replies are unaffected.

Upgrade notes

Four migrations run on upgrade (043–046), all of them adding optional columns to the helpdesk project settings and ticket info tables. Nothing needs to be reconfigured and no existing behaviour changes.

The completeness check is off after the upgrade and stays off until you switch it on twice: once centrally under Administration → Plugins → Redmine expert Helpdesk → Enable completeness check, and once per project under Project settings → expert Helpdesk, where the mode starts at Off. While the central switch is off, no project runs the check regardless of how it is configured.

It is deliberately conservative, because a wrong follow-up annoys a customer who wrote everything correctly:

  • Only the first mail of a new ticket is ever checked. A reply in a running conversation never triggers a follow-up.
  • Each ticket is asked at most once. The follow-up is claimed before the mail is sent, so a repeated mail fetch, a reopened ticket, a retried job or a manual re-run cannot mail the same customer twice.
  • The AI mode fails closed: an unreadable model answer, an API error, or a verdict of "incomplete" without a single reason all count as complete, so no mail goes out.
  • The check runs after the mail has been processed and can never break the mail fetch.

The follow-up does not touch the SLA. The note does not stop the reaction clock, and the optional status change can never close the ticket — every SLA evaluation treats a closed ticket as both the reaction and the solution, so closing here would mark both clocks met before the customer had even answered. Closed statuses are therefore not offered in the status selector. If your "waiting for customer" status is flagged as closed in Redmine, pick an open status for this setting or leave it empty.

The AI mode sends the mail body and the attachment names to the configured AI provider, exactly like the existing summaries. It needs no additional configuration beyond the AI settings that are already there, and the rule-based mode needs no AI at all.

0.5.1 (2026-09-02)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Reply templates, footers and autoresponders understand far more macros. Besides {{issue.id}}, {{issue.subject}} and {{issue.url}} a template can now use the ticket's status, priority, tracker, author, assignee, category, target version, start and due date, creation and update date, progress, description and parent id — 26 macros in total. The short forms of the original seven ({{ticket_id}}, {{contact_name}} and friends) keep working unchanged.
  • Macros for the replying agent. {{user.firstname}}, {{user.lastname}}, {{user.login}} and {{user.mail}} join the existing {{user.name}}, so a signature can be built from the agent actually sending the mail. {{project.identifier}} was added next to {{project.name}}.
  • Issue custom fields as macros, enabled per field. An administrator picks the fields under Administration → Plugins → expert Helpdesk → Custom fields as macros; only those expand. Each enabled field is addressable either by id ({{issue.cf.42}}, which survives renaming the field) or by its name ({{issue.cf.vertragsnummer}}, easier to read in a template). Redmine's own field visibility still applies on top: a field the replying agent may not see renders empty, so an internal field cannot reach a customer through a shared template.
  • Optional sender override for mailboxes that send through Redmine's SMTP configuration. A mailbox whose reply transport is SMTP (Redmine default) can send under an address other than the mailbox address. The case this serves is a distribution list that was turned into a helpdesk mailbox: the list address still exists and the mailbox is its only member, so mail should go out under the list address. Setting Reply-To to the mailbox address is a separate option and off by default, because for that setup the list address already delivers back to the mailbox and a Reply-To would only expose the internal address. The override is ignored for the Graph and own-SMTP transports, which authenticate as the mailbox and reject a foreign sender.

Fixed

  • The macro list no longer drifts apart from what the renderer understands. The clickable macro chips read their list from the renderer itself instead of a separately maintained copy, and offer the common macros directly with the rest behind a "more macros" expander.

Upgrade notes

Two migrations run on upgrade (smtp_from_address and smtp_reply_to_mailbox on the mailboxes table). Both are optional columns with empty defaults, so every existing mailbox keeps sending exactly as before and nothing needs to be reconfigured.

Custom field macros are off until an administrator enables individual fields — an upgrade alone exposes no custom field in any template. On an installation that has saved the plugin settings before, the new setting starts out empty, which means no custom field macros.

The sender override only applies to the SMTP (Redmine default) transport. If you use it, the relay configured in Redmine's configuration.yml must be permitted to send for that address; the plugin checks the syntax only, so a refusal by your mail infrastructure (SPF/DMARC) shows up when the mail is sent, not when the mailbox is saved.

0.5.0 (2026-09-01)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • The issue list can show a "Customer email" column. The existing Customer column renders the contact's display name, which for many senders is long or unclear — a "Firstname Lastname | Company | Department" display name tells an agent less at a glance than the plain address. The new optional column shows only the contact's email address and is sortable like its sibling. The existing Customer filter matches name or email, so it covers the new column too and no second filter is needed. The value is deliberately plain text, not a mailto: link — links in dense lists get clicked by accident.

Fixed

  • The Customer column and filter now also work for agent-created tickets with an assigned customer. Both used to read the sender of the first incoming mail only, but a ticket an agent creates and assigns a customer to (initial-mail flow) has no incoming mail until the customer replies — its customer was invisible in the list and unfindable through the filter, although the ticket sidebar showed it. Columns, sorting and filter now resolve the customer the way the rest of the plugin does: the authoritative ticket link first, the first incoming sender as fallback for legacy tickets.
  • Deleting a contact no longer leaves broken ticket links behind. The ticket-to-customer link now clears when its contact is deleted (as it always did for the message log), and the customer columns and filter skip stale links from earlier deletions instead of showing a blank customer although a valid sender is on record.

Upgrade notes

No migration and no new permission or setting. One visible change: tickets whose customer was assigned by an agent now show that customer in the Customer column and are found by the Customer filter — before this release those cells were empty and the filter missed them.

0.4.0 (2026-08-13)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • New tickets can be assigned to a fixed user or group, per project. Until now the plugin could only put a ticket on the agent who happened to answer it: a ticket arriving by mail was created unassigned, and the only way to route it anywhere was a mailbox rule matching subject or sender. Teams working out of a shared queue — 2nd level, an on-call group, a dispatcher — could not express that at all, because nothing in the plugin could address a group. Project settings → expert Helpdesk → Reply settings now carries Assign new tickets to, a dropdown of the project's assignable principals split into Users and Groups, preset to "none" so nothing changes for existing projects. It applies when incoming mail creates a ticket, and only then — replies never re-assign, so an agent's manual decision always stands.
  • Mailbox rules can assign to a group. The rule action Assign to resolved its value against the project's members and could therefore only ever find a user, which left the rules engine unable to express the same routing the new project default supports. The dropdown is built from the project's assignable principals, groups marked as such, and stores the principal id. Rules created before this release store a login and keep resolving unchanged.

Changed

  • "Assign ticket to me after reply" only takes effect while the ticket is still unassigned. The option overwrote the assignee on every single reply, so a ticket a dispatcher had just routed to 2nd level was silently taken over by the first agent who answered — and an assignee picked in the very same form was discarded on send. It now claims the ticket only when nobody holds it, which is what the option was meant to do; existing assignments, to a user or a group, are left alone.
  • Rules assigning a ticket now use the project's assignable members rather than all members. A member whose role is not assignable could previously be set as assignee by a rule, which the ticket form itself would refuse.

Upgrade notes

This release runs a migration. One column is added to the per-project settings:

bundle exec rake redmine:plugins:migrate NAME=redmine_expert_helpdesk RAILS_ENV=production

No new permission is introduced, and no existing project changes behaviour on upgrade: Assign new tickets to starts empty, which means "do not assign" — exactly what happened before.

The reply option behaves differently after the upgrade. If you relied on Assign ticket to me after reply to move a ticket to whoever answered last, it no longer does that once somebody is assigned. Clear the assignee to hand a ticket back to the pool, or set a project default so tickets start on a group and agents claim them from there.

Three settings now decide who owns a new ticket, in this order: an Assigned to: keyword in the mail, which Redmine's own MailHandler has already honoured before the plugin looks; a matching mailbox rule; and the project default. The option list comes from the same source as the ticket form, so Groups appear only while Allow issue assignment to groups is enabled under Administration → Settings → Issue tracking. A user or group that later loses its role or leaves the project is silently skipped instead of producing invalid tickets — the setting is not cleared, so restoring the membership restores the routing.

0.3.0 (2026-08-06)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Prior ticket content can be quoted into the reply with one click. Agents answering a customer were copy-pasting out of the ticket history by hand, which loses the > prefixing mail clients need to fold a quote — and silently drags along whatever happened to be selected. A Quote button now sits next to the formatting icons of the note editor with three entries: Original email quotes the ticket description (the mail that became the ticket, embedded images already resolved), Complete conversation adds every public journal note, and Email conversation adds only the notes belonging to a mail actually exchanged with the customer. Entries carry author and timestamp and are separated by a horizontal rule, so a long history stays readable while scrolling. Private notes are excluded from all three — including for agents who are allowed to read them, because the text is destined for a customer.
  • Answer templates, globally and per project. Support cases repeat, and until now every acknowledgement, follow-up question and closing text was retyped. A Templates button next to the Quote button inserts a stored answer. Global templates are maintained under Administration → Plugins → Redmine expert Helpdesk, project templates in the project settings tab expert Helpdesk; a project's own templates are offered first, so it can override a central wording by reusing its name. Templates understand the same macros as autoresponder, header/footer and subject templates, and the macros are expanded at insertion time against the ticket, its customer and the acting agent.
  • Embedded images of an incoming mail are shown in the ticket instead of the [cid:…] marker the mail client left behind, so a signature arrives as a logo rather than as [cid:image001.png@01DD2980.37ED1560]. Outlook's [cid:…], Gmail's [image: …] and <img src="cid:…"> are all recognised and rewritten to the image syntax of the configured formatting, pointing at the attachment that was just stored. The archived .eml keeps the untouched original, and markers without a matching attachment are left alone.
  • Outgoing mails carry their send time in the journal header, the same way received mails already did, so the correspondence can be followed on one time axis instead of reading the outgoing side off the journal's own timestamp — which is when the note was saved, not when the mail left.
  • The mailbox connection test can copy the whole message. Provider errors are long and the status line wraps; Copy message puts the full text on the clipboard, and where the clipboard API is unavailable — an internal Redmine over plain http is not a secure context — it selects the text instead of failing silently.

Fixed

  • Images already attached to the ticket never reached the customer. Only images an agent had just pasted or dropped into the form were sent as inline mail parts; anything already stored on the ticket stayed a relative reference that means nothing in a mail client, so the customer received an empty box. This was invisible while pasting was the only way to get an image into a reply, and became obvious once quoting made it easy to reference the pictures of the original mail.
  • -WhatIf did not stop the Entra ID writes in setup-azure-app.ps1. It was documented as a dry run and guarded the Exchange Online calls, but the Microsoft Graph ones ran regardless.

Changed

  • setup-azure-app.ps1 can add mailboxes to an existing setup instead of aborting as soon as an app registration with the given name exists. Onboarding one more project no longer means tearing the tenant setup down and rebuilding it — which would mint a new client secret and force a Redmine config change.
  • -ListRoleAssignments shows which mailboxes a scope actually covers, by asking Exchange Online to evaluate the filter rather than reading it by eye — the only way to answer that for a CustomAttribute, domain or group filter.

Upgrade notes

This release runs a migration. One table is added for the answer templates:

bundle exec rake redmine:plugins:migrate NAME=redmine_expert_helpdesk RAILS_ENV=production

No new permission is introduced. Quoting and inserting a template require the existing Send customer replies permission, the same one that governs the reply form; maintaining a project's templates requires Manage helpdesk mailboxes, and the global templates are administrators only.

Quoting deliberately never includes private notes, not even for agents holding View private notes — the result is meant for a customer. For the same reason the plugin's own bookkeeping notes (autoresponder sent, phishing links removed) stay out of the quoted conversation. Very long histories are capped so the note field stays usable, and the toolbar says so rather than truncating silently.

Embedded images can be switched off under Administration → Plugins → Redmine expert Helpdesk → Embedded images if the previous behaviour is preferred.

0.2.4 (2026-08-06)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • The "AI statistics" tab no longer appears when the AI features are switched off. It showed up in every helpdesk project for anyone holding the global View AI statistics permission, even with both the AI features and the knowledge base disabled — where it led to a permanently empty page. The tab now appears once at least one of the two is enabled, because the page reports both AI summaries and knowledge-base requests, and either one alone makes it meaningful. The page itself answers 403 while both are off, so it can no longer be reached by typing the URL.
  • Every checkbox in the plugin settings and the mailbox form shipped a duplicate DOM id. Each checkbox is preceded by a hidden field carrying its unchecked value, and both were given the same id, derived from the shared field name — so getElementById returned the invisible hidden field instead of the checkbox. Twelve checkboxes were affected. Saving the forms was never affected, which is why it went unnoticed.

Upgrade notes

No migration, no new setting, no new permission.

One visible change on upgrade: sites that hold the View AI statistics permission but run with the AI features and the knowledge base switched off will find the tab gone. Enabling either one under Administration → Plugins → Redmine expert Helpdesk brings it back with all of its history intact — nothing is deleted, only hidden.

0.2.3 (2026-08-06)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Tickets waiting for a response are now visible at a glance. When a customer replied by mail to an existing ticket — or a reply reopened a closed one — nothing marked that ticket as needing attention, and agents had to fall back on filtering by SLA status. A ticket is now flagged Awaiting response as soon as an inbound reply arrives on an existing ticket, and the flag clears the moment an agent posts a public note or closes the ticket. Four places show it: a sortable Awaiting response column and filter in the issue list, a marker on the row itself, a counter in the issue-list sidebar, and a "Helpdesk: awaiting response" block for My page. The stored timestamp is that of the oldest unanswered reply, so a second mail does not make a ticket that has been waiting for days look fresh. A private note deliberately does not clear the flag — an internal remark is not an answer to the customer.
  • Short mails no longer cost an AI call. A two-line "please call me back" summarises to itself, yet every ingested mail was sent to the provider. The new Min. input length setting skips the call below a configurable number of characters.
  • AI diagnostics have their own log level. The diagnostic lines about input length and skip decisions can be raised, lowered or silenced independently of the rest of the log. AI errors are always logged.
  • Every outgoing mail is logged with the transport it took. Replies, initial mails, autoresponder mails and SLA notifications leave over three different transports and previously all looked alike in the log. Each send now records which one it used; failures are always logged as errors.

Fixed

  • Auto-reopening a ticket now shows up in its history. The reopen status was written without a journal entry, so a ticket jumped from closed back to open with no trace in the ticket history or the activity feed — nothing indicated why it had reopened. The status change is now recorded on the journal that the inbound reply already creates: one history entry rather than two, and no extra notification mail.
  • Graph was offered as the send path for IMAP mailboxes it cannot serve. The rule read the mailbox's oauth_preset column, which is not in force for a mailbox using the central credentials, so the option appeared where it could not work.

Changed

  • The send path moved next to the mail provider in the mailbox form, where it belongs — it governs replies, initial mails and the autoresponder alike, not just replies.

Upgrade notes

This release runs a migration. Two columns are added to helpdesk_ticket_infos for the awaiting-response flag:

bundle exec rake redmine:plugins:migrate NAME=redmine_expert_helpdesk RAILS_ENV=production

Existing tickets start unflagged; the flag is set from the next inbound reply onwards.

The feature is on by default and can be switched off under Administration → Plugins → Redmine expert Helpdesk. Switching it off hides the column content, row markers, sidebar counter and My page block. No new permission is needed — the existing expert Helpdesk project module and the Send customer replies permission govern it, the latter deciding whose public note counts as an answer.

0.2.2 (2026-08-04)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Mailboxes can now be managed over the REST API. The generic IMAP/SMTP support in 0.2.0 gave a mailbox a large configuration surface — which backend it uses, IMAP and SMTP hosts, OAuth2 credentials, folders — but none of it could be reached from outside the web interface, and a ticket fetched over the API did not even say which backend its mail had arrived through.

There is now a full set of endpoints: list and create mailboxes per project, read, update and delete an individual one, and a test_connection call that probes the stored settings and reports the folders it can see. Reading requires the same manage_helpdesk permission as writing, because a mailbox exposes mail hosts, usernames and OAuth client ids.

Secrets go in but never come back out: the mailbox password, OAuth client secret and service-account key can be set through the API and are never returned. Responses only report whether one is stored. Sending a single hyphen clears a stored secret, matching the masked fields in the web interface. The interactive OAuth consent stays in the browser, where an identity provider needs it.

  • AI summary and knowledge-base settings are available over the API. The per-project settings endpoint quietly left out every AI and knowledge-base field, so those two features could only be configured by hand. They are now read and written like the SLA and phishing settings.

Fixed

  • Phishing detection stopped working on Ruby 4.0. Microsoft SafeLinks and other redirect links are unwrapped by reading the original address out of the link's query string. The method used for that was removed in Ruby 4.0, so on Redmine 7 every link carrying a query string raised an error: SafeLinks were no longer decoded and redirect links were never flagged. Query strings are now parsed with a method that is not going away.
  • Documentation no longer describes the plugin as Microsoft-365-only. The README still opened by calling this a Microsoft 365 plugin, months after generic IMAP/SMTP had shipped, and the plugin description shown in Redmine's own plugin list said the same. Both now state the actual choice of backends. The READMEs also gained a table of contents, and the German edition was brought back in line with the English one — it was missing a section entirely.

Upgrade notes

No migrations and no configuration changes. Existing mailboxes are untouched.

To use the new endpoints, enable the REST web service under Administration → Settings → API; requests authenticate with the usual X-Redmine-API-Key header and are authorized by the existing helpdesk permissions.

0.2.1 (2026-08-04)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Fixed

  • The folder fields on the mailbox form are real dropdowns now. All five (source, processed, skipped, failed, sent) were text inputs backed by an HTML datalist, which browsers draw exactly like their own autofill history — so a folder genuinely read from the mailbox was indistinguishable from a value typed into the field once before.

Each field now has a chevron that opens the full folder list and filters by substring while typing, so arbeit finds Verarbeitet. Arrow keys, Enter, Escape, hover and mouse selection all work. Free text stays valid: a folder that does not exist yet is offered for creation and is still created when the form is saved.

The field and the list follow the active Redmine theme — the input uses the same chevron and metrics as a native select, and the list reuses Redmine's own autocomplete styling. No new dependency.

0.2.0 (2026-08-03)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Added

  • Generic IMAP/SMTP mailboxes. Until now mail could only be fetched from Microsoft 365 via the Graph API. A mailbox now picks its backend itself, so Google Workspace, Exchange on-premises, self-hosted servers (Dovecot, Zimbra) and ordinary hosters work as well. Incoming mail arrives over IMAP, outgoing mail leaves through the mailbox's own SMTP server.
  • OAuth2 (XOAUTH2) as the default authentication, with three grants: client_credentials (application-only), authorization_code (one-time consent through the browser) and jwt_bearer (service account with domain-wide delegation, assertion signed locally). Username/password over TLS stays available for servers without OAuth2.
  • Per-mailbox secrets are stored encrypted — client secrets, refresh tokens, passwords and service-account keys. Existing plaintext values keep working, so no data migration is forced.
  • Connection presets for Microsoft, Google and generic servers prefill host, port and encryption, and central defaults can be set once under Administration for operators running a single mail server.
  • "Test connection" button on the mailbox form: verifies login, lists the folders and probes the outgoing route in one step.

Fixed

  • Rotating an OAuth2 client secret did not invalidate the cached access token — the token issued for the superseded secret stayed in use until it expired.
  • A new IMAP mailbox defaulted to a reply transport its own validation rejects.
  • Connection presets and the central defaults could never set a port or an encryption mode; only the host fields worked.
  • An IMAP mailbox could be configured to send through Microsoft Graph even when Microsoft does not host it.
  • The autoresponder ignored the mailbox's reply transport and always sent through Graph.
  • Boot failure and a silently missing OAuth controller when installed alongside RedmineUP's helpdesk plugin.

Upgrade notes

Migrations 034–037 run on upgrade. Existing Microsoft 365 mailboxes are untouched and keep using the Graph API — the new backend is opt-in per mailbox.

0.1.6 (2026-08-03)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.

Documentation: "Flow per mailbox fetch" brought back in sync with `MailProcessor`.

The diagram had drifted and was missing the auto-reply filter, the phishing scan (quarantine vs. neutralize), the MIME preprocessing stage (thread-header stripping for the reopen-age limit, `Auto-Submitted` stripping on NDRs, `In-Reply-To`/`References` injection), ticket reopening on replies, the `HelpdeskTicketInfo` link and the async AI-summary enqueue.

It also showed a single "target folder" where the code actually uses three (`processed_folder` / `skipped_folder` / `failed_folder`, with fallbacks).

Adds notes on target folders and per-message failure isolation. Mirrored in `README.de.md`.

`LICENSE` file (GPL-2.0-or-later)

The plugin is now explicitly licensed under the GNU General Public License v2 or later, matching Redmine itself.

Adds a copyright/license header to `init.rb` and a License plus Third-party components section to `README.md` / `README.de.md`, documenting the bundled MIT-licensed Chart.js 4.4.6 and chartjs-plugin-datalabels 2.2.0, both served locally — no CDN request at runtime.

Screenshots in both READMEs

`README.md` and `README.de.md` now open with a short gallery of the main screens (SLA statistics, AI statistics, customer list, ticket view) and carry inline screenshots in the customer, AI-summary and knowledge-base sections, so the plugin can be evaluated without installing it first.

Images live in `docs/screenshots/{en,de}/` and are excluded from the release archives (`--exclude='docs'` in `.github/workflows/release.yml`), so the installable package does not grow.

New maintenance scripts

`scripts/seed_screenshot_demo.rb` and `scripts/teardown_screenshot_demo.rb` build and remove the synthetic demo project the screenshots are taken from (contacts, tickets with a realistic SLA mix, AI request log, knowledge-base entries).

Both are scoped to a single project, write no global settings and are excluded from the release archives along with the rest of `scripts/`.

0.1.5 (2026-07-29)

Compatible with Redmine 7.0.x, 6.1.x, 6.0.x, 5.1.x.