Skip to content

Attachment::getContent() returns raw MIME part instead of decoded content when email has nested multipart/mixed with double-dash boundary values #512

@junnysolanovega

Description

@junnysolanovega

Describe the bug

Attachment::getContent() and Attachment::save() return/write the full raw MIME
part instead of the decoded attachment content when the email has a nested
multipart/mixed structure and the boundary values in the Content-Type headers
already include the -- prefix.

To Reproduce

  1. Receive or parse an email with the following structure:

    • Root: multipart/mixed (boundary_27)
    • Part 1: multipart/alternative (boundary_26) containing text/plain and text/html
    • Part 2: multipart/mixed (boundary_28) containing an application/octet-stream attachment
  2. Note that the boundary values in the Content-Type headers include -- as prefix:

   Content-Type: multipart/mixed; boundary=--boundary_28_ef2e7fb5-cf91-4186-95ff-f421173253ee

This causes the actual delimiters in the body to have four dashes (----boundary_28...)
instead of the standard two.

  1. Fetch the message and call getAttachments():
   $attachments = $message->getAttachments();
  1. Call getContent() or save() on any attachment:
   $content = $attachments->first()->getContent();
   // or
   $attachments->first()->save($path, $filename);
  1. Observe that instead of the decoded file content, the returned value is the raw
    MIME block including boundary lines, headers and the base64 body unprocessed:
   ----boundary_28_ef2e7fb5-cf91-4186-95ff-f421173253ee
   Content-Type: application/octet-stream; name="text/csv"
   Content-Transfer-Encoding: base64
   ...
   77u/ZmVjaGEsTHVnYXIs...
   ----boundary_28_ef2e7fb5-cf91-4186-95ff-f421173253ee--

The following raw body sample can be used to reproduce the issue:

----boundary_27_e518d7ea-b9aa-481d-9e56-7542504f05f2
Content-Type: multipart/alternative; boundary=--boundary_26_04f7c6a7-a7e7-494c-b32e-f2ed2d42b011

----boundary_26_04f7c6a7-a7e7-494c-b32e-f2ed2d42b011
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit

The report is accessible at the following address: http://example.com/report
----boundary_26_04f7c6a7-a7e7-494c-b32e-f2ed2d42b011--

----boundary_27_e518d7ea-b9aa-481d-9e56-7542504f05f2
Content-Type: multipart/mixed; boundary=--boundary_28_ef2e7fb5-cf91-4186-95ff-f421173253ee

----boundary_28_ef2e7fb5-cf91-4186-95ff-f421173253ee
Content-Type: application/octet-stream; name="text/csv"
Content-Transfer-Encoding: base64
Content-ID: <d62fe95b-6e96-4193-897d-fada2361702b>
Content-Disposition: attachment; filename="=?utf-8?B?RkxUQ1IwMDAxLmNzdg==?="

77u/ZmVjaGEsTHVnYXIsVGlwb19Db21idXN0aWJsZSx2b2x1bWVuLG1vbnRvLElkZW50aWZpY2Fk
b3JfQ2xpZW50ZSxwbGFjYSxJZGVudGlmaWNhZG9yX0NvbXByYQ0KMjAyNi0wMS0wMSAwODowMDow
MCxTVEFUSU9OIE5PUlRILERpZXNlbCw1MC4wMCwiMjYsNTAwLjAwIiwxMDAxLEFBQS0wMDEsNTU1
RTAwNDAxNTMwQUFBQUFBMQ0KMjAyNi0wMS0wMSAwOToxNTowMCxTVEFUSU9OIFNPVVRILFBsdXMg
OTEsMzAuMDAsIjE4LDAwMC4wMCIsMTAwMixCQkItMDAyLDU1NUUwMDQwMTUzMEJCQkJCQjINCjIw
MjYtMDEtMDEgMTA6MzA6MDAsU1RBVElPTiBFQVNULERpZXNlbCw3NS4wMCwiMzksNzUwLjAwIiwx
MDAzLENDQy0wMDMsNTU1RTAwNDAxNTMwQ0NDQ0NDMw0KMjAyNi0wMS0wMSAxMTo0NTowMCxTVEFU
SU9OIFdFU1QsRGllc2VsLDYwLjAwLCIzMSw4MDAuMDAiLDEwMDQsRERELTAwNCw1NTVFMDA0MDE1
MzBEREREREQ0DQoyMDI2LTAxLTAxIDEzOjAwOjAwLFNUQVRJT04gQ0VOVFJBTCxTdXBlciwyNS4w
MCwiMTUsNzUwLjAwIiwxMDA1LEVFRS0wMDUsNTU1RTAwNDAxNTMwRUVFRUVFNQ0K
----boundary_28_ef2e7fb5-cf91-4186-95ff-f421173253ee--

----boundary_27_e518d7ea-b9aa-481d-9e56-7542504f05f2--

Expected behavior

getContent() should return the decoded file content:

fecha,Lugar,Tipo_Combustible,volumen,monto,Identificador_Cliente,placa,Identificador_Compra
2026-01-01 08:00:00,STATION NORTH,Diesel,50.00,"26,500.00",1001,AAA-001,555E00401530AAAAAA1
2026-01-01 09:15:00,STATION SOUTH,Plus 91,30.00,"18,000.00",1002,BBB-002,555E00401530BBBBBB2
2026-01-01 10:30:00,STATION EAST,Diesel,75.00,"39,750.00",1003,CCC-003,555E00401530CCCCCC3
2026-01-01 11:45:00,STATION WEST,Diesel,60.00,"31,800.00",1004,DDD-004,555E00401530DDDDDD4
2026-01-01 13:00:00,STATION CENTRAL,Super,25.00,"15,750.00",1005,EEE-005,555E00401530EEEEEE5

Screenshots

Not applicable.

Desktop / Server

  • OS: Linux (Ubuntu 24)
  • PHP: 8.x
  • webklex/laravel-imap: 6.2.0
  • Laravel: 12

Additional context

Two factors combine to cause the failure:

1 — Double-dash boundary values:
The sender includes -- inside the boundary value in the Content-Type header,
which is non-standard per RFC 2046. This produces four-dash delimiters (----) in
the body instead of the standard two, likely breaking the boundary regex
/boundary=(.*?(?=;)|(.*))/i used by Structure::find_parts().

2 — Missing recursive parsing of nested multipart/mixed:
Instead of recursively entering the inner multipart/mixed (Part 2 / boundary_28)
to extract the application/octet-stream attachment, the library returns the entire
raw MIME block of Part 2 as the attachment content.

Current workaround until this is fixed:

private function decodeMimeAttachment(string $rawContent): string
{
    $parts = preg_split('/\r?\n\r?\n/', $rawContent, 2);

    if (count($parts) < 2) {
        return $rawContent;
    }

    $headers = $parts[0];
    $body = preg_replace('/\r?\n?----boundary.*$/s', '', $parts[1]);

    if (stripos($headers, 'Content-Transfer-Encoding: base64') !== false) {
        $body = base64_decode(str_replace(["\r\n", "\r", "\n"], '', trim($body)));
    }

    return $body;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions