Sending Emails

Info: Zero Dependencies: Complete RFC 5321 compliant SMTP implementation without external libraries. Symfony Mailer support coming after PHP 8.5 GA.

Overview

Larafony's Mail component provides:

Quick Start

1. Configuration

use Larafony\Framework\Mail\MailerFactory;

// From DSN with smart defaults
$mailer = MailerFactory::fromDsn('smtp://user:pass@smtp.example.com:587');

// MailHog for local development
$mailer = MailerFactory::createMailHogMailer('localhost', 1025);

Success: Development Tip: Use MailHog to test emails locally without sending real messages. Install with Docker: docker run -p 1025:1025 -p 8025:8025 mailhog/mailhog

2. Create a Mailable Class

namespace App\Mail;

use Larafony\Framework\Mail\Mailable;
use Larafony\Framework\Mail\Envelope;
use Larafony\Framework\Mail\Content;
use Larafony\Framework\Mail\Address;

class WelcomeEmail extends Mailable
{
public function __construct(
private readonly string $userName,
private readonly string $userEmail
) {}

protected function envelope(): Envelope
{
return (new Envelope())
->from(new Address('noreply@example.com', 'Larafony'))
->to(new Address($this->userEmail))
->subject('Welcome to Larafony!');
}

protected function content(): Content
{
return new Content(
view: 'emails.welcome',
data: ['userName' => $this->userName]
);
}
}

3. Create Email View

Create resources/views/emails/welcome.blade.php:

@component('components.Layout', ['title' => 'Welcome'])

<h1>Welcome to Larafony!</h1>

Hello **{{ $userName }}**,

Thank you for joining Larafony! We're excited to have you on board.

[
Get Started
](https://github.com/larafony/framework)

@endcomponent

4. Send the Email

$mailer->send(new WelcomeEmail('John Doe', 'john@example.com'));

DSN Configuration

DSN format: smtp://[username:password@]host[:port]

// Basic SMTP (port 25)
$mailer = MailerFactory::fromDsn('smtp://localhost');

// With authentication (port 587 for TLS by default)
$mailer = MailerFactory::fromDsn('smtp://user:pass@smtp.gmail.com:587');

// SSL (port 465)
$mailer = MailerFactory::fromDsn('smtps://user:pass@smtp.gmail.com:465');

// Explicit TLS
$mailer = MailerFactory::fromDsn('smtp+tls://user:pass@smtp.example.com:587');

Smart Port Defaults

Advanced Usage

Multiple Recipients

protected function envelope(): Envelope
{
return (new Envelope())
->from(new Address('noreply@example.com', 'Larafony'))
->to(new Address('user1@example.com'))
->to(new Address('user2@example.com'))
->cc(new Address('manager@example.com'))
->bcc(new Address('admin@example.com'))
->replyTo(new Address('support@example.com'))
->subject('Team Update');
}

Email Logging

Track sent emails in the database:

use Larafony\Framework\Mail\MailHistoryLogger;
use Larafony\Framework\Mail\Mailer;

$logger = new MailHistoryLogger();
$mailer = new Mailer($transport, $logger);

// Emails are automatically logged when sent
$mailer->send($mailable);

Create the mail_log table:

php bin/larafony table:mail-log
php bin/larafony migrate

SMTP Protocol Details

Implemented Commands

Response Codes

Multi-line Response Handling

SMTP responses can span multiple lines. The implementation detects the last line by checking character at index 3:

250-mail.example.com
250-SIZE 52428800
250-8BITMIME
250 HELP
^ Space indicates this is the final line

Info: RFC Compliance: Our implementation follows RFC 5321 specification for SMTP protocol.

PHP 8.5 Features

Asymmetric Visibility

Email and Envelope use private(set) for immutability:

final class Email
{
public function __construct(
public private(set) ?Address $from = null,
public private(set) array $to = [],
public private(set) ?string $subject = null,
) {}

// Immutable API using clone()
public function from(Address $address): self
{
return clone($this, ['from' => $address]);
}

public function to(Address $address): self
{
return clone($this, ['to' => [...$this->to, $address]]);
}
}

Property Hooks

Smart defaults with property hooks:

final class MailEncryption
{
public bool $isSsl {
get => $this->value === 'ssl';
}

public bool $isTls {
get => $this->value === 'tls';
}

private function __construct(
public private(set) string $value
) {}
}

Warning: Important Caveat: Properties with private(set) cannot use reference-based operations like array_walk(). According to RFC Asymmetric Visibility v2, obtaining a reference follows set visibility, not get visibility. Use foreach for read-only iteration.

Architecture

Value Objects

Contracts (Interfaces)

interface TransportContract
{
public function send(Email $message): void;
}

interface MailerContract
{
public function send(Mailable $mailable): void;
}

interface MailHistoryLoggerContract
{
public function log(Email $message): void;
}

Framework Integration

Testing

Using MailHog

# Start MailHog with Docker
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog

# View sent emails at http://localhost:8025
use Larafony\Framework\Mail\MailerFactory;

$mailer = MailerFactory::createMailHogMailer();
$mailer->send(new WelcomeEmail('Test User', 'test@example.com'));

// Check http://localhost:8025 to see the email

Future Enhancements

After PHP 8.5 GA release and base implementation completion:

Resources

Learn More

This native SMTP implementation is explained in detail with RFC compliance, PHP 8.5 features, and production-ready patterns at masterphp.eu

Zero Dependencies: Unlike other frameworks that rely on Symfony Mailer or SwiftMailer, Larafony implements SMTP from scratch using only PSR standards and PHP 8.5. This demonstrates complete framework transparency - you can read and understand every line of the mail system without diving into external libraries.

Coming Soon: Symfony Mailer integration will be added as an optional transport after PHP 8.5 GA, giving you the choice between native implementation and battle-tested external solutions.

View on Packagist View on GitHub