Skip to content

Reply Attachments Never Reached The Customer

Ed Mozley edited this page Oct 2, 2026 · 3 revisions

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, with Undefined 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


What you saw

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.


What was actually wrong

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.


It was four problems, not one

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.


How it was fixed

One message builder, for SMTP and Gmail

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).

Thread pictures: the pattern, and two limits

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 emails and 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.

Pasted screenshots

A data: image in the body becomes a picture part with a cid: reference, on all three providers.


πŸ“ Files changed

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.


How it was verified

php tests/outbound-email-mime.php β€” 21 checks, all against the real code and real data:

  1. The builder: the right structure in each case, bytes identical, the injected header refused, non-English names and subjects encoded.
  2. A real forwarded thread from the database: all 8 stored pictures become cid: references, and each is byte-for-byte the file on disk.
  3. 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.
  4. The real SMTP client sends to a fake mail server the test starts on 127.0.0.1. What arrives is multipart/mixed, the PDF is byte-identical, all 8 pictures are inside the message, and the CC recipient is delivered to.
  5. A ticket holding more than 2 MB of files: one embedded (1.46 MB), two left as links.
  6. 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 from send_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.


What this means for you

  • 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.

Related pages

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally