Repository navigation
Reply Attachments Never Reached The Customer
Reported in #158 Β· Fixed in #2045-#2049, #2055 Β· Shipped in 2.10.0 Β· Corrected in 2.10.1 (#2081)
β οΈ 2.10.0 brought a new fault with this fix: a reply or forward whose quoted thread held a picture from the ticket failed to send, on every provider, withUndefined constant "INLINE_THREAD_BUDGET". Fixed in 2.10.1 β see Replies with a picture in the thread failed to send.
π οΈ How it works underneath, with the code: Outbound email: attachments and pictures β Developer Guide
Your mailbox sent through SMTP β a basic IMAP/SMTP mailbox, in the report pointed at Exchange Server 2019. An analyst replied to a ticket and attached a file.
- The attachment showed on the ticket, and opened from there.
- The email was sent, and the send log said so.
- The customer received the email without the attachment.
The reporter looked at the raw message Exchange received and found Content-Type: text/html and nothing else β no multipart/mixed, no attachment part. A second user confirmed the same on Forward.
FreeITSM sends through three providers, and they take mail in two different forms:
| Provider | What FreeITSM hands it |
|---|---|
| Microsoft (Graph) | JSON β the body, plus a list of files. Microsoft builds the email. |
| Gmail (API) | A finished raw email. |
| SMTP (basic IMAP) | A finished raw email. |
The Microsoft path passed the files. The other two built their raw email as one HTML part and had nowhere to put a file. It was not an accident β the code said so:
// Send via SMTP (username/password). HTML body only β outbound attachments
// aren't supported on the basic-IMAP path (parity with the Gmail path).
imapSmtpSend($mailbox, $to, $cc, $subject, $bodyForSending);A known limit, written down, is not a bug on its own. What made it one is the line after the send: the files were saved to the ticket anyway, under a comment saying they were "the same files β¦ just sent". So the ticket, the send log and the analyst all agreed the customer had the file. Nothing anywhere said otherwise β which is why it took a customer to notice.
Following the send path through turned up three more, each silent in the same way.
1. Gmail dropped the CC list. gmailSendEmail() had no CC parameter at all. Anyone in the CC box was left off, with no error.
2. Pictures in the quoted thread arrived broken β on every provider, Microsoft included. When an email with pictures comes in, FreeITSM saves each picture and rewrites its <img> to point back at FreeITSM:
<img src="/api/tickets/get_attachment.php?cid=...&email_id=326">That link is fine in the reading pane. But a reply or forward quotes the thread underneath, so the link travels in the email to the customer, whose mail client has no idea where /api/tickets/... is. There was a function to turn these into pictures carried inside the email, processInlineImages() β and it looked for this:
$pattern = '/src=["\']api\/get_attachment\.php\?([^"\']+)["\']/i';api/get_attachment.php, with no tickets/ and no leading /. No stored email contains that form. Counted on a real install: 0 matches. The function had been converting nothing, on any provider, while looking as if it handled exactly this case.
π The code that looks like it deals with a problem is the easiest place for the problem to hide. The first reading of
buildEmailMessage()said "Microsoft already embeds thread pictures". It only called the function that would have.
3. Pasted screenshots. The reply editor stores a pasted screenshot as a data: image β the picture's bytes written straight into the HTML. Gmail and Outlook both refuse to show a data: image in a received email.
A new includes/mime_message.php builds the raw email, only as deep as it needs to be:
| What the reply has | What is sent |
|---|---|
| Just text |
text/html β exactly what was sent before |
| Pictures in the body |
multipart/related β the HTML plus its pictures |
| Attached files |
multipart/mixed β the above, plus the files |
It takes the same list of files the Microsoft path already used, so all three providers now get identical input. SMTP writes the result down its existing connection; Gmail posts it to its API, now with the CC list.
A file's type and name come from the browser, so they are treated as untrusted: a type must look like type/subtype or becomes application/octet-stream, and line breaks and quotes are removed from names. A test sends a type of application/pdf\r\nBcc: evil@β¦ and checks no Bcc header appears. Non-English file names travel in full (RFC 2231).
The pattern now matches any relative link to get_attachment.php β /api/tickets/β¦, ../api/tickets/β¦ and the old form. A full https:// address is somebody else's server and is left alone.
Two limits came with it:
-
Only this ticket's files. The old lookup took any attachment id it was given. An analyst can edit the HTML source of a reply, so a typed-in link could have mailed out a file from another ticket β on a multi-company install, another company's. The lookup now joins to
emailsand requires the same ticket. - At most 2 MB of thread pictures per email. This one is easy to miss. Every reply re-sends the whole thread's pictures, and Microsoft refuses a send request over 4 MB. Before the fix those pictures were not sent at all, so a picture-heavy thread still sent β with broken images. Embedding them without a cap would have turned "sends with broken pictures" into "does not send", on the provider that was never reported as broken. Past the cap, a picture keeps its link, exactly as before. The analyst's own attachments are not capped; they never were.
A data: image in the body becomes a picture part with a cid: reference, on all three providers.
| File | Change |
|---|---|
includes/mime_message.php |
New. Builds the raw email β HTML, inline pictures, attachments |
includes/mailbox_imap.php |
imapSmtpSend() takes the file list; its HTML-only builder is gone |
includes/gmail.php |
gmailSendEmail() takes CC and the file list; recipients must be valid addresses |
api/tickets/send_email.php |
SMTP and Gmail get the same files as Microsoft; the thread-picture pattern fixed, limited to the ticket and capped; pasted screenshots converted |
tests/outbound-email-mime.php |
New. 21 checks, below (29 since 2.10.1) |
Every other place that sends through SMTP or Gmail β notifications, templates, workflow emails, portal emails β calls the same two functions without the new arguments and gets the plain HTML email it always did.
php tests/outbound-email-mime.php β 21 checks, all against the real code and real data:
- The builder: the right structure in each case, bytes identical, the injected header refused, non-English names and subjects encoded.
- A real forwarded thread from the database: all 8 stored pictures become
cid:references, and each is byte-for-byte the file on disk. - A link to another ticket's file is left alone β and, as the positive control, the same link to this ticket's file is embedded.
- The real SMTP client sends to a fake mail server the test starts on
127.0.0.1. What arrives ismultipart/mixed, the PDF is byte-identical, all 8 pictures are inside the message, and the CC recipient is delivered to. - A ticket holding more than 2 MB of files: one embedded (1.46 MB), two left as links.
- A pasted
data:screenshot becomes an inline part.
Run against the old code, the same test finds 0 of the 8 thread pictures converted, and the SMTP section cannot run at all β so it fails on the bug it is there to catch.
β οΈ What this test missed. It loads only the functions fromsend_email.php, never the file as the server runs it, so it could not see that the 2 MB limit's constant was declared below the code that sends. Every check here passed on code that failed in production. Since 2.10.1 the test also checks that order β see Replies with a picture in the thread failed to send.
The inbound side is untouched. Getting pictures to display in incoming email was a large piece of earlier work, and this change could not be allowed near it. Before changing anything, the reading pane's responses were recorded for six inbound emails with pictures β Microsoft, Gmail and IMAP β and all 22 of their pictures and files. After the change they are byte-for-byte identical, and none of the inbound files appear in the diff.
π A wrong turn worth keeping: the first recording differed from a second one taken seconds later, with nothing changed. Opening an email marks it read β so the recording itself changed
is_read, and had marked two real emails read on the install it ran against. The recording now saves and restores that flag and leaves it out of the comparison. Check that a "before" snapshot is stable before trusting an "after" one.
Live test, Gmail mailbox β Gmail inbox: the attached PDF arrived, and the pasted screenshot displayed inline. On the first try the screenshot looked broken β an empty box, with the picture as an attachment β because Gmail had put the reply in Spam, where it strips pictures. See the red herring. Not yet tried live: Exchange over SMTP, the reporter's own setup. None of these live sends quoted a picture already stored on the ticket, which is why they did not hit the 2.10.0 fault.
2.10.1, live on all three providers: nine numbered emails through the real endpoint β Microsoft, Gmail and SMTP, each with a reply quoting a thread picture, a plain reply, and a forward carrying a thread picture, a pasted screenshot and an attached file.
Since then, email sent through SMTP also carries Date and Message-ID headers (#2055), which spam filters look for.
- On 2.10.0? Upgrade to 2.10.1 β on 2.10.0 a reply quoting a picture from the ticket fails to send.
- SMTP or Gmail mailbox: after upgrading, attachments, pasted screenshots and thread pictures reach the customer, and Gmail sends to your CC list. Nothing to configure.
- A picture shows as an empty box in Gmail? Check the Spam folder first β Gmail strips pictures there. If your replies land in Spam, check your domain's SPF record includes the servers you send through.
- Microsoft mailbox: attachments always worked. Thread pictures and pasted screenshots now arrive as pictures rather than broken links.
- Sent before 2.10.0 from SMTP or Gmail? The ticket shows the attachment, but the customer did not get it. Those emails cannot be re-sent automatically; if a customer is waiting on a file, send it again.
- Replies with a picture in the thread failed to send β the 2.10.0 fault in this fix
- Outbound email: attachments and pictures β Developer Guide
- Basic IMAP mailboxes
- Bugs resolved
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- 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: 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
- 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
- π CardDAV contact sync
- 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: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ 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
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π 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
- Domains
- 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
- β³ π’ 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
- β³ πΌοΈ 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