It is my fervent wish that this file guide every AI coding agent working with code in this repository.
php artisan boost:add-skill nette/mail
Save this content to: AGENTS.md
# To My Agents!
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
## Documentation
Any distilled, agent-facing documentation for this package - how it works
internally and the rationale behind key design decisions - lives in `docs/`.
Consult it before non-trivial changes; it is the source of truth from which the
public manual is distilled.
`docs/upgrading.md` records, per released version, every change visible to
users of the package (renames, removals, deprecations, signatures, behavior,
configuration), each with its commit hash; internal changes are not in it. It
is the place to look up the history of a public symbol. Do not write to it; it
is extended at release time from the git history.
## Project Overview
Nette Mail is a standalone PHP library for creating and sending emails with support for SMTP, sendmail, DKIM signing, and fallback mechanisms. Part of the Nette Framework ecosystem but usable independently.
- **Requirements:** PHP 8.2 - 8.6, ext-iconv required
- **Optional extensions:** ext-dom (CssInliner, PHP 8.4+), ext-fileinfo (attachment type detection), ext-intl (IDN domains in addresses), ext-openssl (DKIM with an RSA key), ext-sodium (DKIM with an Ed25519 key)
- **Main dependency:** nette/utils ^4.1
## Essential Commands
### Testing
```bash
# Run all tests
composer run tester
# or
vendor/bin/tester tests -s
# Run specific test file
vendor/bin/tester tests/Mail/Message.phpt -s
# Run tests in specific directory
vendor/bin/tester tests/Mail/ -s
```
### Static Analysis
```bash
# Run PHPStan analysis (level 8)
composer run phpstan
# or
vendor/bin/phpstan analyse
```
## Architecture
### Core Components
The library consists of five main areas:
1. **Email Creation** (`src/Mail/`)
- `Message` - Main class for composing emails, extends MimePart
- `MimePart` - Base class handling MIME encoding, headers, and structure
- `HtmlComposer` - Pipeline behind `setHtmlBody()`: optional CSS inlining and image embedding, subject from `<title>`, plain-text alternative generation
- Priority constants: `Message::High`, `Message::Normal`, `Message::Low`
2. **Email Sending** (`src/Mail/`)
- `Mailer` interface - Contract for all mailer implementations
- `SendmailMailer` - Uses PHP's `mail()` function
- `SmtpMailer` - Full SMTP protocol implementation with TLS/SSL support and XOAUTH2
- `FileMailer` - Writes emails to `.eml` files instead of sending them (development, tests)
- `FallbackMailer` - Retry mechanism across multiple mailers; a mailer failing permanently is not retried
- `Interceptor` - Wraps any Mailer with optional redirect of outgoing emails to a fixed address (debugging/safety) and an `$onSent` event fired after each send (success or failure)
3. **Email Signing** (`src/Mail/`)
- `Signer` interface - Contract for signing implementations
- `DkimSigner` - DKIM (DomainKeys Identified Mail) signing using RSA-SHA256 or Ed25519-SHA256
4. **CSS Inlining** (`src/Mail/`)
- `CssInliner` - Converts CSS rules to inline `style` attributes for email HTML (requires PHP 8.4+ for `Dom\HTMLDocument`)
5. **Tracy Bar Panel** (`src/Bridges/MailTracy/`)
- `MailPanel` - IBarPanel showing sent emails in the Tracy Bar; subscribes to `Interceptor::$onSent`. Each message has a collapsed preview: the HTML body in a sandboxed iframe (`cid:` images inlined as `data:` URIs), the text body and the attachment list
### Dependency Injection Integration
`src/Bridges/MailDI/MailExtension.php` - Nette DI compiler extension for configuration.
**DI Services registered:**
- `mail.mailer` - autowired public Mailer; either the real transport directly (FileMailer when `file` is set, otherwise SmtpMailer/SendmailMailer by `smtp`) or, when `redirect` or `debugger: true` is configured, an `Interceptor` wrapping the real transport
- `mail.innerMailer` - private real transport (only present when `mail.mailer` is an `Interceptor`)
- `mail.signer` - DKIM Signer instance (only when DKIM is configured)
- `mail.panel` - `MailPanel` Tracy Bar panel (only in debug mode when the Interceptor is active and `Tracy\Bar` is registered)
- `nette.mailer` - alias to `mail.mailer` (for backward compatibility)
**Configuration:**
```neon
mail:
# Write emails as .eml files into a directory instead of sending them (FileMailer);
# takes precedence over smtp/sendmail, DKIM and redirect still apply
file: %tempDir%/mails # (string|null) defaults to null
# Use SmtpMailer instead of SendmailMailer
smtp: true # (bool) defaults to false
# SMTP connection settings
host: smtp.gmail.com # (string) SMTP server hostname
port: 587 # (int) defaults: 465 for ssl, 587 for tls, otherwise 25
username: [email protected]
password: ****
# OAuth 2.0 access token for XOAUTH2 (needs 'smtp: true' and no 'file', otherwise a configuration error):
# a string, a dynamic parameter, or a callable resolved on every connection so an expiring token can be refreshed
accessToken: @App\GoogleOAuth::getAccessToken(...)
encryption: tls # (ssl|tls|null) null = no encryption
timeout: 20 # (int) connection timeout in seconds, default 20; 0 leaves it to PHP's default_socket_timeout
persistent: false # (bool) use persistent connection
clientHost: localhost # (string) defaults to $_SERVER['HTTP_HOST'] or 'localhost'
# SSL/TLS context options for SMTP connection
context:
ssl:
verify_peer: true # NEVER set to false in production!
verify_peer_name: true
allow_self_signed: false # Do not allow self-signed certificates
# See https://www.php.net/manual/en/context.ssl.php for all options
# DKIM signing configuration
dkim:
domain: example.com # Your domain name
selector: dkim # DKIM selector from DNS
privateKey: %appDir%/../dkim/private.key # Path to private key file (PEM RSA, or raw base64 Ed25519)
passPhrase: **** # Optional passphrase for private key
# Redirect all outgoing emails to a fixed address (debugging/safety)
# Shortcut form: just the address
redirect: [email protected]
# Or full form with subject prefix:
# redirect:
# to: [email protected]
# subjectPrefix: '[debug]'
# Tracy panel:
# - default: attached when Interceptor is active, in debug mode, and Tracy\Bar exists
# - 'debugger: true' explicitly opts in (and forces Interceptor wiring even without redirect)
# - 'debugger: false' explicitly opts out
debugger: true
```
**Security Warning:** Never disable SSL certificate verification (`verify_peer: false`) as it makes your application vulnerable to man-in-the-middle attacks. Instead, add certificates to the trust store if needed.
### Exception Hierarchy
All exceptions in `src/Mail/exceptions.php`:
- `SendException` - Base exception for sending failures; `isPermanent()` tells whether a retry is pointless
- `SmtpException` - SMTP-specific errors (extends SendException); `fromReply()` marks a 5xx reply as permanent
- `FallbackMailerException` - All mailers failed (contains array of failures)
- `SignException` - Signing/verification errors
### Key Features
**Message Creation:**
- Fluent API with method chaining
- Automatic text alternative generation from HTML
- Auto-embedding images from filesystem using `[[...]]` syntax or `<img src=...>`
- Subject auto-extraction from `<title>` element
- Attachment support with auto-detection of MIME types; parts are readable via `getAttachments()` and `getEmbeddedFiles()`
- One-click unsubscribe headers via `setUnsubscribe()` (RFC 2369, RFC 8058)
- Internationalized (IDN) domains in addresses converted to punycode (ext-intl)
**MIME Handling:**
- Encoding methods: Base64, 7bit, 8bit, quoted-printable
- Line length management (76 characters default)
- Full UTF-8 support throughout
**SMTP Features:**
- TLS/SSL encryption support (`encryption: 'ssl'` or `'tls'`); any other value throws, it never falls back to a plain connection
- Default ports: 465 (SSL), 587 (TLS/STARTTLS), otherwise 25
- STARTTLS accepts only TLS 1.2 and 1.3
- Persistent connections (probed with NOOP, waiting at most 5 s, and re-established when the server dropped them)
- Configurable timeout (default 20s)
- Custom stream options for SSL context
- Envelope sender support
- AUTH PLAIN, LOGIN and XOAUTH2 (`setAccessToken()`, or the `accessToken` config key), only with mechanisms the server advertises
**DKIM Signing:**
- RSA-SHA256 or Ed25519-SHA256 (RFC 8463) signing algorithm, chosen by the key format
- Optional oversigning of headers (`$oversignHeaders`)
- Private key passphrase support (RSA)
- Automatic header canonicalization
- Compatible with Gmail, Outlook, and other major providers
## Testing Strategy
Uses Nette Tester with `.phpt` format:
```php
<?php
declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/../bootstrap.php';
test('Message correctly sets recipient', function () {
$mail = new Nette\Mail\Message;
$mail->addTo('[email protected]');
Assert::same(['[email protected]' => null], $mail->getHeader('To'));
});
```
- Test files in `tests/Mail/` cover all major functionality
- Test fixtures in `tests/Mail/fixtures/` for email samples
- Bootstrap in `tests/bootstrap.php` provides `test()` helper function
- Tests run on PHP 8.2-8.6 in CI
## Coding Standards
Follows Nette Coding Standard (PSR-12 based) with these requirements:
- **Mandatory:** `declare(strict_types=1)` in all PHP files
- **Indentation:** Tabs (not spaces)
- **Method spacing:** Two empty lines between methods
- **Types:** All properties, parameters, and return values must be typed
- **Documentation:** Only when adding information beyond PHP types
- Document array contents: `@return string[]`
- Document nullable relationships: `@param ?string`
- Skip obvious parameters (width, height, name)
- **String quotes:** Single quotes unless containing apostrophes
- **Naming:** PascalCase for classes, camelCase for methods/properties
- **No prefixes:** No `Abstract`, `Interface`, or `I` prefixes
### Return Type Format
Opening brace on separate line after return type:
```php
public function send(Message $mail): void
{
// method body
}
```
### phpDoc Examples
```php
/**
* Adds email recipient. Email or format "John Doe" <[email protected]>
*/
public function addTo(string $email, ?string $name = null): static
/**
* Sets a header.
* @param string|array<string, ?string>|null $value value or pair email => name
*/
public function setHeader(string $name, string|array|null $value, bool $append = false): static
```
## Development Workflow
1. **Before making changes:**
- Read existing code to understand patterns
- Check related test files
- Verify PHPStan passes: `composer run phpstan`
2. **When adding features:**
- Add corresponding tests in `tests/Mail/`
- Use `test()` helper for test cases
- Run tests: `vendor/bin/tester tests -s`
3. **When fixing bugs:**
- Add regression test first
- Ensure fix doesn't break existing tests
- Update PHPDoc if behavior changes
4. **Before committing:**
- Run full test suite: `composer run tester`
- Run static analysis: `composer run phpstan`
- Check code style with DressCode (configured in `dresscode.neon`)
## Usage in Nette Application
When using Nette Mail within a full Nette Application (with presenters), you can integrate it with Latte templates and create absolute links using `LinkGenerator`.
### Email Templates with Links
To use `n:href` and `{link}` in email templates, inject both `TemplateFactory` and `LinkGenerator`:
```php
use Nette;
class MailSender
{
public function __construct(
private Nette\Application\LinkGenerator $linkGenerator,
private Nette\Bridges\ApplicationLatte\TemplateFactory $templateFactory,
) {
}
private function createTemplate(): Nette\Application\UI\Template
{
$template = $this->templateFactory->createTemplate();
// Add LinkGenerator as 'uiControl' provider for n:href and {link}
$template->getLatte()->addProvider('uiControl', $this->linkGenerator);
return $template;
}
public function sendOrderConfirmation(int $orderId): void
{
$template = $this->createTemplate();
$html = $template->renderToString(__DIR__ . '/templates/orderEmail.latte', [
'orderId' => $orderId,
]);
$mail = new Nette\Mail\Message;
$mail->setFrom('[email protected]')
->addTo('[email protected]')
->setHtmlBody($html);
$this->mailer->send($mail);
}
}
```
**Template with absolute links:**
```latte
<p>Your order #{$orderId} has been confirmed.</p>
<p><a n:href="Order:detail $orderId">View order details</a></p>
```
All links created via `LinkGenerator` are absolute (include full domain), which is required for emails.
## Important Patterns
### Encoding
- All input strings must be valid UTF-8 (`setHeader` validates via `Validators::isUnicode` and throws otherwise)
- Non-ASCII header values are encoded as RFC 2047 base64 encoded-words
- Body transfer encoding (7bit/8bit/quoted-printable) is chosen automatically by content sniffing
### Header Management
- Header names are looked up **case-insensitively**; the spelling used first is kept as the stored key and emitted as is
- Email headers (To, Cc, Bcc, From, Reply-To) are stored as `[email => name]` arrays; each address is validated and an IDN domain is converted to punycode
- `setHeader` strips `\r\n` from values - it is the injection-prevention boundary
### Image Embedding
Automatic embedding supports:
- `<img src="...">`
- `<body background="...">`
- CSS `url(...)` in style attributes
- Special `[[filename]]` syntax
### SendmailMailer Configuration
`SendmailMailer` uses PHP's `mail()` function. The envelope sender (`-f` option) is set automatically from the From header; disable with `setEnvelopeSender(false)` or pass custom arguments:
```php
$mailer = new Nette\Mail\SendmailMailer;
$mailer->commandArgs = '[email protected]'; // Set return path manually
```
### SMTP Connection
`SmtpMailer` handles SMTP protocol details:
- Automatic STARTTLS negotiation (TLS 1.2/1.3 only)
- AUTH PLAIN, LOGIN and XOAUTH2 support, chosen from the mechanisms the server advertises
- HELO fallback when EHLO is rejected, only without credentials; with them an `SmtpException` names the rejected EHLO (permanent for 5xx, retryable for 4xx)
- Proper QUIT handling in persistent mode
- Full error message parsing; 5xx replies raise a permanent `SmtpException`
- Connection reuse with persistent mode; a dropped connection is detected by NOOP and reopened, any exception closes it
### CSS Inlining
`CssInliner` converts CSS rules to inline `style` attributes for email HTML compatibility. Uses `Dom\HTMLDocument` (PHP 8.4+) for DOM manipulation and CSS selectors.
```php
// From <style> tags in HTML (automatically extracted, tag preserved)
$html = (new CssInliner)->inline($html);
// External CSS string
$html = (new CssInliner)->addCss('p { margin: 0; }')->inline($html);
```
**Architecture:**
- Single-regex tokenizer (comment, whitespace, string, url, escape, at-ident, hash, number, ident, char)
- Token-based stylesheet parser with CSS nesting and @-rule skipping
- Declarations parsed into ordered `[property, value]` lists with duplicates kept; `getRules()` exposes them as `property => value` with the last value
- Cascade per property, ranked `!important` > existing inline `style=""` > selector specificity (per comma-separated part) > source order (`<style>` → `addCss()`); every candidate is written ordered by rank, the winner last, so client fallbacks (`display: -webkit-box; display: flex`) keep working; a value repeated by a higher-ranked candidate is dropped
- Property names lowercased except custom properties (`--x`); an existing inline style that cannot be parsed is kept verbatim
- `<style>` tags are preserved (keeps @media queries intact)
- Automatic HTML attribute generation for Outlook compatibility (background-color→bgcolor, width→width, height→height, border-spacing→cellspacing; numeric ones only for plain lengths/percentages; align/valign intentionally excluded, their semantics differ from CSS)
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
Any distilled, agent-facing documentation for this package - how it works
internally and the rationale behind key design decisions - lives in docs/.
Consult it before non-trivial changes; it is the source of truth from which the
public manual is distilled.
docs/upgrading.md records, per released version, every change visible to
users of the package (renames, removals, deprecations, signatures, behavior,
configuration), each with its commit hash; internal changes are not in it. It
is the place to look up the history of a public symbol. Do not write to it; it
is extended at release time from the git history.
Nette Mail is a standalone PHP library for creating and sending emails with support for SMTP, sendmail, DKIM signing, and fallback mechanisms. Part of the Nette Framework ecosystem but usable independently.
# Run all tests
composer run tester
# or
vendor/bin/tester tests -s
# Run specific test file
vendor/bin/tester tests/Mail/Message.phpt -s
# Run tests in specific directory
vendor/bin/tester tests/Mail/ -s
# Run PHPStan analysis (level 8)
composer run phpstan
# or
vendor/bin/phpstan analyse
The library consists of five main areas:
Email Creation (src/Mail/)
Message - Main class for composing emails, extends MimePartMimePart - Base class handling MIME encoding, headers, and structureHtmlComposer - Pipeline behind setHtmlBody(): optional CSS inlining and image embedding, subject from <title>, plain-text alternative generationMessage::High, Message::Normal, Message::LowEmail Sending (src/Mail/)
Mailer interface - Contract for all mailer implementationsSendmailMailer - Uses PHP's mail() functionSmtpMailer - Full SMTP protocol implementation with TLS/SSL support and XOAUTH2FileMailer - Writes emails to .eml files instead of sending them (development, tests)FallbackMailer - Retry mechanism across multiple mailers; a mailer failing permanently is not retriedInterceptor - Wraps any Mailer with optional redirect of outgoing emails to a fixed address (debugging/safety) and an $onSent event fired after each send (success or failure)Email Signing (src/Mail/)
Signer interface - Contract for signing implementationsDkimSigner - DKIM (DomainKeys Identified Mail) signing using RSA-SHA256 or Ed25519-SHA256CSS Inlining (src/Mail/)
CssInliner - Converts CSS rules to inline style attributes for email HTML (requires PHP 8.4+ for Dom\HTMLDocument)Tracy Bar Panel (src/Bridges/MailTracy/)
MailPanel - IBarPanel showing sent emails in the Tracy Bar; subscribes to Interceptor::$onSent. Each message has a collapsed preview: the HTML body in a sandboxed iframe (cid: images inlined as data: URIs), the text body and the attachment listsrc/Bridges/MailDI/MailExtension.php - Nette DI compiler extension for configuration.
DI Services registered:
mail.mailer - autowired public Mailer; either the real transport directly (FileMailer when file is set, otherwise SmtpMailer/SendmailMailer by smtp) or, when redirect or debugger: true is configured, an Interceptor wrapping the real transportmail.innerMailer - private real transport (only present when mail.mailer is an Interceptor)mail.signer - DKIM Signer instance (only when DKIM is configured)mail.panel - MailPanel Tracy Bar panel (only in debug mode when the Interceptor is active and Tracy\Bar is registered)nette.mailer - alias to mail.mailer (for backward compatibility)Configuration:
mail:
# Write emails as .eml files into a directory instead of sending them (FileMailer);
# takes precedence over smtp/sendmail, DKIM and redirect still apply
file: %tempDir%/mails # (string|null) defaults to null
# Use SmtpMailer instead of SendmailMailer
smtp: true # (bool) defaults to false
# SMTP connection settings
host: smtp.gmail.com # (string) SMTP server hostname
port: 587 # (int) defaults: 465 for ssl, 587 for tls, otherwise 25
username: [email protected]
password: ****
# OAuth 2.0 access token for XOAUTH2 (needs 'smtp: true' and no 'file', otherwise a configuration error):
# a string, a dynamic parameter, or a callable resolved on every connection so an expiring token can be refreshed
accessToken: @App\GoogleOAuth::getAccessToken(...)
encryption: tls # (ssl|tls|null) null = no encryption
timeout: 20 # (int) connection timeout in seconds, default 20; 0 leaves it to PHP's default_socket_timeout
persistent: false # (bool) use persistent connection
clientHost: localhost # (string) defaults to $_SERVER['HTTP_HOST'] or 'localhost'
# SSL/TLS context options for SMTP connection
context:
ssl:
verify_peer: true # NEVER set to false in production!
verify_peer_name: true
allow_self_signed: false # Do not allow self-signed certificates
# See https://www.php.net/manual/en/context.ssl.php for all options
# DKIM signing configuration
dkim:
domain: example.com # Your domain name
selector: dkim # DKIM selector from DNS
privateKey: %appDir%/../dkim/private.key # Path to private key file (PEM RSA, or raw base64 Ed25519)
passPhrase: **** # Optional passphrase for private key
# Redirect all outgoing emails to a fixed address (debugging/safety)
# Shortcut form: just the address
redirect: [email protected]
# Or full form with subject prefix:
# redirect:
# to: [email protected]
# subjectPrefix: '[debug]'
# Tracy panel:
# - default: attached when Interceptor is active, in debug mode, and Tracy\Bar exists
# - 'debugger: true' explicitly opts in (and forces Interceptor wiring even without redirect)
# - 'debugger: false' explicitly opts out
debugger: true
Security Warning: Never disable SSL certificate verification (verify_peer: false) as it makes your application vulnerable to man-in-the-middle attacks. Instead, add certificates to the trust store if needed.
All exceptions in src/Mail/exceptions.php:
SendException - Base exception for sending failures; isPermanent() tells whether a retry is pointlessSmtpException - SMTP-specific errors (extends SendException); fromReply() marks a 5xx reply as permanentFallbackMailerException - All mailers failed (contains array of failures)SignException - Signing/verification errorsMessage Creation:
[[...]] syntax or <img src=...><title> elementgetAttachments() and getEmbeddedFiles()setUnsubscribe() (RFC 2369, RFC 8058)MIME Handling:
SMTP Features:
encryption: 'ssl' or 'tls'); any other value throws, it never falls back to a plain connectionsetAccessToken(), or the accessToken config key), only with mechanisms the server advertisesDKIM Signing:
$oversignHeaders)Uses Nette Tester with .phpt format:
<?php
declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/../bootstrap.php';
test('Message correctly sets recipient', function () {
$mail = new Nette\Mail\Message;
$mail->addTo('[email protected]');
Assert::same(['[email protected]' => null], $mail->getHeader('To'));
});
tests/Mail/ cover all major functionalitytests/Mail/fixtures/ for email samplestests/bootstrap.php provides test() helper functionFollows Nette Coding Standard (PSR-12 based) with these requirements:
declare(strict_types=1) in all PHP files@return string[]@param ?stringAbstract, Interface, or I prefixesOpening brace on separate line after return type:
public function send(Message $mail): void
{
// method body
}
/**
* Adds email recipient. Email or format "John Doe" <[email protected]>
*/
public function addTo(string $email, ?string $name = null): static
/**
* Sets a header.
* @param string|array<string, ?string>|null $value value or pair email => name
*/
public function setHeader(string $name, string|array|null $value, bool $append = false): static
Before making changes:
composer run phpstanWhen adding features:
tests/Mail/test() helper for test casesvendor/bin/tester tests -sWhen fixing bugs:
Before committing:
composer run testercomposer run phpstandresscode.neon)When using Nette Mail within a full Nette Application (with presenters), you can integrate it with Latte templates and create absolute links using LinkGenerator.
To use n:href and {link} in email templates, inject both TemplateFactory and LinkGenerator:
use Nette;
class MailSender
{
public function __construct(
private Nette\Application\LinkGenerator $linkGenerator,
private Nette\Bridges\ApplicationLatte\TemplateFactory $templateFactory,
) {
}
private function createTemplate(): Nette\Application\UI\Template
{
$template = $this->templateFactory->createTemplate();
// Add LinkGenerator as 'uiControl' provider for n:href and {link}
$template->getLatte()->addProvider('uiControl', $this->linkGenerator);
return $template;
}
public function sendOrderConfirmation(int $orderId): void
{
$template = $this->createTemplate();
$html = $template->renderToString(__DIR__ . '/templates/orderEmail.latte', [
'orderId' => $orderId,
]);
$mail = new Nette\Mail\Message;
$mail->setFrom('[email protected]')
->addTo('[email protected]')
->setHtmlBody($html);
$this->mailer->send($mail);
}
}
Template with absolute links:
<p>Your order #{$orderId} has been confirmed.</p>
<p><a n:href="Order:detail $orderId">View order details</a></p>
All links created via LinkGenerator are absolute (include full domain), which is required for emails.
setHeader validates via Validators::isUnicode and throws otherwise)[email => name] arrays; each address is validated and an IDN domain is converted to punycodesetHeader strips \r\n from values - it is the injection-prevention boundaryAutomatic embedding supports:
<img src="..."><body background="...">url(...) in style attributes[[filename]] syntaxSendmailMailer uses PHP's mail() function. The envelope sender (-f option) is set automatically from the From header; disable with setEnvelopeSender(false) or pass custom arguments:
$mailer = new Nette\Mail\SendmailMailer;
$mailer->commandArgs = '[email protected]'; // Set return path manually
SmtpMailer handles SMTP protocol details:
SmtpException names the rejected EHLO (permanent for 5xx, retryable for 4xx)SmtpExceptionCssInliner converts CSS rules to inline style attributes for email HTML compatibility. Uses Dom\HTMLDocument (PHP 8.4+) for DOM manipulation and CSS selectors.
// From <style> tags in HTML (automatically extracted, tag preserved)
$html = (new CssInliner)->inline($html);
// External CSS string
$html = (new CssInliner)->addCss('p { margin: 0; }')->inline($html);
Architecture:
[property, value] lists with duplicates kept; getRules() exposes them as property => value with the last value!important > existing inline style="" > selector specificity (per comma-separated part) > source order (<style> → addCss()); every candidate is written ordered by rank, the winner last, so client fallbacks (display: -webkit-box; display: flex) keep working; a value repeated by a higher-ranked candidate is dropped--x); an existing inline style that cannot be parsed is kept verbatim<style> tags are preserved (keeps @media queries intact)How can I help you explore Laravel packages today?