PHPackages                             actengage/talon - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. actengage/talon

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

actengage/talon
===============

Extract original messages from email reply chains — a PHP port of mailgun/talon

v0.1.0(1mo ago)012MITPHPPHP ^8.1CI passing

Since May 6Pushed 1mo agoCompare

[ Source](https://github.com/ActiveEngagement/talon)[ Packagist](https://packagist.org/packages/actengage/talon)[ RSS](/packages/actengage-talon/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (6)Versions (3)Used By (0)

Talon (PHP)
===========

[](#talon-php)

Extract the original message from an email reply chain. A PHP port of [mailgun/talon](https://github.com/mailgun/talon), validated against ~42K real-world Python talon outputs at 99.66% parity.

```
use Actengage\Talon\Facades\Talon;

$reply = Talon::extractFrom($emailBody);                     // auto-detects html vs plain
$reply = Talon::extractFrom($emailBody, 'text/html');        // or be explicit
$reply = Talon::extractFrom($plainText,  'text/plain');
```

Installation
------------

[](#installation)

```
composer require actengage/talon
```

Requires PHP 8.1+, `ext-dom`, `ext-mbstring`. Auto-registers in Laravel via package discovery.

Usage
-----

[](#usage)

### Facade

[](#facade)

```
use Actengage\Talon\Facades\Talon;

Talon::extractFrom($body);                    // auto-detect
Talon::extractFrom($body, 'text/html');
Talon::extractFrom($body, 'text/plain');
```

When `$contentType` is `null` (the default), the body is scanned for HTML block-level tags (``, ``, ``, ``, ``, ``, ``, etc.). If any are present the input is treated as HTML; otherwise as plain text. Pass an explicit content type to override.

### Service

[](#service)

```
use Actengage\Talon\Talon;

(new Talon())->extractFromHtml($html);
(new Talon())->extractFromPlain($text);
```

### Lower-level API

[](#lower-level-api)

For direct access to the text-mode primitives:

```
use Actengage\Talon\TextQuotations;

TextQuotations::extract($text);
TextQuotations::isSplitter($line);            // returns the matched splitter or null
TextQuotations::markLines($lines);            // returns marker string: e/m/s/t/f
TextQuotations::processMarkedLines($lines, $markers, $flags);
```

What it handles
---------------

[](#what-it-handles)

HTMLPlain textGmail, Outlook 2003–2013, Zimbra, Windows Mail`>` quotation blocks (≥3 consecutive)Top-level ```On ,  wrote:` in 9 languages`From:` / `Date:` header blocks (text and tail)`-----Original Message-----` and variantsKnown quote-container IDsMulti-line splitters (≤6 lines)Two-pass for nested forwarded guardsInline replies preserved; forwarded messages skippedUTF-8 throughout (`mb_*` for offsets); both `\n` and `\r\n` delimiters.

Behaviour &amp; limits
----------------------

[](#behaviour--limits)

- Plain-text extraction caps at the first 1,000 lines (`TextQuotations::MAX_LINES`).
- HTML extraction returns the original unchanged when `treeToText()` produces more than 10,000 lines (large marketing emails are passed through, since they rarely contain reply chains).
- The HTML pipeline runs twice (`Talon::extractFromHtml` calls `extractFromHtmlOnce` twice) to mirror Python `talon.batch`. The second pass catches forwarded-message guards that block the first.
- Inputs without recognisable HTML block-level tags are returned as-is to avoid misfires on plain-text bodies that happen to contain `` brackets.

Python parity
-------------

[](#python-parity)

This port is intentionally close to mailgun/talon. The implementation preserves:

- lxml-style `el.text` / `el.tail` traversal in `Talon::walkForText`
- Exact XPath strings for Outlook splitter detection
- `mg:tail()`-equivalent matching in `cutFromBlock` Case 2 via `following-sibling::node()[1][self::text()]`
- Mandatory-newline `[^\n]+\n` per header field in `RE_FROM_COLON_OR_DATE_COLON` (PCRE backtracking equivalent of Python's `[^\n$]+\n`)
- Splitter pattern check order and regex flags
- Checkpoint stamping order (append, not prepend) so markers land on the last line of multi-line text blocks

Validated against the full Active Engagement mailbox dataset (41,977 messages):

MetricCount%Compared41,977100%Matches41,83599.66%Mismatches1420.34%Remaining mismatches are all pre-existing data artefacts (stored Python results computed against a previous version of the body, or invalid-UTF-8 sequences that PHP/libxml2 and Python/lxml decode differently).

Development
-----------

[](#development)

145 Pest tests covering the cutters, splitter detection, marker logic, multilingual splitters, multi-line splitters, inline replies, forwarded-message guards, and end-to-end extraction. The codebase is held to **PHPStan level max** (with Larastan extensions), formatted with **Laravel Pint**, and refactor-checked with **Rector**.

```
composer test          # pest
composer lint          # pint
composer stan          # phpstan max
composer rector:check  # rector dry-run
composer check         # all of the above
```

CI runs all four checks in parallel on every push and PR (`.github/workflows/ci.yml`), with composer / vendor / phpstan / rector caches keyed off `composer.lock` and the workflow concurrency-grouped per ref.

Releases
--------

[](#releases)

Versioning is managed with [Changesets](https://github.com/changesets/changesets). Add a changeset whenever you make a user-facing change:

```
pnpm changeset
```

On push to `main`, `.github/workflows/release.yml` opens a "Version Packages" PR. Merging that PR bumps the version, writes `CHANGELOG.md`, and tags the release. The package is `private: true` in `package.json` — no npm publish happens.

Public API
----------

[](#public-api)

These methods form the stability surface — anything else is internal.

- `Talon::extractFrom(string $body, ?string $contentType = null): string`
- `Talon::detectContentType(string $body): string`
- `Talon::extractFromHtml(string $html): string`
- `Talon::extractFromPlain(string $text): string`
- `TextQuotations::extract(string $text): string`
- `TextQuotations::isSplitter(string $line): ?string`
- `TextQuotations::markLines(array $lines): string`
- `TextQuotations::processMarkedLines(array $lines, string $markers, array &$flags = []): array`

The `HtmlQuotations::cut*` methods and checkpoint helpers are public for testing but should be considered internal.

License
-------

[](#license)

MIT. Originally derived from [mailgun/talon](https://github.com/mailgun/talon) (Apache 2.0).

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance93

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity33

Early-stage or recently created project

 Bus Factor1

Top contributor holds 85.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

34d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/33735047?v=4)[actengage](/maintainers/actengage)[@actengage](https://github.com/actengage)

---

Top Contributors

[![actengage](https://avatars.githubusercontent.com/u/33735047?v=4)](https://github.com/actengage "actengage (6 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

laravelemailmailgunquotationreplytalon

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/actengage-talon/health.svg)

```
[![Health](https://phpackages.com/badges/actengage-talon/health.svg)](https://phpackages.com/packages/actengage-talon)
```

###  Alternatives

[propaganistas/laravel-disposable-email

Disposable email validator

6012.9M7](/packages/propaganistas-laravel-disposable-email)[erag/laravel-disposable-email

A Laravel package to detect and block disposable email addresses.

249143.0k](/packages/erag-laravel-disposable-email)[notebrainslab/filament-email-templates

A powerful and flexible Email Template Management plugin for Filament v4 and v5.

101.1k](/packages/notebrainslab-filament-email-templates)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
