Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Mime Mail Parser Laravel Package

messaged/php-mime-mail-parser

Parse MIME emails in PHP with ease. Extract headers, plain and HTML bodies, inline content, and attachments from raw messages through a simple API. Handles nested multipart messages, common transfer encodings, and file saving. Useful for inbox processing, archiving, and email import tools.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the extension first — the package is useless without it.

# Debian/Ubuntu
sudo apt-get install php-mailparse     # or: pecl install mailparse
php -m | grep mailparse                # must print "mailparse"

Then:

composer require messaged/php-mime-mail-parser

If you are on Laravel Sail/Forge/Docker, add the extension to your image now — this is the single most common reason the package "doesn't work" in CI:

RUN pecl install mailparse && docker-php-ext-enable mailparse

2. Where to look first. Read src/Parser.php and src/Attachment.php end-to-end (they are small). Then open the tests/ fixtures — the .eml files there are the fastest way to see how multipart/alternative, inline images, and RFC 2231 filenames are represented. The public surface is basically five methods: setPath/setText/setStream, getHeader/getHeaders/getAddresses, getMessageBody, getAttachments, and Attachment::save.

3. First use case — dump one real email.

use PhpMimeMailParser\Parser;

$parser = new Parser();
$parser->setPath(storage_path('app/mail/sample.eml'));

dump($parser->getHeader('subject'));
dump($parser->getAddresses('from'));       // [['display' => 'Jane', 'address' => '[email protected]']]
dump($parser->getMessageBody('text'));     // plain part
dump($parser->getMessageBody('html'));     // html part

foreach ($parser->getAttachments() as $a) {
    dump($a->getFilename(), $a->getContentType(), strlen($a->getContent()));
}

Start by running this against three real messages from your own mailbox (one plain, one HTML+inline image, one with a PDF). That covers ~90% of the parsing bugs you will hit in production.

Implementation Patterns

Wrap it in a service, never call new Parser() in controllers. The parser is stateful (you set the source, then read), so keep it request/job-scoped and hand back an immutable DTO.

final class ParsedEmail
{
    public function __construct(
        public readonly array $headers,
        public readonly array $from,
        public readonly array $to,
        public readonly string $subject,
        public readonly ?string $text,
        public readonly ?string $html,
        public readonly array $attachments, // Attachment DTOs, not raw
    ) {}
}

final class EmailParser
{
    public function parseFile(string $path): ParsedEmail
    {
        $parser = new Parser();
        $parser->setPath($path);

        return new ParsedEmail(
            headers: $parser->getHeaders(),
            from:    $parser->getAddresses('from'),
            to:      $parser->getAddresses('to'),
            subject: $this->decodeHeader($parser->getHeader('subject')),
            text:    $this->body($parser, 'text'),
            html:    $this->body($parser, 'html'),
            attachments: iterator_to_array($parser->getAttachments()),
        );
    }

    private function body(Parser $p, string $type): ?string
    {
        try { return $p->getMessageBody($type); }
        catch (\Exception) { return null; }   // part absent is normal, not an error
    }
}

Ingest pipeline for a real mailbox. The package parses raw bytes only — it does not speak IMAP. Pair it with webklex/php-imap (or a mail:fetch cron) and do the work in a job:

// Fetch raw, store raw, parse later. Never parse inline in the IMAP loop.
$raw = $client->getMessage($uid)->getRawBody();
$path = "mail/raw/{$uid}.eml";
Storage::put($path, $raw);

ParseIncomingEmail::dispatch($path)->onQueue('mail');

Store the raw .eml on disk/object storage and keep only metadata in the DB. Re-parsing a stored file is free; re-fetching from IMAP often is not.

Attachments go straight to a disk, not into the DB.

foreach ($parser->getAttachments() as $attachment) {
    $name = $attachment->getFilename() ?: 'attachment-'.Str::uuid();
    $safe = Str::slug(pathinfo($name, PATHINFO_FILENAME)).'.'.pathinfo($name, PATHINFO_EXTENSION);

    Storage::disk('mail-attachments')->put(
        "{$messageId}/{$safe}",
        $attachment->getContent()
    );
}

Attachment::save($dir) exists and is convenient for one-offs, but it writes to a local path — bypass it when you're on S3 or need to enforce size caps.

