-
Notifications
You must be signed in to change notification settings - Fork 15
Issue 77 Reading Mail From Another Folder
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.
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.
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.
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.
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.
| 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 |
| 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 |
| File | Role |
|---|---|
tests/mailbox-folder-resolve.php |
15 assertions against a fixture, including a negative control |
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.
- 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.
- 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
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)