Imported from nette/union (
mail/AGENTS.md). Install upstream withnpx skills add nette/union --skill mail. Copyright stays with the author.
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.
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.5, ext-iconv required
- Optional extensions: ext-dom (CssInliner, PHP 8.4+), ext-fileinfo (attachment type detection), ext-openssl (DKIM signing)
- Main dependency: nette/utils ^4.1
Essential Commands
Testing
# 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
# Run PHPStan analysis (level 8)
composer run phpstan
# or
vendor/bin/phpstan analyse
Architecture
Core Components
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 behindsetHtmlBody(): optional CSS inlining and image embedding, subject from<title>, plain-text alternative generation- Priority constants:
Message::High,Message::Normal,Message::Low
-
Email Sending (
src/Mail/)Mailerinterface - Contract for all mailer implementationsSendmailMailer- Uses PHP'smail()functionSmtpMailer- Full SMTP protocol implementation with TLS/SSL supportFallbackMailer- Retry mechanism across multiple mailersInterceptor- Wraps any Mailer with optional redirect of outgoing emails to a fixed address (debugging/safety) and an$onSentevent fired after each send (success or failure)
-
Email Signing (
src/Mail/)Signerinterface - Contract for signing implementationsDkimSigner- DKIM (DomainKeys Identified Mail) signing using RSA-SHA256
-
CSS Inlining (
src/Mail/)CssInliner- Converts CSS rules to inlinestyleattributes for email HTML (requires PHP 8.4+ forDom\HTMLDocument)
-
Tracy Bar Panel (
src/Bridges/MailTracy/)MailPanel- IBarPanel showing sent emails in the Tracy Bar; subscribes toInterceptor::$onSent
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 (SendmailMailer/SmtpMailer) or, whenredirectordebugger: trueis configured, anInterceptorwrapping the real transportmail.innerMailer- private real transport (only present whenmail.maileris anInterceptor)mail.signer- DKIM Signer instance (only when DKIM is configured)mail.panel-MailPanelTracy Bar panel (only in debug mode when the Interceptor is active andTracy\Baris registered)nette.mailer- alias tomail.mailer(for backward compatibility)
Configuration:
mail:
# 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, otherwise 25
username: user@example.com
password: ****
encryption: tls # (ssl|tls|null) null = no encryption
timeout: 20 # (int) connection timeout in seconds, default 20
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
passPhrase: **** # Optional passphrase for private key
# Redirect all outgoing emails to a fixed address (debugging/safety)
# Shortcut form: just the address
redirect: dev@example.com
# Or full form with subject prefix:
# redirect:
# to: dev@example.com
# 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 failuresSmtpException- SMTP-specific errors (extends SendException)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
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') - Default ports: 465 (SSL), otherwise 25 (incl. TLS/STARTTLS; set
port: 587explicitly for submission) - Persistent connections
- Configurable timeout (default 20s)
- Custom stream options for SSL context
- Envelope sender support
- AUTH PLAIN and LOGIN authentication methods
DKIM Signing:
- RSA-SHA256 signing algorithm
- Private key passphrase support
- Automatic header canonicalization
- Compatible with Gmail, Outlook, and other major providers
Testing Strategy
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('test@example.com');
Assert::same(['test@example.com' => 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.phpprovidestest()helper function - Tests run on PHP 8.2-8.5 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)
- Document array contents:
- String quotes: Single quotes unless containing apostrophes
- Naming: PascalCase for classes, camelCase for methods/properties
- No prefixes: No
Abstract,Interface, orIprefixes
Return Type Format
Opening brace on separate line after return type:
public function send(Message $mail): void
{
// method body
}
phpDoc Examples
/**
* Adds email recipient. Email or format "John Doe" <doe@example.com>
*/
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
-
Before making changes:
- Read existing code to understand patterns
- Check related test files
- Verify PHPStan passes:
composer run phpstan
-
When adding features:
- Add corresponding tests in
tests/Mail/ - Use
test()helper for test cases - Run tests:
vendor/bin/tester tests -s
- Add corresponding tests in
-
When fixing bugs:
- Add regression test first
- Ensure fix doesn't break existing tests
- Update PHPDoc if behavior changes
-
Before committing:
- Run full test suite:
composer run tester - Run static analysis:
composer run phpstan - Check code style with Nette Code Checker
- Run full test suite:
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:
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('shop@example.com')
->addTo('customer@example.com')
->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.
Important Patterns
Encoding
- All input strings must be valid UTF-8 (
setHeadervalidates viaStrings::checkEncodingand 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 stored and looked up case-sensitively, exactly as passed to
setHeader()/getHeader()(use the canonicalTo,Content-Type, ... spelling) - Email headers (To, Cc, Bcc, From, Reply-To) are stored as
[email => name]arrays; each address is validated setHeaderstrips\r\nfrom 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:
$mailer = new Nette\Mail\SendmailMailer;
$mailer->commandArgs = '-fmy@email.com'; // Set return path manually
SMTP Connection
SmtpMailer handles SMTP protocol details:
- Automatic STARTTLS negotiation
- AUTH PLAIN and LOGIN support
- Proper QUIT handling in persistent mode
- Full error message parsing
- Connection reuse with persistent mode
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.
// 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
property => valuearrays (enables deduplication and HTML attribute generation) - Cascade:
<style>→addCss()→ existing inlinestyle=""wins (last declaration wins) <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; align/valign intentionally excluded, their semantics differ from CSS)