Prefer htmlEmbedded for rendering, html + getAttachments() for archiving. getMessageBody('htmlEmbedded') rewrites cid: references to data: URIs so the message renders standalone in a browser. That is exactly what you want for a "view original email" screen. For storage, keep the parts separate.

Always compute a text fallback.

$text = $parser->getMessageBody('text') ?: trim(strip_tags($html ?? ''));

Multipart/alternative messages sometimes ship an empty or whitespace-only text part; indexers and previews then look broken.

Multiple parts of the same type. When a message has two text/plain parts (common in forwarded mail), getMessageBody('text') returns the first. Newer API versions accept an index — getMessageBody('text', 1) — but verify it against the version you pinned; on the old PHP 5.3-era codebase it may not exist.

Testing. Keep .eml fixtures in tests/Fixtures/mail/ and parse them with setPath(); never mock the parser. Assert on getHeader('subject'), attachment count, and a hash of one attachment body. A single fixture with an Outlook-style winmail.dat and a message/rfc822 forward will catch most regressions.

Gotchas and Tips

  • The extension is a hard dependency and fails loudly at the wrong time. Class 'mailparse' not found at runtime, not at composer install. Add ext-mailparse to composer.json's require so the failure surfaces during install, and add a php -m check to your deploy health check.

  • This specific package is old and effectively unmaintained. messaged/php-mime-mail-parser (PHP 5.3+, ~62 stars, zero dependents) is an ancestor of the actively maintained php-mime-mail-parser/php-mime-mail-parser (eXorus). Before committing, diff the public API — it is largely the same Parser/Attachment surface, so a swap is usually a namespace change plus re-testing fixtures. If you ship this, plan to fork it: you will be the one fixing PHP 8.x deprecations (${var} interpolation, each(), non-nullable implicit params).

  • getHeader() may return the raw MIME-encoded word. A subject line can come back as =?UTF-8?B?...?=. Do not assume decoding:

    $subject = iconv_mime_decode($parser->getHeader('subject') ?? '', 0, 'UTF-8');
    

    getAddresses() does decode display names, which is why the two can look inconsistent.

  • Line endings matter. setText() on a payload with bare \n can produce truncated or empty bodies. If you are parsing from a queue or a copy-pasted string, normalize first: $raw = preg_replace("~\r\n|\r|\n~", "\r\n", $raw);

  • setStream() needs a seekable stream. php://input and non-rewindable socket streams will break; read into a string and use setText(), or copy to php://temp first.

  • setText() holds the entire message (including base64 attachments) in memory. A 25 MB email becomes ~35 MB of PHP strings plus the decoded parts. Cap message size at ingest (upload_max_filesize, IMAP fetch size) and consider a hard reject above a threshold rather than letting PHP hit memory_limit mid-job.

  • Charset conversion is iconv-based and silently lossy on bad bytes. If you see iconv(): Detected an illegal character notices, that is production email, not a bug in your code. Wrap decoding and fall back to mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1') on failure.

  • getMessageBody('html') throws when the part is absent. Catch it — the absence of an HTML or text part is a normal condition, not an exception-worthy one. Wrap it in a body() helper (as above) once and never think about it again.

  • Treat getFilename() as nullable and hostile. It returns the raw filename from the header: wrong charset, ../../etc/passwd, 200-character names, control characters. Slug it, cap the length, and never concatenate it into a path unvalidated.

  • Sanitize the HTML before rendering it anywhere. Inbound email HTML is attacker-controlled: <script>, onerror=, remote <img> tracking pixels, javascript: hrefs. Run it through a sanitizer (e.g. mews/purifier) and consider proxying images. htmlEmbedded makes this harder because it inlines data: URIs — sanitize before you embed, or sanitize the embedded output with data: explicitly allowed.

  • Nested message/rfc822 attachments appear as attachments with the full inner message as content. Decide explicitly whether you store them as .eml blobs or recurse into them — silently treating them as generic files is how attachments "disappear" from user-facing lists.

  • Duplicate Content-IDs and inline images with no Content-ID are common. Key inline images by getContentId(), but keep a positional fallback for the ones missing it.

  • Extension points are thin. There is no plugin/event system — the practical hooks are a decorator around Parser (recommended), or subclassing it if you need to override header decoding. Do not patch vendor/; fork to app/Support/Mime/ and own it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource