4

I have a webpage in PHP that lets me access an IMAP account. I can fetch the messages fine. But, when I go to download attachments, Firefox opens the download dialog, but immediately after pops an error window, saying: "W227LcEc.png.part could not be saved, because the source file could not be read." And it doesn't let me click the "Save" button in the download dialog.

However, I checked the Network tab of the Developer Tools. The file was correctly received (return code 200), the size is exactly as expected, and in the Response tab I can see the contents of the file, encoded in Base64. If I take this content, and save into a text file, decode Base64, I have a perfectly working file.

I've googled, found advices about header directives, but to no avail to solve the problem.

I'm including only relevant part of codes.

This is the HTML:

<div onclick="download('1234','ABCEDF')">clinical-guidelines-2024-en.pdf</div>

In this example, '1234' would be a unique identifier to a specific email, and 'ABCEDF' a unique identifier to its attachment (in this example, the mencioned PDF).

Now, the javascript code:

function download(message_id,attachment_id) {
    var link_el = document.createElement("A");
    link_el.href = 'https://my.server.com/webservice.php?message_id='+message_id+'&attachment_id='+attachment_id;
    link_el.click();
}

And this is the relevant part of the PHP code in webservice.php:

$struct = imap_fetchstructure($imap_instance,$message_id,FT_UID);
...
// Then I process $struct, loop through $struct->parts, until I find the correct attachment
...
// When attachment is found:
$type = $struct->parts[$i]->type;
$subtype = strtolower($struct->parts[$i]->subtype);
$filesize = $struct->parts[$i]->bytes;
$encoding = $struct->parts[$i]->encoding;
$attachment_partno = $i + 1;
$data = imap_fetchbody($imap_instance,$message_id,$attachment_partno,FT_UID | FT_PEEK);
if ($encoding == 4) {
    $data = quoted_printable_decode($data);
} elseif ($encoding == 3) {
    $data = base64_decode($data);
}
$mimetypes = array(0 => 'text',1 => 'multipart',2 => 'message',3 => 'application',4 => 'audio',5 => 'image',6 => 'video',7 => 'model',8 => 'other');
header('Content-Type: '.$mimetypes[$type].'/'.$subtype); 
// Force the browser to download the file as an attachment
// The $filename variable is processed from the PARAMETERS and DPARAMETERS values, and decoded with imap_mime_header_decode(). I'm omitting all this for brevity, since this part is working all right.
header('Content-Disposition: attachment; filename="'.$filename.'"');
// Specify the file size for download progress indicators
header('Content-Length: '.$filesize);
// Prevent caching of the file
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Expires: 0');
echo $data;

Clicking on the link, a request to the server is correctly made, and the answer also is correct. Here are the response headers:

HTTP/2 200 
date: Tue, 18 Nov 2025 15:07:57 GMT
content-type: application/pdf
content-length: 408416
access-control-allow-origin: UNSET
content-disposition: attachment; filename="clinical-guidelines-2024-en.pdf"
cache-control: must-revalidate, post-check=0, pre-check=0
pragma: public
expires: 0
strict-transport-security: max-age=63072000; includeSubDomains; preload
permissions-policy: accelerometer=(), ambient-light-sensor=(), attribution-reporting=(), autoplay=(), bluetooth=(), browsing-topics=(), camera=(), compute-pressure=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), local-fonts=(), magnetometer=(), microphone=(), midi=(), otp-credentials=(), payment=(), picture-in-picture=(), publickey-credentials-create=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), storage-access=(), usb=(), web-share=(), window-management=(), xr-spatial-tracking=(), aria-notify=(), captured-surface-control=(), cross-origin-isolated=(), deferred-fetch=(), deferred-fetch-minimal=(), on-device-speech-recognition=(), summarizer=(), wildcards=(), interest-cohort=()
x-dns-prefetch-control: off
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-security-policy: object-src 'none'; form-action 'self'; frame-ancestors 'self';
X-Firefox-Spdy: h2

My platform is Windows 10, and I'm using Firefox 144.0 64-bit.

Any help is appreciated!

10
  • 1
    Please provide a minimal reproducible example. If (as usual) PHP and JavaScript run in separate processes with HTTP communication in between, you will probably be able to excise either of the two languages and maybe even the HTTP server in between. Commented Nov 17 at 20:41
  • Please add the version of Firefox you are experiencing the problem with to your question by editing it. Also the question could benefit if you add the returned headers as well as you mention you diagnose per the webdev network tab tools. Commented Nov 18 at 9:00
  • 1
    "immediately after pops another Alert window" - where does that come from? The code you've shared does not contain any alerts. Additionally, if you needed to decode the file in any way to make it properly readable on the client, where's the code for that? Commented Nov 18 at 9:14
  • 1
    @hakre I have edited the question, and added all the info you asked for. Commented Nov 18 at 22:23
  • 1
    The question is now reopened. If you could move the solution to a self-answer, that would be great. Commented Nov 18 at 23:03

1 Answer 1

2

The problem lays in an incorrect mention to the attachment's size. Effectively, I'm getting the size from the headers returned by the IMAP server:

$filesize = $struct->parts[$i]->bytes;

Then, I fetch the data from the server:

$data = imap_fetchbody($imap_instance,$message_id,$attachment_partno,FT_UID | FT_PEEK);

But, following that, I'm processing the data:

$data = base64_decode($data);

Well, this process, decoding from Base64, obviously changes the size of the data, which no longer matches the initial size.

Moreover, Base64 is roughly 33% larger in size than the plain data; so, if I feed the browser with the former size, it will expect a larger file; when the file transfer concludes, resulting in a smaller file, the browser assumes a network error, and aborts, sometimes without logging any error in the Network tab.

So, the solution is simply to change the line that sets the 'Content-Length' header. Instead of:

header('Content-Length: '.$filesize);

I must use:

header('Content-Length: '.strlen($data));

Now, all is well and working. Note the response headers:

HTTP/2 200 
date: Tue, 18 Nov 2025 15:20:27 GMT
content-type: application/pdf
content-length: 298458
access-control-allow-origin: UNSET
content-disposition: attachment; filename="clinical-guidelines-2024-en.pdf"
cache-control: must-revalidate, post-check=0, pre-check=0
pragma: public
expires: 0
strict-transport-security: max-age=63072000; includeSubDomains; preload
permissions-policy: accelerometer=(), ambient-light-sensor=(), attribution-reporting=(), autoplay=(), bluetooth=(), browsing-topics=(), camera=(), compute-pressure=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), hid=(), identity-credentials-get=(), idle-detection=(), local-fonts=(), magnetometer=(), microphone=(), midi=(), otp-credentials=(), payment=(), picture-in-picture=(), publickey-credentials-create=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), storage-access=(), usb=(), web-share=(), window-management=(), xr-spatial-tracking=(), aria-notify=(), captured-surface-control=(), cross-origin-isolated=(), deferred-fetch=(), deferred-fetch-minimal=(), on-device-speech-recognition=(), summarizer=(), wildcards=(), interest-cohort=()
x-dns-prefetch-control: off
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
content-security-policy: object-src 'none'; form-action 'self'; frame-ancestors 'self';
X-Firefox-Spdy: h2

Basically, only the 'content-length' changed, from 408416 to 298458.

Now, the file downloads normally.

Please note that you could also simply omit the 'Content-Length' header, although in this case the download progress indicators wouldn't work.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.