Skip to content

Issue 77 Reading Mail From Another Folder

Ed Mozley edited this page Aug 17, 2026 · 1 revision

Mail could only ever be collected from Inbox (issue #77)

Point a Microsoft 365 mailbox at any folder of your own β€” freeitsm, say β€” and nothing was ever collected:

Failed to fetch emails. HTTP Code: 400.
Response: {"error":{"code":"ErrorInvalidIdMalformed","message":"Id is malformed."}}

Reported in issue #77 by tjedelhauser.

Fixed in 465ec754, released as update #1107.

The general guide to mailboxes is Mailbox authentication.


1. It had never worked, for any folder but seven

This reads like a regression. It is not. Reading mail from a folder by name had never worked at all β€” it only ever appeared to, because the default was one of the handful of names that happen to work by accident.

FreeITSM asked Microsoft for the messages like this:

'/mailFolders/' . $mailbox['email_folder'] . '/messages'

That places the folder name where Graph expects a folder identifier β€” a long opaque string like AAMkADNkNDYyYTQxLTY3ODEt…. Graph makes one concession: a short list of well-known names are accepted there as aliases.

Accepted as a name Not accepted
inbox, drafts, sentitems, deleteditems, junkemail, archive, outbox anything you created yourself

INBOX is on that list. freeitsm is not, so Graph tried to read it as an identifier, found it malformed, and said so. Every custom folder failed identically, and always had.


2. The fix was already in the file, and was never called

resolveMailFolderId() sat in the same file doing precisely the right thing: map the well-known names, and otherwise ask Graph for the folder by display name and use the identifier it returns.

It had exactly one caller β€” moveEmailToFolder(), the code that files a message after it has been imported. The code that reads the mailbox never called it.

Which is why the screen contradicted itself

Two folder fields sit on the mailbox dialogue, one above the other:

Field What it does Verify button? Resolved names?
Email folder Where mail is read from ❌ none ❌ no
Move imported mail to Where mail is filed afterwards βœ… yes βœ… yes

So the reporter's screenshot shows the application cheerfully confirming

Folder "freeitsm" found (1 items, 1 unread)

in the box directly beneath the box that could not use that folder at all. The half of the feature that mattered least got the resolution and the reassurance; the half that decides whether any mail arrives got neither.

The general lesson: the same question asked in two places will eventually be answered two different ways. Here "does this folder exist?" had two implementations and one caller each, and the one attached to a button was the one that worked.


3. What was changed

One resolver, shared by everything. mailboxResolveFolderId() now lives in includes/mailbox_graph.php and is used by the fetch, the move, and the Verify button. Verify and the fetch can no longer disagree, because they are the same code.

Verify now proves the thing that was failing. It resolves the name, then reads the folder back by identifier β€” the exact operation that used to return the 400. Confirming that a folder of that name exists was never the question.

Nested folders work, written with a slash:

Inbox/freeitsm

This matters more than it looks. /mailFolders lists top-level folders only, so a folder tucked inside Inbox was invisible to the lookup and would still have failed after the obvious fix β€” with a different message ("not found"), for a folder plainly visible in Outlook. Each segment after the first is resolved through childFolders.

Matching is case-insensitive, and deliberately does not use Graph's $filter: filtering on displayName is case-sensitive, so a folder typed Freeitsm would have read as missing.

The Email folder field gained a Verify button, and both fields now call one shared verify routine rather than two copies of one.


4. πŸ“ The files involved

🟒 The fix

File Role
includes/mailbox_graph.php New: mailboxResolveFolderId() (name β†’ id, well-known aliases, nested paths) and mailboxGraphGet(). Takes its fetcher as an argument, so it is testable without a mailbox

πŸ”΅ The three callers

File Role
api/tickets/check_mailbox_email.php getEmails() resolves the name before building the path; resolveMailFolderId() is now a thin delegation, not a second implementation
api/tickets/verify_mailbox_folder.php Resolves through the shared helper, then reads the folder back by id
tickets/settings/index.php Verify button on Email folder; both fields share one verify routine

βšͺ Test

File Role
tests/mailbox-folder-resolve.php 15 assertions against a fixture, including a negative control

5. How it was verified

Run against a real Microsoft 365 mailbox, old code and new, in the same process seconds apart β€” so the only difference is the fix:

Check Result
Old behaviour, live 400 ErrorInvalidIdMalformed β€” the reporter's exact error, reproduced
New behaviour, same mailbox 200, message found
Full check with real settings Ticket created from a message in freeitsm
INBOX control Still resolves as an alias with no lookup, still fetches
Case-insensitivity, live FREEITSM resolves to the same folder
Unknown folder, live Names the folder and suggests the slash form
childFolders request, live 200 β€” the nested lookup's request shape is correct

Plus 15 fixture assertions, of which the one that matters most is the negative control: a custom folder must never resolve to the name that was typed. If it ever does, this bug is back and every other assertion would still pass.

What is not proven. A successful nested match is fixture-only β€” the test mailbox's Inbox has no child folders, so only the request shape and the failure path were exercised live. And the live mailbox authenticates as a signed-in user (delegated) while the reporter's authenticates as an application (app-only); the resolver is identical for both and only the base path differs, but that combination was not run.


6. What this means for you

  • Point a mailbox at any folder you like. It no longer has to be Inbox.
  • A folder inside another is written with a slash: Inbox/freeitsm.
  • Capitalisation does not matter.
  • Use the Verify button next to Email folder before saving β€” it now checks the folder the way the mail collection will actually use it.
  • If you were using a custom folder and seeing nothing, that is this bug, and no mail was lost β€” it was never collected, so it is still sitting in the folder.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally