Skip to content

Issue 45 Verify Button Only Worked For Microsoft

Ed Mozley edited this page Aug 24, 2026 · 3 revisions

The Verify button only ever worked for Microsoft (issue #45)

Click Verify beside the mail folder on a Basic IMAP mailbox and it answered:

Mailbox is not authenticated

The mailbox was authenticated. It was listed as Connected, it was being checked on schedule, and it was collecting mail normally. Only the button disagreed.

Reported by mbsouth on issue #45 β€” the issue that added Basic IMAP support in the first place, which is exactly what this bug is: somewhere that feature never reached. He had already worked out the important half himself, reporting that mail to and from his Postfix/Dovecot servers was "working fine" and that it was only the warnings he could not identify. He was right on both counts.

Fixed in a1f64426, released as update #1171.

The companion bug reported alongside this one is IMAP mailboxes reported as not authenticated. The general guides are Mailbox authentication and Basic IMAP mailboxes.


1. A button offered on three providers and written for one

verify_mailbox_folder.php was built for Microsoft Graph, back when Graph was the only thing a mailbox could be. Basic IMAP and Google arrived later. The button was shown for all three β€” the mailbox dialogue hides fields by provider, but the Email folder row carries none of those classes, so every mailbox gets the button β€” and nothing in the endpoint had ever been taught that the other two exist.

Provider What Verify did
Microsoft Worked. The only one it was written for
Basic IMAP Refused at the door, every time, on every install
Google Ran the entire Microsoft flow using Google credentials

IMAP: refused before it did anything

The first thing the endpoint did was look for an OAuth token:

if (empty($mailbox['token_data'])) {
    echo json_encode(['success' => false, 'error' => 'Mailbox is not authenticated']);
    exit;
}

token_data holds the OAuth token. A Basic IMAP mailbox does not have one and does not need one β€” it connects with a stored username and password. No IMAP code path ever writes that column; the only mention of it in save_mailbox.php sets it to NULL.

So the test was not usually false for IMAP, it was structurally false. No credential could ever satisfy it. The failure was deterministic on every install, for every IMAP mailbox, and re-entering the password could not possibly help, because the password was not what was being examined.

The one mercy is that it stopped there. No connection was attempted to anybody's mail server, and nothing was sent to Microsoft.

Google: it got further, and that was worse

A Google mailbox does hold a token β€” a Google one β€” so it sailed through the gate and into a function that assumed any token was Microsoft's:

  • the access token was sent to https://graph.microsoft.com/v1.0/..., which rejected it
  • an expired token was refreshed against https://login.microsoftonline.com/…/oauth2/v2.0/token, using azure_client_id and azure_client_secret that a Google mailbox has never had

The user was shown a Microsoft error about a mailbox with nothing to do with Microsoft. Only the identity-mismatch check was properly fenced behind provider === 'microsoft'; everything after it was Microsoft by assumption rather than by decision.

The general lesson, and it is #77's lesson again one turn further on: that write-up ended by saying the same question asked in two places will eventually be answered two different ways. This is what happens when a provider is added and a Microsoft-shaped function is left in the path β€” the question was never re-asked at all, so two of the three answers were Microsoft's.


2. What was changed

Each provider is verified its own way, and the branch happens before the token gate β€” which is precisely where IMAP used to die.

Through the same helper the reader uses. IMAP verification opens the folder with imapStreamFor(), the function the inbound poll itself calls. Verify cannot now confirm a folder that reading then rejects, which was the exact failure shape of #77.

A folder that is not found names the folders that are. On a server with an INBOX. personal namespace the real name is INBOX.Support, and Support on its own genuinely will not open β€” so "not found" without a list is a puzzle rather than an answer:

Folder "NoSuchFolder" was not found in this mailbox.
Folders on the server: INBOX, Drafts, Templates, Snoozed, Sent, Spam, Trash, Archive.

Folder names arrive in IMAP's own modified UTF-7, so they are decoded before being shown β€” otherwise a folder called Anfragen or Übergabe comes back mangled in the one message whose whole purpose is to be read.

Connection failures are told apart from missing folders. The connection is made against INBOX, which always exists, so a failure at that point is unambiguously credentials, TLS or the missing PHP extension β€” not a mistyped folder name.

Gmail is checked against labels, because Gmail has labels. The label is looked up by name, case-insensitively, and a miss lists the labels that exist.

Gmail counts mirror the reader, not the label. gmailGetEmails() always lists with labelIds=INBOX and applies the configured folder as an extra label: filter on top β€” so it only ever sees mail that is in the Inbox and carries the label. Reporting the label's own message count would have passed a label whose mail is all archived, which reading then collects nothing from. Instead Verify runs the reader's own query, and says so plainly when the label exists but its mail sits outside the Inbox:

The label holds 42 message(s), but none are in the Inbox. FreeITSM only reads mail that is in the Inbox AND labelled "Support", so archived mail carrying this label will not be collected.

That result is shown in amber rather than green, because "found" in green reads as "working".


3. πŸ“ The files involved

🟒 The fix

File Role
includes/mailbox_imap.php New: imapVerifyFolder() β€” connects via imapStreamFor(), matches the folder against the server's own list, returns real message counts. New: imapListFolders() β€” strips the {host} prefix and decodes modified UTF-7
includes/gmail.php New: gmailVerifyFolder() β€” resolves the label, then counts through the reader's own query rather than the label's totals

πŸ”΅ The callers

File Role
api/tickets/verify_mailbox_folder.php Branches on provider before the OAuth-token gate; Google refreshes through gmailGetValidAccessToken(), not Microsoft's endpoint. The Microsoft path is untouched
tickets/settings/index.php Renders the Gmail caveat in amber; stopped double-encoding a folder name containing & or a quote (escapeHtml into textContent)

4. How it was verified

Run live against a real installation carrying all three provider types at once β€” a Microsoft 365 mailbox, a Gmail mailbox and a Zoho Basic IMAP mailbox β€” so each result has the other two beside it as controls:

Check Result
IMAP, INBOX Found, with counts
IMAP, a real subfolder (Archive) Found
IMAP, a folder that does not exist Named it, and listed the ten real folders on the server β€” which is itself proof the login succeeded and a genuine IMAP LIST ran
Google, INBOX Found, with counts from the live account
Google, a label that does not exist Named it, and listed the account's real labels
Microsoft, INBOX Unchanged β€” still resolves to a folder id and reads back
Microsoft, unknown folder Unchanged β€” still its own "check the spelling, use a slash" message

The two Microsoft rows are the regression control, and they matter as much as the new ones: the fix had to leave the only provider that already worked exactly as it was.

What is not proven. The mailbox tested is a hosted Zoho account. The reporter's is a self-hosted Postfix/Dovecot pair, which was not available to test against. The defect itself was structural rather than server-specific β€” the OAuth-token check could not pass for any IMAP server, whoever ran it β€” but Dovecot commonly presents an INBOX. personal namespace, and that shape was not exercised live. It is precisely the case the folder list in the error message exists to explain.

The IMAP mailbox tested has a flat folder layout, so the delimiter-normalising match (accepting Inbox/Support where the server names it INBOX.Support) was exercised only against flat names. The Gmail out-of-Inbox caveat was reasoned from the reader's query and not reproduced against an account with an archived label. And the PHP IMAP extension was present on the test machine, so the "extension not enabled" message was not seen live β€” it is raised by imapStreamFor(), which the mail collection has always used.


5. What this means for you

  • Verify works on all three provider types now. On IMAP it really opens the folder; on Gmail it really looks the label up.
  • If Verify told you a working IMAP mailbox was not authenticated, nothing was wrong with your mailbox and nothing needed reconnecting. Mail was being collected the whole time.
  • If a folder is not found, read the list in the message. On many servers the name you want is INBOX.Something, not Something.
  • On Gmail, watch for the amber result. A label whose mail has been archived out of the Inbox will not be collected, and that is worth knowing before you wonder why no tickets appear.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally