Issue 77 Reading Mail From Another Folder - edmozley/freeitsm GitHub Wiki
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
- Bugs resolved β the index of write-ups like this one
- Mailbox authentication β delegated vs app-only, and the troubleshooting table
- Tickets β the module these mailboxes feed