|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +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. |
| 8 | + |
| 9 | +- **Requirements:** PHP 8.2 - 8.5, ext-iconv required |
| 10 | +- **Optional extensions:** ext-dom (CssInliner, PHP 8.4+), ext-fileinfo (attachment type detection), ext-openssl (DKIM signing) |
| 11 | +- **Main dependency:** nette/utils ^4.0 |
| 12 | + |
| 13 | +## Essential Commands |
| 14 | + |
| 15 | +### Testing |
| 16 | + |
| 17 | +```bash |
| 18 | +# Run all tests |
| 19 | +composer run tester |
| 20 | +# or |
| 21 | +vendor/bin/tester tests -s |
| 22 | + |
| 23 | +# Run specific test file |
| 24 | +vendor/bin/tester tests/Mail/Message.phpt -s |
| 25 | + |
| 26 | +# Run tests in specific directory |
| 27 | +vendor/bin/tester tests/Mail/ -s |
| 28 | +``` |
| 29 | + |
| 30 | +### Static Analysis |
| 31 | + |
| 32 | +```bash |
| 33 | +# Run PHPStan analysis (level 5) |
| 34 | +composer run phpstan |
| 35 | +# or |
| 36 | +vendor/bin/phpstan analyse |
| 37 | +``` |
| 38 | + |
| 39 | +## Architecture |
| 40 | + |
| 41 | +### Core Components |
| 42 | + |
| 43 | +The library consists of four main areas: |
| 44 | + |
| 45 | +1. **Email Creation** (`src/Mail/`) |
| 46 | + - `Message` - Main class for composing emails, extends MimePart |
| 47 | + - `MimePart` - Base class handling MIME encoding, headers, and structure |
| 48 | + - Priority constants: `Message::High`, `Message::Normal`, `Message::Low` |
| 49 | + |
| 50 | +2. **Email Sending** (`src/Mail/`) |
| 51 | + - `Mailer` interface - Contract for all mailer implementations |
| 52 | + - `SendmailMailer` - Uses PHP's `mail()` function |
| 53 | + - `SmtpMailer` - Full SMTP protocol implementation with TLS/SSL support |
| 54 | + - `FallbackMailer` - Retry mechanism across multiple mailers |
| 55 | + |
| 56 | +3. **Email Signing** (`src/Mail/`) |
| 57 | + - `Signer` interface - Contract for signing implementations |
| 58 | + - `DkimSigner` - DKIM (DomainKeys Identified Mail) signing using RSA-SHA256 |
| 59 | + |
| 60 | +4. **CSS Inlining** (`src/Mail/`) |
| 61 | + - `CssInliner` - Converts CSS rules to inline `style` attributes for email HTML (requires PHP 8.4+ for `Dom\HTMLDocument`) |
| 62 | + |
| 63 | +### Dependency Injection Integration |
| 64 | + |
| 65 | +`src/Bridges/MailDI/MailExtension.php` - Nette DI compiler extension for configuration. |
| 66 | + |
| 67 | +**DI Services registered:** |
| 68 | +- `mail.mailer` - Mailer instance (SendmailMailer or SmtpMailer based on config) |
| 69 | +- `mail.signer` - DKIM Signer instance (if DKIM is configured) |
| 70 | +- `nette.mailer` - Alias to mail.mailer (for backward compatibility) |
| 71 | + |
| 72 | +**Configuration:** |
| 73 | + |
| 74 | +```neon |
| 75 | +mail: |
| 76 | + # Use SmtpMailer instead of SendmailMailer |
| 77 | + smtp: true # (bool) defaults to false |
| 78 | +
|
| 79 | + # SMTP connection settings |
| 80 | + host: smtp.gmail.com # (string) SMTP server hostname |
| 81 | + port: 587 # (int) defaults: 25, 465 for ssl, 587 for tls |
| 82 | + username: user@example.com |
| 83 | + password: **** |
| 84 | + encryption: tls # (ssl|tls|null) null = no encryption |
| 85 | + timeout: 20 # (int) connection timeout in seconds, default 20 |
| 86 | + persistent: false # (bool) use persistent connection |
| 87 | + clientHost: localhost # (string) defaults to $_SERVER['HTTP_HOST'] or 'localhost' |
| 88 | +
|
| 89 | + # SSL/TLS context options for SMTP connection |
| 90 | + context: |
| 91 | + ssl: |
| 92 | + verify_peer: true # NEVER set to false in production! |
| 93 | + verify_peer_name: true |
| 94 | + allow_self_signed: false # Do not allow self-signed certificates |
| 95 | + # See https://www.php.net/manual/en/context.ssl.php for all options |
| 96 | +
|
| 97 | + # DKIM signing configuration |
| 98 | + dkim: |
| 99 | + domain: example.com # Your domain name |
| 100 | + selector: dkim # DKIM selector from DNS |
| 101 | + privateKey: %appDir%/../dkim/private.key # Path to private key file |
| 102 | + passPhrase: **** # Optional passphrase for private key |
| 103 | +``` |
| 104 | + |
| 105 | +**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. |
| 106 | + |
| 107 | +### Exception Hierarchy |
| 108 | + |
| 109 | +All exceptions in `src/Mail/exceptions.php`: |
| 110 | +- `SendException` - Base exception for sending failures |
| 111 | +- `SmtpException` - SMTP-specific errors (extends SendException) |
| 112 | +- `FallbackMailerException` - All mailers failed (contains array of failures) |
| 113 | +- `SignException` - Signing/verification errors |
| 114 | + |
| 115 | +### Key Features |
| 116 | + |
| 117 | +**Message Creation:** |
| 118 | +- Fluent API with method chaining |
| 119 | +- Automatic text alternative generation from HTML |
| 120 | +- Auto-embedding images from filesystem using `[[...]]` syntax or `<img src=...>` |
| 121 | +- Subject auto-extraction from `<title>` element |
| 122 | +- Attachment support with auto-detection of MIME types |
| 123 | + |
| 124 | +**MIME Handling:** |
| 125 | +- Encoding methods: Base64, 7bit, 8bit, quoted-printable |
| 126 | +- Line length management (76 characters default) |
| 127 | +- Full UTF-8 support throughout |
| 128 | + |
| 129 | +**SMTP Features:** |
| 130 | +- TLS/SSL encryption support (`encryption: 'ssl'` or `'tls'`) |
| 131 | +- Default ports: 25 (unencrypted), 465 (SSL), 587 (TLS) |
| 132 | +- Persistent connections |
| 133 | +- Configurable timeout (default 20s) |
| 134 | +- Custom stream options for SSL context |
| 135 | +- Envelope sender support |
| 136 | +- AUTH PLAIN and LOGIN authentication methods |
| 137 | + |
| 138 | +**DKIM Signing:** |
| 139 | +- RSA-SHA256 signing algorithm |
| 140 | +- Private key passphrase support |
| 141 | +- Automatic header canonicalization |
| 142 | +- Compatible with Gmail, Outlook, and other major providers |
| 143 | + |
| 144 | +## Testing Strategy |
| 145 | + |
| 146 | +Uses Nette Tester with `.phpt` format: |
| 147 | + |
| 148 | +```php |
| 149 | +<?php |
| 150 | +declare(strict_types=1); |
| 151 | + |
| 152 | +use Tester\Assert; |
| 153 | + |
| 154 | +require __DIR__ . '/../bootstrap.php'; |
| 155 | + |
| 156 | +test('Message correctly sets recipient', function () { |
| 157 | + $mail = new Nette\Mail\Message; |
| 158 | + $mail->addTo('test@example.com'); |
| 159 | + |
| 160 | + Assert::same(['test@example.com' => null], $mail->getHeader('To')); |
| 161 | +}); |
| 162 | +``` |
| 163 | + |
| 164 | +- **33 test files** covering all major functionality |
| 165 | +- Test fixtures in `tests/Mail/fixtures/` for email samples |
| 166 | +- Bootstrap in `tests/bootstrap.php` provides `test()` helper function |
| 167 | +- Tests run on PHP 8.2-8.5 in CI |
| 168 | + |
| 169 | +## Coding Standards |
| 170 | + |
| 171 | +Follows Nette Coding Standard (PSR-12 based) with these requirements: |
| 172 | + |
| 173 | +- **Mandatory:** `declare(strict_types=1)` in all PHP files |
| 174 | +- **Indentation:** Tabs (not spaces) |
| 175 | +- **Method spacing:** Two empty lines between methods |
| 176 | +- **Types:** All properties, parameters, and return values must be typed |
| 177 | +- **Documentation:** Only when adding information beyond PHP types |
| 178 | + - Document array contents: `@return string[]` |
| 179 | + - Document nullable relationships: `@param ?string` |
| 180 | + - Skip obvious parameters (width, height, name) |
| 181 | +- **String quotes:** Single quotes unless containing apostrophes |
| 182 | +- **Naming:** PascalCase for classes, camelCase for methods/properties |
| 183 | +- **No prefixes:** No `Abstract`, `Interface`, or `I` prefixes |
| 184 | + |
| 185 | +### Return Type Format |
| 186 | + |
| 187 | +Opening brace on separate line after return type: |
| 188 | + |
| 189 | +```php |
| 190 | +public function send(Message $mail): |
| 191 | +{ |
| 192 | + // method body |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +### phpDoc Examples |
| 197 | + |
| 198 | +```php |
| 199 | +/** |
| 200 | + * Adds email recipient. |
| 201 | + * @param string|array $email Address or [address => name] pairs |
| 202 | + */ |
| 203 | +public function addTo(string|array $email, ?string $name = null): static |
| 204 | + |
| 205 | +/** |
| 206 | + * Sets message priority. |
| 207 | + */ |
| 208 | +public function setPriority(int $priority): static |
| 209 | +``` |
| 210 | + |
| 211 | +## Development Workflow |
| 212 | + |
| 213 | +1. **Before making changes:** |
| 214 | + - Read existing code to understand patterns |
| 215 | + - Check related test files |
| 216 | + - Verify PHPStan passes: `composer run phpstan` |
| 217 | + |
| 218 | +2. **When adding features:** |
| 219 | + - Add corresponding tests in `tests/Mail/` |
| 220 | + - Use `test()` helper for test cases |
| 221 | + - Run tests: `vendor/bin/tester tests -s` |
| 222 | + |
| 223 | +3. **When fixing bugs:** |
| 224 | + - Add regression test first |
| 225 | + - Ensure fix doesn't break existing tests |
| 226 | + - Update PHPDoc if behavior changes |
| 227 | + |
| 228 | +4. **Before committing:** |
| 229 | + - Run full test suite: `composer run tester` |
| 230 | + - Run static analysis: `composer run phpstan` |
| 231 | + - Check code style with Nette Code Checker |
| 232 | + |
| 233 | +## Usage in Nette Application |
| 234 | + |
| 235 | +When using Nette Mail within a full Nette Application (with presenters), you can integrate it with Latte templates and create absolute links using `LinkGenerator`. |
| 236 | + |
| 237 | +### Email Templates with Links |
| 238 | + |
| 239 | +To use `n:href` and `{link}` in email templates, inject both `TemplateFactory` and `LinkGenerator`: |
| 240 | + |
| 241 | +```php |
| 242 | +use Nette; |
| 243 | + |
| 244 | +class MailSender |
| 245 | +{ |
| 246 | + public function __construct( |
| 247 | + private Nette\Application\LinkGenerator $linkGenerator, |
| 248 | + private Nette\Bridges\ApplicationLatte\TemplateFactory $templateFactory, |
| 249 | + ) { |
| 250 | + } |
| 251 | + |
| 252 | + |
| 253 | + private function createTemplate(): Nette\Application\UI\Template |
| 254 | + { |
| 255 | + $template = $this->templateFactory->createTemplate(); |
| 256 | + // Add LinkGenerator as 'uiControl' provider for n:href and {link} |
| 257 | + $template->getLatte()->addProvider('uiControl', $this->linkGenerator); |
| 258 | + return $template; |
| 259 | + } |
| 260 | + |
| 261 | + |
| 262 | + public function sendOrderConfirmation(int $orderId): void |
| 263 | + { |
| 264 | + $template = $this->createTemplate(); |
| 265 | + $html = $template->renderToString(__DIR__ . '/templates/orderEmail.latte', [ |
| 266 | + 'orderId' => $orderId, |
| 267 | + ]); |
| 268 | + |
| 269 | + $mail = new Nette\Mail\Message; |
| 270 | + $mail->setFrom('shop@example.com') |
| 271 | + ->addTo('customer@example.com') |
| 272 | + ->setHtmlBody($html); |
| 273 | + |
| 274 | + $this->mailer->send($mail); |
| 275 | + } |
| 276 | +} |
| 277 | +``` |
| 278 | + |
| 279 | +**Template with absolute links:** |
| 280 | + |
| 281 | +```latte |
| 282 | +<p>Your order #{$orderId} has been confirmed.</p> |
| 283 | +<p><a n:href="Order:detail $orderId">View order details</a></p> |
| 284 | +``` |
| 285 | + |
| 286 | +All links created via `LinkGenerator` are absolute (include full domain), which is required for emails. |
| 287 | + |
| 288 | +## Important Patterns |
| 289 | + |
| 290 | +### Encoding Detection |
| 291 | + |
| 292 | +The library automatically handles encoding with these patterns: |
| 293 | +- Uses `mb_detect_encoding()` for content detection |
| 294 | +- Defaults to UTF-8 for all string operations |
| 295 | +- Converts to ASCII for headers when needed |
| 296 | + |
| 297 | +### Header Management |
| 298 | + |
| 299 | +Headers are case-insensitive and normalized: |
| 300 | +- Storage: lowercase with first letter capitalized |
| 301 | +- Access: case-insensitive lookup |
| 302 | +- Special handling for To, Cc, Bcc, From headers |
| 303 | + |
| 304 | +### Image Embedding |
| 305 | + |
| 306 | +Automatic embedding supports: |
| 307 | +- `<img src="...">` |
| 308 | +- `<body background="...">` |
| 309 | +- CSS `url(...)` in style attributes |
| 310 | +- Special `[[filename]]` syntax |
| 311 | + |
| 312 | +### SendmailMailer Configuration |
| 313 | + |
| 314 | +`SendmailMailer` uses PHP's `mail()` function. To set return path when server overwrites it: |
| 315 | + |
| 316 | +```php |
| 317 | +$mailer = new Nette\Mail\SendmailMailer; |
| 318 | +$mailer->commandArgs = '-fmy@email.com'; // Set return path |
| 319 | +``` |
| 320 | + |
| 321 | +### SMTP Connection |
| 322 | + |
| 323 | +`SmtpMailer` handles SMTP protocol details: |
| 324 | +- Automatic STARTTLS negotiation |
| 325 | +- AUTH PLAIN and LOGIN support |
| 326 | +- Proper QUIT handling in persistent mode |
| 327 | +- Full error message parsing |
| 328 | +- Connection reuse with persistent mode |
| 329 | + |
| 330 | +### CSS Inlining |
| 331 | + |
| 332 | +`CssInliner` converts CSS rules to inline `style` attributes for email HTML compatibility. Uses `Dom\HTMLDocument` (PHP 8.4+) for DOM manipulation and CSS selectors. |
| 333 | + |
| 334 | +```php |
| 335 | +// From <style> tags in HTML (automatically extracted, tag preserved) |
| 336 | +$html = (new CssInliner)->inline($html); |
| 337 | +
|
| 338 | +// External CSS string |
| 339 | +$html = (new CssInliner)->addCss('p { margin: 0; }')->inline($html); |
| 340 | +``` |
| 341 | +
|
| 342 | +**Architecture:** |
| 343 | +- Single-regex tokenizer (comment, whitespace, string, url, escape, at-ident, hash, number, ident, char) |
| 344 | +- Token-based stylesheet parser with CSS nesting and @-rule skipping |
| 345 | +- Declarations parsed into `property => value` arrays (enables deduplication and HTML attribute generation) |
| 346 | +- Cascade: `<style>` → `addCss()` → existing inline `style=""` wins (last declaration wins) |
| 347 | +- `<style>` tags are preserved (keeps @media queries intact) |
| 348 | +- Automatic HTML attribute generation for Outlook compatibility (background-color→bgcolor, width→width, height→height, text-align→align, vertical-align→valign) |
0 commit comments