# Native Mailer
Native Mailer is a cross-platform desktop app that runs a local SMTP server for catching and viewing emails during development. Point your app at it and every email it sends lands in a local inbox instead of a real one. Built with Laravel, NativePHP and Filament; no external services needed.
See FEATURES.md for a full guide to every feature and how it works.
- Local SMTP server on
127.0.0.1:1025, started automatically with the app - Accepts any login: apps with
MAIL_USERNAME/MAIL_PASSWORDset deliver without changes - Every recipient: To, CC, and BCC recovered from the SMTP envelope
- Decodes real-world mail: any charset to UTF-8, quoted-printable/base64, encoded subjects and filenames, nested multipart, inline images, calendar invites
- Size limit: messages over 50 MB are refused with
552(configurable)
- Live updates: new mail appears the moment it's captured, with a desktop notification
- Search by sender, recipients, subject, or message body
- Catcher status under the title: running, port taken by another program, or down (with the reason)
- Unread badge on the app icon (macOS and Linux)
- Read/unread state, filters, bulk actions, and a paperclip for emails with attachments
- Tabs for HTML, HTML source, text, headers, raw source and attachments
- Responsive preview at desktop, tablet (768px) and mobile (375px) widths
- Links open in your browser; the preview never navigates away
- Locked-down preview: sandboxed iframe with a Content-Security-Policy that blocks the email's scripts
- HTML checks: flags development URLs, broken links, missing alt text, Gmail clipping, missing text part, and CSS Outlook ignores
- Release a captured email, unchanged, through a real SMTP relay to a real inbox
- Retention: delete emails older than N days and/or keep at most N emails, applied hourly
- Delete all in one click, compacting the database afterwards
| Technology | Version | Purpose |
|---|---|---|
| PHP | 8.4+ | Core language (with the sockets extension) |
| Laravel | 12.0 | Application framework |
| Filament | 4.0 | Admin panel framework |
| NativePHP Desktop | 2.0 | Native application wrapper |
| Livewire | 3.6 | Reactive components |
| Tailwind CSS | 4.0 | Styling framework |
| Vite | 7.0 | Frontend build tool |
| SQLite | - | Database |
Before you begin, ensure your system meets these requirements:
- PHP 8.4 or higher
- Composer (latest version recommended)
- Node.js 18+ and npm
- SQLite (usually included with PHP)
- Windows 10 or later (64-bit)
- Visual C++ Redistributable (automatically handled by NativePHP)
- macOS 10.15 (Catalina) or later
- Xcode Command Line Tools (for building)
- Modern Linux distribution (Ubuntu 20.04+, Fedora 35+, etc.)
- GTK 3.0+ libraries
# Clone the repository
git clone https://github.com/1hanif/nativemailer.git
cd nativemailer
# Run the automated setup
composer setupThe composer setup command will:
- Install PHP dependencies via Composer
- Create
.envfile from.env.example - Generate application key
- Run database migrations
- Install Node.js dependencies
- Build frontend assets
If you prefer manual installation:
# 1. Clone the repository
git clone https://github.com/1hanif/nativemailer.git
cd nativemailer
# 2. Install PHP dependencies
composer install
# 3. Set up environment
cp .env.example .env
php artisan key:generate
# 4. Create and migrate database
touch database/database.sqlite
php artisan migrate
# 5. Install Node dependencies
npm install
# 6. Build assets
npm run buildStart the desktop app with hot-reload for assets:
composer native:devThis runs php artisan native:run and the Vite dev server together.
What happens when you start the app:
- 🚀 NativePHP opens the desktop window
- 📡 The SMTP catcher starts as a background process on
127.0.0.1:1025 - 📬 The inbox opens, ready to receive emails
In development the app uses its own database, database/nativephp.sqlite, and doesn't migrate it on start. After pulling changes that add migrations, run:
php artisan native:migrateBuilt apps migrate automatically the first time each new version starts.
Build native executables for distribution:
# Build for a platform: mac, win or linux
php artisan native:build macBuilds are written to nativephp/electron/dist/:
- macOS:
.dmgand.zip - Windows:
.exeinstaller - Linux:
.AppImageand.deb
Pushing a v* tag builds all three platforms on GitHub Actions and attaches them to a GitHub release (see .github/workflows/release.yml).
To send emails from your Laravel application (or any app) to Native Mailer:
Update your .env file:
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_FROM_ADDRESS="test@example.com"
MAIL_FROM_NAME="${APP_NAME}"MAIL_USERNAME and MAIL_PASSWORD can be left as they are: Native Mailer accepts any credentials. If you change the port in the app's Settings, update MAIL_PORT to match.
// PHPMailer
$mail = new PHPMailer();
$mail->isSMTP();
$mail->Host = '127.0.0.1';
$mail->Port = 1025;
$mail->SMTPAuth = false;
// Symfony Mailer
$transport = Symfony\Component\Mailer\Transport::fromDsn('smtp://127.0.0.1:1025');
$mailer = new Symfony\Component\Mailer\Mailer($transport);Send a test email:
php artisan tinkerMail::raw('Test email from Native Mailer', function ($message) {
$message->to('test@example.com')
->subject('Test Email');
});Check the Native Mailer app to see your email!
nativemailer/
├── app/
│ ├── Console/Commands/
│ │ └── StartSmtpCatcher.php # `smtp:start`: runs the catcher
│ ├── Events/
│ │ └── EmailReceived.php # Broadcast to the UI on capture
│ ├── Filament/Resources/Emails/
│ │ ├── EmailResource.php
│ │ ├── Pages/
│ │ │ ├── ListEmails.php # Inbox, settings, Delete all, status
│ │ │ └── ViewEmail.php # Email view, Release, link opening
│ │ ├── Schemas/EmailInfolist.php
│ │ └── Tables/EmailsTable.php # Inbox columns, search, bulk actions
│ ├── Http/Controllers/
│ │ └── AttachmentController.php # Serves attachment downloads
│ ├── Models/
│ │ ├── Email.php # Retention (pruning), inline images
│ │ ├── EmailAttachment.php
│ │ └── Setting.php # Key/value app settings
│ ├── Providers/
│ │ └── NativeAppServiceProvider.php # Window, catcher process, notifications
│ ├── Services/
│ │ ├── SmtpCatcher.php # Wires the server to the handler
│ │ ├── Smtp/
│ │ │ ├── SmtpServer.php # Sockets and the select loop
│ │ │ ├── SmtpSession.php # SMTP protocol state machine
│ │ │ ├── MimeMessageParser.php # Raw message → headers, bodies, attachments
│ │ │ └── PersistCapturedEmail.php # Store, broadcast, notify
│ │ ├── CatcherStatus.php # Is the catcher up?
│ │ ├── EmailChecks.php # HTML checks tab
│ │ ├── ReleaseEmail.php # Forward through a real relay
│ │ └── UnreadBadge.php # App icon badge
│ └── Support/
│ ├── MimeHeader.php # RFC 2047 decoding
│ └── PreviewLinks.php # Preview CSP and link forwarding
├── database/migrations/
├── resources/views/filament/
│ └── email-html-view.blade.php # The tabbed email viewer
├── routes/
│ ├── web.php # Attachment route
│ └── console.php # Hourly retention cleanup
└── tests/
├── Feature/ # Pages, storage, retention, integration
├── Unit/ # SMTP session, parser, checks
└── Fixtures/emails/ # Real-world .eml messages
The catcher runs as a separate NativePHP child process (php artisan smtp:start), split into small classes under app/Services/Smtp:
┌──────────────────────────────────────────────────────────────┐
│ smtp:start (child process) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ SmtpServer │───▶│ SmtpSession │───▶│ Persist │ │
│ │ sockets and │ │ one per │ │ CapturedEmail │ │
│ │ select loop │ │ client │ │ │ │
│ └──────────────┘ └──────────────┘ └────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌───────┐ │
│ │ MimeMessage │ │ DB, │ │
│ │ Parser │ │ event,│ │
│ └──────────────┘ │ badge │ │
│ └───────┘ │
└──────────────────────────────────────────────────────────────┘
Key Features:
- Non-blocking I/O: one
socket_selectloop handles many connections, with an idle timeout - State machine: HELO/EHLO, AUTH, MAIL, RCPT and DATA states, with dot-stuffing and size limits
- Fail-safe: a malformed email is logged to
storage/logs/smtp.logand never stops the server
Email Sent ──▶ SMTP Server ──▶ Parse Headers ──▶ Extract Content
│ │
▼ ▼
Store Raw Data ──────────────▶ Save to Database
│
▼
Trigger Event ──▶ UI Update
emails
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
from |
string | Sender address |
to |
string | To addresses |
cc |
text | CC addresses |
bcc |
text | Envelope recipients not listed in To or CC |
subject |
string | Decoded subject line |
body_text |
longtext | Plain-text part (UTF-8) |
body_html |
longtext | HTML part (UTF-8) |
raw |
longtext | The complete message as received |
is_read |
boolean | Read state |
received_at |
timestamp | When it was captured (indexed) |
email_attachments: one row per attachment (name, content_type, size, content_id, inline, and the file bytes in content), deleted along with its email.
settings: key/value store for the port, retention and release relay settings.
The full guide is in FEATURES.md. A few highlights:
HTML emails are rendered in a sandboxed iframe with an opaque origin:
<iframe srcdoc="{{ $email->previewHtml() }}" sandbox="allow-scripts"></iframe>- The iframe can't access the app: there's no
allow-same-origin - A Content-Security-Policy injected into the email only allows Native Mailer's own link handler (by nonce). The email's scripts, event handlers and
javascript:links are blocked - Clicked links are passed to the app and opened in your default browser
- Remote images and styles load as they would in a real mail client
| Command | Description | Example |
|---|---|---|
HELO/EHLO |
Start a session (EHLO lists extensions) | EHLO client.example.com |
AUTH |
PLAIN or LOGIN; any credentials accepted |
AUTH PLAIN AHVzZXIAc2VjcmV0 |
MAIL FROM |
Sender, with optional SIZE= |
MAIL FROM:<sender@example.com> |
RCPT TO |
A recipient (repeat for each) | RCPT TO:<recipient@example.com> |
DATA |
The message, ending with a line of . |
DATA |
RSET |
Abort the current message | RSET |
NOOP |
Do nothing | NOOP |
QUIT |
Close the connection | QUIT |
EHLO advertises SIZE, AUTH PLAIN LOGIN, 8BITMIME and SMTPUTF8.
# Run all tests
php artisan test
# Run one file or test
php artisan test --filter=MimeMessageParserTestThe suite includes:
- Unit tests for the SMTP session, the MIME parser and the HTML checks
- Fixture tests against real
.emlmessages intests/Fixtures/emails/ - An integration test that starts the real
smtp:starton a free port and sends mail to it over TCP (needs thesocketsextension)
GitHub Actions runs the tests and a Pint style check on every pull request (.github/workflows/tests.yml).
Format code using Laravel Pint:
# Fix code style
./vendor/bin/pint
# Preview changes without fixing
./vendor/bin/pint --test# Runs the desktop app and the Vite dev server together
composer native:devTest the SMTP server directly using telnet:
telnet 127.0.0.1 1025EHLO localhost
MAIL FROM:<test@example.com>
RCPT TO:<recipient@example.com>
DATA
Subject: Test Email
From: test@example.com
To: recipient@example.com
This is a test email body.
.
QUIT
Logs are stored in:
storage/logs/laravel.log: application logs, including catcher start-up errorsstorage/logs/smtp.log: emails that failed to parse or store, with the start of the raw message
The php.ini settings for the desktop app are in NativeAppServiceProvider::phpIni(). display_errors is off so errors never render in the app window; they go to the logs.
The status line under the inbox title tells you when another program holds the port. Either pick a different port in Settings (and update MAIL_PORT in your apps), or free the port:
# Windows - find the process using port 1025
netstat -ano | findstr :1025
taskkill /F /PID <PID>
# macOS/Linux
lsof -ti:1025 | xargs kill -9The desktop app in development uses database/nativephp.sqlite. To start over (this deletes all captured emails):
php artisan native:migrate:freshClear caches and rebuild:
php artisan config:clear
php artisan cache:clear
php artisan view:clear
npm run buildContributions are welcome and appreciated! Here's how you can help:
- Check if the issue already exists
- Include detailed reproduction steps
- Provide system information (OS, PHP version, etc.)
- Include relevant logs or screenshots
- Describe the feature and its benefits
- Provide use cases
- Consider implementation complexity
- Fork the repository
- Create a feature branch
git checkout -b feature/amazing-feature
- Commit your changes
git commit -m 'Add amazing feature' - Push to your branch
git push origin feature/amazing-feature
- Open a Pull Request
- Follow PSR-12 coding standards
- Write tests for new features
- Update documentation as needed
- Run
./vendor/bin/pintbefore committing - Keep commits atomic and well-described
This project is open-source software licensed under the MIT License.
MIT License
Copyright (c) 2025 Mustapha Hanif
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
This project is built on the shoulders of giants:
- Laravel - The PHP framework for web artisans
- NativePHP - Build native desktop applications with PHP
- Filament - Beautiful admin panels for Laravel
- Livewire - Full-stack framework for Laravel
- Tailwind CSS - Utility-first CSS framework
- Vite - Next generation frontend tooling
Special thanks to:
- The Laravel community for their amazing packages and support
- The NativePHP team for making native PHP apps possible
- All contributors and users of Native Mailer
Mustapha Hanif
- GitHub: @projecthanif
- Email: Contact via GitHub
If you find this project helpful, please consider:
- ⭐ Starring the repository
- 🐛 Reporting bugs
- 💡 Suggesting new features
- 🔀 Contributing code
- 📢 Sharing with others
Made with ❤️ using Laravel and NativePHP