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
Agents
nette/mail
claude-code
cursor
mail
mailer
mime
nette
smtp

It is my fervent wish that this file guide every AI coding agent working with code in this repository.

Install
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)

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

# 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:

  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:

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
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:

public function send(Message $mail): void
{
	// method body
}

phpDoc Examples

/**
 * 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:

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.

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:

$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.

// 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)
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings