Repository navigation
Basic IMAP Client Developer Guide
How FreeITSM reads a Basic IMAP mailbox without PHP's imap extension: the files, the protocol conversation, the PHP that drives it, and how it plugs into the existing mail pipeline.
- Why it is built this way: Basic IMAP without the PHP extension
- Testing and verifying it: Basic IMAP client - Testing
- Issues: #257, #259, #50
| File | Role |
|---|---|
includes/imap_client.php |
New. The IMAP client: the ImapClient class (the connection), the reply parser, and the readers for BODYSTRUCTURE, ENVELOPE, encoded headers and filenames. Knows nothing about tickets |
includes/mailbox_imap.php |
Changed. The Basic IMAP provider: turns a mailbox row into a signed-in connection, reads unread mail into FreeITSM's message shape, applies the post-import action, verifies a folder - and sends replies over SMTP. Its public functions kept their names and signatures |
api/tickets/check_mailbox_email.php |
Unchanged. Calls imapGetEmails() (line ~162) and imapHandleAfterProcessing() (line ~832) |
api/tickets/verify_mailbox_folder.php |
Unchanged. Calls imapVerifyFolder()
|
includes/ssl.php |
Unchanged. Its sslResolveCaBundle() now also serves the IMAP and SMTP sockets |
tests/imap-client-parse.php, tests/imap-client-live.php
|
New. See Testing |
docker/php-extensions.txt, setup/index.php
|
imap removed from both - nothing needs it |
Nothing above mailbox_imap.php changed. The importer, whitelisting, company routing, threading and attachment storage all receive the same arrays as before.
check_mailbox_email.php ββ (unchanged)
verify_mailbox_folder.phpβ€
βΌ
mailbox_imap.php imapStreamFor() Β· imapGetEmails() Β· imapNormaliseMessage()
(the provider) imapWalkStructure() Β· imapHandleAfterProcessing() Β· imapVerifyFolder()
βΌ
imap_client.php 3. message structure imapBodyStructure() imapEnvelope()
imapPartFilename() imapDecodeMimeHeader()
2. reply parser imapParseValues() β arrays, strings, null
1. connection ImapClient: TLS, tags, literals, NO/BAD
βΌ
PHP: stream_socket_client(), OpenSSL, base64, mbstring/iconv
Each layer only talks to the one below. The connection does not know what a message is; the structure readers do not know there is a network.
IMAP is a text conversation over one connection. This is a real session with the Dovecot server used in the tests, recorded from the client, reading one email with an attachment (C = FreeITSM, S = server). The password is masked, long capability lists and the server's timings on tagged lines are shortened to ..., and nothing else is changed.
(TLS handshake on port 993 - certificate checked)
S: * OK [CAPABILITY IMAP4rev1 IMAP4rev2 LOGIN-REFERRALS ID ENABLE IDLE SASL-IR LITERAL+ AUTH=PLAIN] Dovecot ready.
C: A0001 LOGIN "wiki-5744" "********"
S: A0001 OK [CAPABILITY IMAP4rev1 IMAP4rev2 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT ...] ...
C: A0002 CAPABILITY
S: * CAPABILITY IMAP4rev1 IMAP4rev2 SASL-IR ... UIDPLUS ... MOVE ...
S: A0002 OK ...
-
Every command starts with a tag (
A0001,A0002...). The server answers with any number of untagged lines (* ...) and then one tagged line -A0002 OK,NOorBAD- saying whether that command worked. The client reads until its own tag comes back. That is how it knows a reply is complete; never "the next line". -
Capabilities are asked for again after sign-in, because servers commonly reveal
MOVEandUIDPLUSonly then. They decide which commands to use later. -
STARTTLS (encryption
tls, port 143): the client connects in plain text, sendsSTARTTLS, upgrades the socket to TLS, then throws away the capabilities it learnt before the upgrade and asks again (RFC 3501 Β§6.2.1 - a man in the middle could have forged them).
C: A0005 SELECT "INBOX"
S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
S: * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted.
S: * 1 EXISTS
S: * 1 RECENT
S: * OK [UNSEEN 1] First unseen.
S: * OK [UIDVALIDITY 1794081623] UIDs valid
S: * OK [UIDNEXT 2] Predicted next UID
S: A0005 OK [READ-WRITE] Select completed ...
C: A0006 UID SEARCH UNSEEN
S: * SEARCH 1
S: A0006 OK ...
(A0003 and A0004 were the test putting an email in and making a folder - APPEND and CREATE - so there was something to read.)
-
* 1 EXISTSis the folder's message count - Verify's total. - Everything is done by UID, which stays the same for a message in that folder, rather than by sequence number, which shifts whenever anything is deleted. The UID is what
imapHandleAfterProcessing()later acts on.
C: A0007 UID FETCH 1 (UID ENVELOPE BODYSTRUCTURE INTERNALDATE)
S: * 1 FETCH (UID 1 INTERNALDATE "07-Nov-2026 20:00:30 +0000"
ENVELOPE (NIL "Printer jammed"
(("=?UTF-8?B?SsO2cmc=?=" NIL "jorg" "example.com")) β from
(("=?UTF-8?B?SsO2cmc=?=" NIL "jorg" "example.com")) β sender
(("=?UTF-8?B?SsO2cmc=?=" NIL "jorg" "example.com")) β reply-to
((NIL NIL "support" "freeitsm.test")) β to
NIL NIL NIL "<wiki-1@example.com>") β cc, bcc, in-reply-to, message-id
BODYSTRUCTURE (
("text" "html" ("charset" "utf-8") NIL NIL "7bit" 20 0 NIL NIL NIL NIL) β part 1
("application" "pdf" NIL NIL NIL "base64" 12 NIL
("attachment" ("filename" "error.pdf")) NIL NIL) β part 2
"mixed" ("boundary" "b") NIL NIL NIL))
S: A0007 OK ...
(The reply is one line; it is broken up here, with β notes, to read. The date is NIL because this test email had no Date header - FreeITSM then uses INTERNALDATE, when the server received it.)
This one reply is the whole of what used to need imap_fetch_overview() and imap_fetchstructure(). The server has already parsed the email. The client never sees a MIME boundary; it sees a tree: part 1 is HTML in UTF-8, part 2 is a base64 PDF called error.pdf, and the two sit in a multipart/mixed.
Then each part it needs, by number:
C: A0008 UID FETCH 1 BODY.PEEK[1]
S: * 1 FETCH (UID 1 BODY[1] {20}
S: <p>It is jammed.</p>)
S: A0008 OK ...
C: A0009 UID FETCH 1 BODY.PEEK[2]
S: * 1 FETCH (UID 1 BODY[2] {12}
S: JVBERi0xLjQ=)
S: A0009 OK ...
-
BODY.PEEK, notBODY. PlainBODY[...]marks the message\Seen;PEEKdoes not. That is the oldFT_PEEK, and it is why a message stays unread until the mailbox's post-import action decides what happens to it. Note the server echoes the section without.PEEK-BODY[1]- which is the key the client looks for. -
{20}is a literal: "the next 20 bytes are data". The client then reads exactly 20 bytes, whatever they contain - CRLFs, quotes, binary. This is how attachments arrive safely. After the literal the line continues ()), so the client keeps reading. - Part numbers nest: in a
multipart/mixedholding amultipart/alternative, the HTML might be1.2and an attachment2. A single-part email's body is part1.
C: A0010 UID MOVE 1 "Processed"
S: * OK [COPYUID 1794081625 1 1] Moved UIDs.
S: * 1 EXPUNGE
S: * 0 RECENT
S: A0010 OK ...
| Post-import action | Commands sent |
|---|---|
| Mark read | UID STORE 1 +FLAGS.SILENT (\Seen) |
| Move to folder |
UID MOVE 1 "Folder" - or, on a server without MOVE: UID COPY, UID STORE +FLAGS.SILENT (\Deleted), then EXPUNGE
|
| Delete / move to deleted |
UID STORE 1 +FLAGS.SILENT (\Deleted), then UID EXPUNGE 1 with UIDPLUS, else EXPUNGE
|
TRAP: plain
EXPUNGEremoves every message flagged\Deletedin the folder, not just ours. WithUIDPLUSthe client expunges only the one UID. Without it, it does what the old extension did on every delete and move (imap_expunge). Both test servers haveUIDPLUS; the fallback is tested by hiding it.
C: A0011 LIST "" "*"
S: * LIST (\HasNoChildren) "/" Processed
S: * LIST (\HasNoChildren \Drafts) "/" Drafts
S: * LIST (\HasNoChildren \Junk) "/" Junk
S: * LIST (\HasNoChildren \Sent) "/" Sent
S: * LIST (\HasNoChildren \Trash) "/" Trash
S: * LIST (\HasNoChildren) "/" INBOX
S: A0011 OK ...
C: A0012 LOGOUT
S: * BYE Logging out
S: A0012 OK Logout completed ...
LIST gives each folder's flags, its hierarchy delimiter (/ here, . on many cPanel servers - hence "INBOX.Support") and its name. Entries flagged \Noselect are namespace placeholders, not folders, and are left out of Verify's list.
The client sends literals too. Any value containing a line break, a NUL or a non-ASCII byte is sent as {n} + the bytes, after the server's go-ahead:
C: A0001 LOGIN "wiki-lit" {10}
S: + OK
C: pΓ€sswΓΆrd (exactly 10 bytes - Γ€ and ΓΆ are two bytes each in UTF-8)
S: A0001 NO [AUTHENTICATIONFAILED] Authentication failed.
(Recorded with a deliberately wrong password, so the last line also shows what a refusal looks like - NO, with the server's reason, which is what the client puts in its error.)
ImapClient::sendCommand() splits the command at each {n}\r\n, waits for the + line, sends exactly n bytes, and carries on. A tagged NO instead of + means the server refused, and the client stops.
| Method | Sends | Returns |
|---|---|---|
connect() |
TLS handshake (ssl), or plain + STARTTLS (tls); reads the greeting |
- |
login($user, $pass) |
LOGIN, then CAPABILITY
|
- |
capability() / hasCapability('MOVE')
|
CAPABILITY |
the list, upper-cased |
listFolders() |
LIST "" "*" |
[['name', 'delimiter', 'flags'], ...], names in UTF-8 |
select($folder) |
SELECT |
the message count (EXISTS) |
uidSearch('UNSEEN') |
UID SEARCH |
UIDs, ascending (oldest first) |
uidFetch($uid, $items) |
UID FETCH |
['ENVELOPE' => [...], 'BODY[2]' => '...'] |
uidAddFlags, uidMove, uidDelete
|
as in the table above | - |
logout() |
LOGOUT |
- |
command($text) |
anything, tagged | the untagged lines; throws on NO/BAD with the server's own words |
How command() reads:
- Send
A0007 <command>(splitting at literals, above). - Read logical lines: a line, and if it ends in
{n}, exactlynmore bytes and the rest of the line, repeatedly. A logical line can therefore contain CRLFs. -
* ...lines are collected (and scanned forCAPABILITY). A line starting with our tag ends the command:OKreturns the collected lines, anything else throwsImapException.
Unrequested lines are normal in IMAP: a server may add * 3 EXISTS when mail arrives, or a * FETCH for a flag change on another message. uidFetch() keeps only the FETCH whose UID matches the one asked for; other commands look only for the lines they need.
imapParseValues($text) turns IMAP data into PHP values:
| IMAP | PHP |
|---|---|
(a b (c)) |
['a', 'b', ['c']] |
"say \"hi\"" |
'say "hi"' |
{5}\r\nhello |
'hello' (a literal) |
NIL |
null |
UID, \Seen, BODY[1.2], BODY[HEADER.FIELDS (SUBJECT)]
|
the string as it is (brackets belong to the atom, spaces inside them too) |
It never loops forever on bad input: a truncated list returns what it has, and a literal shorter than declared stops at the end of the data.
imapBodyStructure($bs) turns the parsed BODYSTRUCTURE into a tree of arrays - the shape imap_fetchstructure() gave, minus the numeric type codes:
['type' => 'application', 'subtype' => 'pdf', 'params' => [], 'id' => null,
'encoding' => 'base64', 'disposition' => 'attachment',
'dparams' => ['filename' => 'error.pdf'], 'parts' => []]A multipart node has type => 'multipart' and its children in parts. The fields sit at fixed positions, with one twist: where the disposition (attachment/inline and its filename) sits depends on the type, because some types carry extra fields first:
| Part type | Fields before the extension data | Disposition at |
|---|---|---|
text/* |
type, subtype, params, id, description, encoding, size, lines | index 9 |
message/rfc822 (an attached email) |
... size, envelope, body, lines | index 11 |
| anything else | ... size | index 8 |
multipart/* |
children, subtype, params | after the subtype + 2 |
TRAP: get those positions wrong and attachments silently become body text, or lose their names.
tests/imap-client-parse.phphas a case for each row, and the live test sends a real attached email.
imapEnvelope($env) reads date, subject, from, to, cc and Message-ID. Addresses arrive as (name route mailbox host); the client joins mailbox@host and decodes the name. Group markers ((NIL NIL "group" NIL) ... (NIL NIL NIL NIL)) carry no address and are skipped.
| What | Example | How |
|---|---|---|
| Transfer encoding | base64, quoted-printable |
base64_decode(), quoted_printable_decode() - imapDecodeTransfer()
|
| Character set | ISO-8859-1, Windows-1252, Shift_JIS |
mb_convert_encoding(), then iconv() - imapCharsetToUtf8(). ISO-8859-1 is read as Windows-1252, its superset, because mail labelled Latin-1 very often contains curly quotes and β¬, which only exist in 1252 |
| Encoded headers (RFC 2047) |
=?UTF-8?B?SsO2cmc=?= β JΓΆrg |
imapDecodeMimeHeader() - B and Q forms; spaces between two encoded words are dropped, as the RFC says |
| Encoded filenames (RFC 2231) |
filename*=utf-8''%C3%9Cbersicht.pdf, split as filename*0*, filename*1*
|
imapPartFilename() / imapRfc2231Param()
|
| Folder names (modified UTF-7) |
Γbergabe β &ANw-bergabe
|
mb_convert_encoding(..., 'UTF7-IMAP') - imapFolderEncode() / imapFolderDecode()
|
TRAP: when both
filenameandfilename*are present,filename*wins. Mail programs - FreeITSM's ownmimeBuildMessage()included - send an ASCII fallback (filename="Bericht f__r Sie.pdf") and the real name. Reading the plain one first turned "Bericht fΓΌr Sie.pdf" into "Bericht f__r Sie.pdf". Each side looked right on its own; only the send-then-read round trip in the live test found it.
The client is new; nothing around it is.
One message shape for every provider. Microsoft (includes/mailbox_graph.php), Google (includes/gmail.php) and Basic IMAP all hand the importer the same Graph-like array. imapNormaliseMessage() builds it exactly as before:
[
'id' => '1', // the UID - post-actions use it
'internet_message_id' => '<wiki-1@example.com>', // de-duplication; 'imap-<mailbox>-<uid>' if none
'subject' => 'Printer jammed',
'from' => ['emailAddress' => ['name' => 'JΓΆrg', 'address' => 'jorg@example.com']],
'toRecipients' => [...], 'ccRecipients' => [...],
'receivedDateTime' => '2026-11-07T20:00:30Z',
'body' => ['contentType' => 'html', 'content' => '<p>It is jammed.</p>'],
'hasAttachments' => true,
'attachments_inline' => [ /* Graph fileAttachment arrays, bytes already fetched */ ],
'importance' => 'normal', 'isRead' => false,
]-
attachments_inlineis the existing convention (from the Gmail connector) for "the bytes are already here".saveEmailToDatabase()incheck_mailbox_email.phpstores them directly instead of calling Graph. IMAP needs this because a part cannot be fetched again once the connection closes. -
imapStreamFor()- the same function, the same per-request cache keyed on mailbox and folder. It now returns a signed-inImapClientwith the folder selected, instead of an extension stream. The fetch and every post-action in one poll share one connection. -
imapVerifyFolder()- opens INBOX first (so a failure means credentials or TLS, not a bad folder name), lists folders, matches case-insensitively and with/for the server's delimiter, then opens the folder by the server's own name and reportsEXISTSand theUNSEENcount. -
TLS policy -
imapStreamContext()applies the same rule as every cURL call (includes/ssl.php): verify unless the install-wideSSL_VERIFY_PEERis off, and use PHP's configured CA bundle, or on Windows the bundledincludes/cacert.pem. SMTP sending now uses the same context. Before this,imapSmtpSend()verified with no CA bundle, so on Windows PHP - which has none of its own - verifying a real certificate depended onphp.ini. -
Sending is unchanged -
imapSmtpSend()andincludes/mime_message.php.
| Risk | Guard | Where |
|---|---|---|
A value ending the string early and starting a command (a password with " or a line break) |
Quoted strings escape " and \; anything with CR, LF, NUL or non-ASCII is sent as a literal, whose length is declared first - its content cannot be read as a command |
imapString() - TRAP comment |
| Search criteria from outside |
uidSearch() sends its argument as-is, so only ever pass a fixed key from code ('UNSEEN') |
uidSearch() - TRAP comment |
| Password sent on an unencrypted connection | If the server advertises LOGINDISABLED, the client refuses to send it and says to choose SSL or STARTTLS |
login() |
| Forged capabilities before STARTTLS | Discarded after the upgrade and asked for again | connect() |
| Unverified server | Certificates verified by default with the app's CA policy; turning verification off is the install-wide SSL_VERIFY_PEER switch, not a per-mailbox one |
imapStreamContext() |
| A hostile or broken server announcing a huge literal |
MAX_LITERAL = 128 MB; larger is refused with an error |
ImapClient::MAX_LITERAL |
| A server that stops talking | Connect and every read time out (30 s); the error says so | readLogicalLine() |
| Errors leaking the password | Error messages contain the server's reply text, never the command sent | command() |
Unencrypted connections are still allowed when the server allows them (encryption "none"), exactly as before, because the setting exists and some installs use it with a local mail server. A server that refuses passwords on an unencrypted line gets a clear message instead of a failed sign-in.
Deliberate, and each one tested:
| Before | Now | |
|---|---|---|
| Needs | the imap extension |
nothing |
| Folder names with accents (move-to folder, the mail folder) | passed as typed; IMAP expects modified UTF-7, so such names could fail to open or move | encoded properly |
Attachment names in RFC 2231 form (filename*=) |
only filename/name were read, so such files could arrive as "attachment" |
decoded, continuations joined, filename* preferred |
| ISO-8859-1 mail | converted as Latin-1 | converted as Windows-1252, so β¬ and curly quotes survive |
| An unreadable Date header |
strtotime() false β 1 Jan 1970 |
falls back to when the server received it, then to now |
| Verify's folder list | included \Noselect placeholders |
real folders only |
Delete / move on a UIDPLUS server |
expunged every \Deleted message in the folder |
expunges only that message |
LOGINDISABLED |
sign-in simply failed | explained: choose SSL or STARTTLS |
| SMTP on Windows | verified with no CA bundle | the bundled cacert.pem, as everywhere else |
Error messages keep their old prefixes (IMAP connection failed, IMAP move to "X" failed), so logs and the mailbox screen read the same.
Added after a real Zoho mailbox was set to STARTTLS on port 465: it collected mail, and every reply failed with SMTP error: expected 220, got "".
| File | What |
|---|---|
includes/mail_ports.php |
New. mailPortExpectedEncryption() and mailPortMismatch() - SMTP 465 = SSL, 587 = STARTTLS; IMAP 993 = SSL, 143 = STARTTLS. Nothing else is judged. none on a STARTTLS port is allowed; none on an SSL port is not |
includes/mailbox_imap.php |
imapSmtpSend() names the failing step - no greeting, STARTTLS refused, SMTP sign-in failed, will not send as, refused the recipient - and appends the port hint. imapSmtpExpect() throws SmtpReplyException, whose ->reply is the server's own words ('' for silence) |
includes/imap_client.php |
connect() does the same for IMAP: no greeting, or a failed TLS handshake, gets the port hint |
includes/mailbox_health.php |
Two dismissible warnings on Basic IMAP mailboxes: smtp_port_encryption, imap_port_encryption
|
includes/mailbox_send.php |
mailboxTestResolve() - which settings and which password the Test button uses |
api/tickets/test_mailbox_send.php |
New. The Test button. Same gate as Save and Verify (TICKETS_MAILBOXES). Sends through mailboxSendSimple() - the provider routes replies use - to the analyst who clicked, route mailbox_test in the send log |
tests/mailbox-send-test.php |
New. 36 checks - see Testing |
Why the test goes to the analyst, and only them: an address field would turn the button into a way to send email as the company to anyone.
TRAP: the stored password is only reused for the server and username it was saved for. Basic IMAP is tested with the form's unsaved values, and a blank password means "use the saved one" - exactly as Save treats it. But Test sends that password to whatever SMTP server is typed. Reusing it for a changed server would let anybody with mailbox settings type a server of their own, click Test, and collect a password they are not allowed to see. So
mailboxTestResolve()reuses it only when the SMTP server (case and spaces ignored) and the username are unchanged; otherwise it asks for the password to be typed. The same rule covers a separate SMTP login.tests/mailbox-send-test.phpsection 3 checks it, and was proved able to fail by running it against a copy with the rule removed - the stored password then went tosmtp.attacker.example.
-
BODY.PEEKis echoed back asBODY. Look upBODY[1.2], notBODY.PEEK[1.2]. - Never assume the next line is the answer. Read to the tag.
- Literals contain anything, CRLFs included. Read by count, never by line.
EXPUNGEwithoutUIDPLUSis folder-wide.-
An open folder only learns of new mail when it next talks to the server (
NOOPis "anything new?"). The poll never meets this, because it opens a fresh connection each time. - Docker's port forwarder accepts connections before the server inside is ready. A test against a just-started container can get an empty greeting; wait for the container's log.
-
Port and encryption go together (
includes/mail_ports.php). STARTTLS on 465 opens a plain connection to a server waiting for TLS; neither speaks, and it looks like a dead server. -
Do not reintroduce an
imap_*()call.tests/docker-php-extensions.phpfails if one appears, becauseimapis neither installed in nor excluded from the image.
- Basic IMAP without the PHP extension - the decision
- Basic IMAP client - Testing
- Basic IMAP / SMTP Mailboxes - setting one up
- RFCs: 3501 (IMAP4rev1), 6851 (MOVE), 4315 (UIDPLUS), 2047 (encoded headers), 2231 (encoded parameters)
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- π PDF exports moved to PDFKit (#254)
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- β³ π Switching modules off
- β³ π οΈ Module availability β Developer Guide
- β³ π Module availability β design (#217)
- 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
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
-
π¦ Microsoft Entra ID
- β³ π₯ Importing people and groups
- β³ π» Intune: assets to their primary user
- β³ π’ Organisation chart
- β³ π οΈ Entra directory sync β Developer Guide
- β³ π οΈ Entra import - review (#235)
- β³ π οΈ Entra follow-ups - review (#237-#240)
- β³ π οΈ Intune primary user - review (#236)
- β³ π οΈ Organisation chart - reviews (#242, #265)
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π οΈ Empty Procedures fold - review (#247)
- β³ π« 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: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π οΈ Inbox.js map β Developer Guide
- β³ π οΈ Floating windows & the file viewer β Developer Guide
- β³ π οΈ Tickets settings β Developer Guide
- β³ π Simpler ticket and task forms - design (#218)
- β³ ποΈ The ticket workspace
- β³ π The ticket table
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π§Ύ A type for every new ticket
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ π Ticket dashboard β Developer Guide
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
-
β³ Basic IMAP mailboxes
- β³ π Basic IMAP without the PHP extension (#257)
- β³ π οΈ Basic IMAP client β Developer Guide
- β³ π§ͺ Basic IMAP client β Testing and verifying
- β³ π οΈ Verifying a mail folder β Developer Guide
- β³ π οΈ Mailbox providers β Developer Guide
- β³ π οΈ How Gmail mail collection works β Developer Guide
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Case reports - a ticket and its work as one PDF
- β³ π Scheduled work in your own calendar
- Problem Management
-
Tasks
- β³ π οΈ Task board and subtasks β Developer Guide
- β³ π§βπ€βπ§ People on a task ("Involved")
- β³ π Repeating tasks
- β³ π’ Moving a task between companies
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π Inventory freshness
- β³ π Linked configuration item
- β³ π Connected assets
- β³ π Asset review
-
β³ π·οΈ QR asset labels
- β³ π οΈ QR asset labels β Developer Guide
- β³ π Asset label designer - design (#229)
- β³ π· Asset scanning β Developer Guide
- β³ π οΈ Asset tag numbering β Developer Guide
- β³ π οΈ Asset reconciliation β Developer Guide
- β³ π οΈ Reconciliation, tags + labels - review (#164)
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ πͺ Hyper-V and Failover Clusters
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Starters & leavers
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- π‘ Zabbix (monitoring)
- π¬ Microsoft Teams alerts
- π 3CX (phone system)
-
System
- β³ π οΈ Preferences β Developer Guide
- β³ π Finding people in the Analysts list
- β³ πͺ When an analyst leaves
- β³ π οΈ System β Branding β Developer Guide
- β³ π©Ί Page health (D019) β Developer Guide
- β³ β Feature Bingo
- β³ π·οΈ Cost centres
- β³ π· Photo Album
- β³ Demo Data
- β³ π οΈ Demo Data β Developer Guide
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ One company supporting another
- β³ π’ One board across every company
- β³ 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)
- What this is
-
π Bugs resolved
- β³ π« #280 Email lookups scanned the whole table
- β³ β #266 Dragging a task told nobody
- β³ π #255 Article previews showed HTML codes
- β³ π #252 A link to a Knowledge article did not open
- β³ π #254 PDFs could not print Polish
- β³ π #250 Clicking a folder left the open article on screen
- β³ π« #260 The dashboard cog did nothing
- β³ π« #248 A department rule never matched on four routes
- β³ π A directory account could be matched to someone else
- β³ π« #244 Reply went to the assigned analyst
- β³ π A lookup answer could name any record
- β³ π« #227 A mailbox could not be deleted
- β³ π« #226 A picture on a note could not be closed
- β³ π» #222 The Assets page never loaded in 4.3.0
- β³ βοΈ #214 A "contains" condition did not match capitals
- β³ βοΈ #216 A workflow email from a chosen mailbox always failed
- β³ π³ The Docker image was missing PHP extensions
- β³ π A failed DNS lookup read as a missing record
- β³ π A link could change data
- β³ π Access outlived its removal
- β³ π Knowledge articles could run scripts
- β³ π Knowledge writes ignored permissions
- β³ π Company boundaries (Oct 2026)
- β³ π Smaller gaps (Oct 2026)
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96