Issue 216 Workflow Email From A Chosen Mailbox Failed - edmozley/freeitsm GitHub Wiki

A workflow email from a chosen mailbox always failed

Reported: #216 by Abdullah31H, October 2026 · Module: Workflows · Fixed in: 4.3.0 (changelog #2646)

The reporter found the cause, named the exact lines and suggested the fix — and then listed two related problems that turned out to be part of the same story. All three are fixed.


What you saw

A workflow with a Send an email action and Send from set to a mailbox — with no ticket involved, say on contract.expiring — failed every time:

Action 0 (send_email): Failed to obtain a valid access token

and the web server log said:

Graph app-only token failed for mailbox 1: client-credentials token request failed (HTTP 404)

Yet the same mailbox sent ticket replies, SLA notifications and Domains alerts without trouble.

And two more, reported alongside it:

  • A ticket raised by Create a ticket could not be emailed afterwards. A following Send an email with {{last.ticket_id}} failed with "Ticket has no associated mailbox — cannot send".
  • That ticket showed Owner: Unassigned even though the action had an analyst in Assign to.

What was actually wrong

1. The chosen mailbox was read as ciphertext

A mailbox's tenant id, client id and secret, its address and its IMAP / SMTP logins are stored encrypted (ENCRYPTED_MAILBOX_COLUMNS in includes/encryption.php). Every reader is supposed to pass the row through decryptMailboxRow(). The ticket path does:

function templateGetMailboxForTicket(PDO $conn, int $ticketId): ?array {
    ...
    if ($mailbox) {
        $mailbox = decryptMailboxRow($mailbox);
    }

The Send from branch of action_send_email() did not:

$mb = $conn->prepare('SELECT * FROM target_mailboxes WHERE id = ?');
$mb->execute([$mailboxId]);
$mailbox = $mb->fetch(PDO::FETCH_ASSOC) ?: null;

So the token request went to https://login.microsoftonline.com/ENC:…/oauth2/v2.0/token — a "tenant" that is a lump of ciphertext — and Microsoft answered 404.

🔑 Why the other paths worked: they never touch this branch. A ticket reply finds its mailbox through the ticket, an SLA notice through sla_get_first_active_mailbox(), and both decrypt. Only a workflow that names a mailbox took the raw-row route. The symptom ("access token") pointed at OAuth; the cause was one missing function call.

It was not only app-only Microsoft mailboxes. Anything that needs an encrypted field broke the same way: a delegated Microsoft mailbox as soon as its token needed refreshing, and a basic IMAP / SMTP mailbox at once — the test below shows SMTP trying to connect to a host called ENC:msGX…. There is a small irony here: #119 gave this same function its IMAP branch, and that branch could only ever have worked through a ticket.

2. A workflow-raised ticket had no mailbox

A ticket's mailbox is not a column on the ticket. It is emails.mailbox_id on the ticket's opening message, and every send looks there — the analyst's Reply (getMailboxForTicket() in api/tickets/send_email.php) as much as the workflow. Every other way of creating a ticket writes one: an imported email, the analyst's New ticket form, the portal. Create a ticket wrote its opening message without one — and had no way to be given one.

So a workflow-raised ticket could not be emailed by anybody: not by the next step, and not by an analyst pressing Reply. That is worth noticing, because "raise a ticket, then email the requester about it" is the example that chaining was built for.

3. owner_id was left empty

FreeITSM stores the assignee twice — assigned_analyst_id and owner_id — and the Owner field reads owner_id. Everything that creates or assigns a ticket sets both: the TicketsService create, api/tickets/assign_ticket.php, and the workflow's own Assign ticket action, whose comment says so:

// Mirror assign_ticket.php's explicit-analyst branch — set BOTH
// assigned_analyst_id and owner_id so the detail-pane Owner field
// stays in sync.

Create a ticket set only the first.


It was three places, not one

Fault Where Also there?
Mailbox row not decrypted action_send_email() No. Every other SELECT * FROM target_mailboxes WHERE id = ? in the code — the change and knowledge share emails, the mailbox check, folder verification, both OAuth callbacks, calendar sync — already decrypts. This was the only one.
No mailbox on the opening message action_create_ticket() Yes — the form path it hands over to, catalogueCreateTicketFromSubmission() in includes/catalogue_approvals.php, had the same gap.
owner_id not set action_create_ticket() Yes — the same form path.

The fix

  1. The Send from branch decrypts the row, like every other reader:

    $mailbox = decryptMailboxRow($mb->fetch(PDO::FETCH_ASSOC) ?: null);
    
  2. Create a ticket has a new Mailbox box. The ticket's opening message gets:

    • the mailbox chosen there, if there is one (checked against the workflow's company, as Send from is);
    • otherwise the mailbox of the ticket that fired the rule — so a fan-out from an emailed ticket replies from the address the requester already wrote to;
    • otherwise none, as before.

    That choice is one function, mailboxForNewTicket(), used by both the plain path and the form path.

  3. Both paths set owner_id with assigned_analyst_id.

📁 Files changed

File Change
🟦 workflow/includes/engine.php decryptMailboxRow() in action_send_email(); the mailbox_id argument on create_ticket; mailboxForNewTicket(); owner_id and mailbox_id written
🟦 includes/catalogue_approvals.php mailbox_id override; owner_id written

No database change: both columns already existed.


How it was proved

A script ran the real engine (WorkflowEngine::runActionList()) against a throwaway install's database, with a basic SMTP mailbox pointing at a closed port on 127.0.0.1. No email can be delivered that way, but the error message names the host it tried, and that is the evidence:

Check Before After
Send from the chosen mailbox SMTP connection to ENC:msGXn94g…:1 failed: getaddrinfo… SMTP connection to 127.0.0.1:1 failed: …actively refused it
Create a ticket with an analyst owner_id NULL owner_id = the analyst
…and a mailbox no mailbox on the opening message the chosen mailbox
Send an email on that ticket Ticket has no associated mailbox reaches SMTP
No mailbox chosen, fired by a ticket — inherits that ticket's mailbox
No mailbox, no ticket, no analyst — stays empty, as before

Seven checks: two pass on the old code (the "nothing chosen" cases, which must not change), five fail; all seven pass on the new code. The rows the script made were deleted afterwards.

Not proven

  • A real Microsoft app-only send. The test proves the row reaching the token request is decrypted; it did not mint a token from a real tenant.
  • The form path (catalogueCreateTicketFromSubmission()) was changed in the same way but not run by the script.

What this means for you

  • Send from a mailbox now works, whatever kind of mailbox it is.
  • In Create a ticket, choose a Mailbox if the ticket should be emailable and nothing fired the rule from a ticket. Tickets raised before this fix still have no mailbox; an analyst can still add notes to them, but Reply will not find a mailbox.
  • Assign to in Create a ticket now shows as the Owner straight away — no separate Assign ticket step needed.

Related