Version 2.1.1 — Changelog
A complete newsletter management system for Laravel, built with Filament. Manage subscribers, campaigns, HTML templates, scheduled sending, and full tracking — all from a modern admin panel. Includes a REST API, OpenAPI documentation, and an MCP server for AI integrations.
- Features
- User Management & Roles
- Requirements
- Installation
- Configuration
- Quick Start
- Sending Newsletters
- Monitoring & Analytics
- Rate Limiting
- REST API
- MCP Server (AI Integrations)
- Testing Tags
- Multilingual Support
- Public Routes
- Scheduled Tasks
- Artisan Commands
- Troubleshooting
| Feature | Description |
|---|---|
| Subscriber Management | Import/export CSV, tagging, status management |
| Campaigns & Messages | Hierarchical organization of campaigns and messages |
| HTML Templates | Customizable templates with placeholder support |
| Scheduled Sending | Automatic delivery via cron |
| Full Tracking | Unique opens/clicks per send, plus unsubscribe tracking |
| Targeting | Filter recipients by tags and status |
| Dashboard | Statistics and monitoring widgets |
| Rate Limiting | Configurable per-minute, per-hour, and per-day limits |
| Bounce Detection | Optional IMAP bounce processing (webklex/laravel-imap) |
| REST API | Full CRUD API with Sanctum authentication and OpenAPI docs |
| MCP Server | AI integration endpoint for Cursor, Claude Code, and other clients |
| Testing Tags | Mark tags as testing to exclude sends from production statistics |
| Multilingual | Admin panel in Italian, English, German, French, Spanish, Portuguese |
| UTM Tracking | Automatic UTM parameters on outbound newsletter links |
| User Roles | Three permission levels: Editor, Manager, Administrator |
The application supports three user roles with progressively broader permissions. Every user is assigned exactly one role, stored in the role column of the users table.
| Role | Description |
|---|---|
| Editor | Focused on content creation. Can create and manage draft messages within existing campaigns. |
| Manager | Full operational access. Manages campaigns, subscribers, tags, templates, and all message states. |
| Administrator | Everything a Manager can do, plus user management and API token management. |
| Area | Editor | Manager | Administrator |
|---|---|---|---|
| Dashboard | — | ✓ | ✓ |
| Campaigns (view) | ✓ | ✓ | ✓ |
| Campaigns (create/edit/delete) | — | ✓ | ✓ |
| Messages (create) | ✓ | ✓ | ✓ |
| Messages (view/edit/delete draft) | ✓ | ✓ | ✓ |
| Messages (view sent/sending) | — | ✓ | ✓ |
| Subscribers | — | ✓ | ✓ |
| Tags | — | ✓ | ✓ |
| Templates | — | ✓ | ✓ |
| Users | — | — | ✓ |
| API Tokens | — | — | ✓ |
| Newsletter Report API | — | ✓ | ✓ |
- Editor — lands on the Campaigns page after login; the dashboard is not shown in navigation.
- Manager / Administrator — lands on the Dashboard; all navigation items are visible according to the matrix above.
- User menu — the API Tokens and Users links appear only for Administrators.
Administrators can create, edit, and delete users from the Users page in the admin panel. When creating or editing a user, the following fields are available:
- Name and Email (required)
- Password (required on create, optional on edit — leave blank to keep unchanged)
- Role — select Editor, Manager, or Administrator
- Locale — preferred language for the admin panel UI
An administrator cannot delete their own account.
When upgrading from a version without roles, two migrations handle the transition:
add_role_to_users_table— adds therolecolumn (defaults toeditor).set_all_existing_users_to_legacy_administrator_role— promotes all pre-existing users toadministratorso they retain full access.
- PHP 8.3+
- Laravel 13
- Filament 5
- Database (SQLite, MySQL, or PostgreSQL)
- Queue driver (database, Redis, etc.)
- Clone the repository and install dependencies:
git clone <repository-url> newsletter
cd newsletter
composer install- Configure the environment:
cp .env.example .env
php artisan key:generate- Run migrations:
php artisan migrate- Build frontend assets:
npm install
npm run build- Create an admin user (if not using seed data):
php artisan make:filament-user- Generate API documentation (optional):
php artisan l5-swagger:generateConfigure your SMTP settings in .env:
MAIL_MAILER=smtp
MAIL_HOST=your-smtp-host.com
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_FROM_ADDRESS="newsletter@yourdomain.com"
MAIL_FROM_NAME="${APP_NAME}"| Variable | Description | Default |
|---|---|---|
NEWSLETTER_TRACKING_ENABLED |
Enable open/click tracking | true |
NEWSLETTER_RATE_LIMIT_PER_MINUTE |
Max sends per rolling minute (0 = no cap) |
0 |
NEWSLETTER_RATE_LIMIT_PER_HOUR |
Max sends per rolling hour (0 = no cap) |
0 |
NEWSLETTER_RATE_LIMIT_PER_DAY |
Max sends per rolling day (0 = no cap) |
0 |
See Rate limiting for behaviour and tuning.
Bounce detection is optional and disabled by default. It requires an extra Composer package that is not shipped with the app:
composer require webklex/laravel-imapThen enable and configure IMAP:
NEWSLETTER_IMAP_ENABLED=true
NEWSLETTER_IMAP_HOST=imap.yourdomain.com
NEWSLETTER_IMAP_PORT=993
NEWSLETTER_IMAP_USERNAME=your-username
NEWSLETTER_IMAP_PASSWORD=your-password
NEWSLETTER_IMAP_ENCRYPTION=ssl
NEWSLETTER_IMAP_FOLDER=INBOXWhen disabled, missing credentials, or without webklex/laravel-imap, newsletter:process-bounces exits cleanly and does nothing. Matched bounces are linked to the subscriber’s most recent successful message_send when available.
To auto-generate the OpenAPI spec on every request in local development:
L5_SWAGGER_GENERATE_ALWAYS=trueFor production, run php artisan l5-swagger:generate during deploy.
- Seed sample data (optional, recommended for testing):
php artisan newsletter:seed-data- Start the queue worker:
./start-worker.shOr manually:
php artisan queue:work --tries=3 --timeout=90- Access the admin panel:
- URL:
https://newsletter.test(Laravel Herd) orhttp://localhost:8000 - Default credentials (after seeding):
admin@newsletter.test/password
- Go to Newsletter > Messages
- Create a new message or select an existing one
- Set status to Ready
- Click Send Now
- Emails are queued and sent automatically via the queue worker
- Create or edit a message
- Set the Scheduled Date field
- The system sends automatically at the scheduled time (requires cron — see Scheduled Tasks)
- Dashboard: Main KPIs and send statistics
- Messages: Status and send counts per message
- Message Details: Individual tracking (opens, clicks) per recipient
Configure how many emails the queue may send using rolling windows (stored in the app cache). Set a variable to 0 to turn off that cap; the other caps still apply.
| Env variable | Window | Typical use |
|---|---|---|
NEWSLETTER_RATE_LIMIT_PER_MINUTE |
~1 min | Burst control; low values cause jobs to release() often (short delays). |
NEWSLETTER_RATE_LIMIT_PER_HOUR |
~1 h | Provider hourly quotas. |
NEWSLETTER_RATE_LIMIT_PER_DAY |
~24 h | Daily provider caps. |
How sending works: for each queued send, SendNewsletterEmail checks daily, then hourly, then per-minute (App\Services\EmailRateLimiter). The send proceeds only if it fits under every enabled limit; otherwise the job is released with a delay and no quota is consumed for that attempt.
Throughput: in practice the tightest of your enabled limits dominates (e.g. high hourly but low per-minute still throttles bursts).
Estimated time: while a message is in Sending, the UI uses the same three env-driven values to approximate completion (Message::getEstimatedSendTime()).
Example (adjust to your SMTP limits):
NEWSLETTER_RATE_LIMIT_PER_MINUTE=0
NEWSLETTER_RATE_LIMIT_PER_HOUR=1000
NEWSLETTER_RATE_LIMIT_PER_DAY=10000Keeping PER_MINUTE=0 and using only hourly/daily is a common choice for large campaigns to avoid many small queue delays.
Queue / worker: ensure the worker --timeout stays a few seconds below your database queue retry_after (see config/queue.php, DB_QUEUE_RETRY_AFTER) so jobs are not processed twice.
Check current counters:
php artisan newsletter:rate-limitsThe application exposes a full REST API authenticated via Laravel Sanctum personal access tokens.
- Go to the admin panel user menu > API tokens (or navigate to
/api-tokens) - Create a token with the API ability enabled
- Use the token in the
Authorizationheader:
Authorization: Bearer <your-token>
All endpoints are prefixed with /api and require a valid Bearer token with the api ability.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/user |
Authenticated user info |
GET |
/api/reports/newsletter |
Newsletter report (filterable) |
GET |
/api/tags |
List all tags |
POST |
/api/tags |
Create a tag |
GET |
/api/tags/{tag} |
Show a tag |
PUT |
/api/tags/{tag} |
Update a tag |
DELETE |
/api/tags/{tag} |
Delete a tag |
GET |
/api/subscribers |
List subscribers |
POST |
/api/subscribers |
Create a subscriber |
GET |
/api/subscribers/{subscriber} |
Show a subscriber |
PUT |
/api/subscribers/{subscriber} |
Update a subscriber |
DELETE |
/api/subscribers/{subscriber} |
Delete a subscriber |
GET |
/api/templates |
List templates |
POST |
/api/templates |
Create a template |
GET |
/api/templates/{template} |
Show a template |
PUT |
/api/templates/{template} |
Update a template |
DELETE |
/api/templates/{template} |
Delete a template |
GET |
/api/campaigns |
List campaigns |
POST |
/api/campaigns |
Create a campaign |
GET |
/api/campaigns/{campaign} |
Show a campaign |
PUT |
/api/campaigns/{campaign} |
Update a campaign |
DELETE |
/api/campaigns/{campaign} |
Delete a campaign |
GET |
/api/campaigns/{campaign}/messages |
List messages in a campaign |
POST |
/api/campaigns/{campaign}/messages |
Create a message |
GET |
/api/campaigns/{campaign}/messages/{message} |
Show a message |
PUT |
/api/campaigns/{campaign}/messages/{message} |
Update a message |
DELETE |
/api/campaigns/{campaign}/messages/{message} |
Delete a message |
Interactive Swagger UI is available at /api/documentation. Generate or update the spec with:
php artisan l5-swagger:generateThe application includes a Model Context Protocol (MCP) server that allows AI clients (Cursor, Claude Code, Windsurf, etc.) to interact with your newsletter data.
/mcp/newsletter
Authenticated with a Sanctum Bearer token that has the mcp ability.
| Tool | Description |
|---|---|
list-campaigns |
Lists campaigns (all campaigns for managers/administrators; editors see their own) |
newsletter-report |
Delivery report with summary, per-message stats, and daily timeseries |
send-history-analysis |
Highlights and trends from recent send history |
subscriber-insights |
Audience breakdown by tags and statuses |
generate-email-template-html |
Generates responsive HTML email template from a description |
create-newsletter-message |
Creates a draft or ready message inside a campaign |
The server ships with a newsletter-assistant prompt — a reusable skill template that guides AI assistants through campaign planning, report interpretation, content drafting, and message creation.
- Create a token in the admin panel with the MCP ability
- Configure your AI client to connect to the MCP endpoint with the token as Bearer auth
- Debug locally with:
php artisan mcp:inspector mcp/newsletter
Point the URL at your app’s origin plus /mcp/newsletter, and send the Sanctum token in the Authorization header:
{
"mcpServers": {
"newsletter": {
"url": "https://your-domain.example/mcp/newsletter",
"headers": {
"Authorization": "Bearer YOUR_SANCTUM_TOKEN"
}
}
}
}Replace your-domain.example with your deployment host (for local development, something like http://127.0.0.1:8000/mcp/newsletter). Replace YOUR_SANCTUM_TOKEN with the plaintext token string from the admin panel (treat it like a password).
You can phrase tasks in natural English; the assistant maps them to MCP tools. Examples:
- Campaigns: “List all newsletter campaigns.” / “What campaigns do we have?”
- Delivery stats: “Give me a newsletter delivery report from 2026-01-01 to 2026-01-31.” / “Show sends, opens, clicks, and bounces for the last 30 days.”
- Scoped report: “Report for the campaign with id … between 2026-01-01 and 2026-01-31.”
- Narrative summary: “Summarize send history and highlights for the last quarter.”
- Audience: “How many subscribers do we have by status? Which tags have the most subscribers?”
- HTML: “Generate a responsive HTML email template for a product launch: hero, short intro, three feature bullets, primary CTA button.”
- New message: “Create a draft message in the given campaign id, using the given template id, subject Weekly digest, HTML body …, and the listed tag ids.”
For send statistics and campaign listing, use a token belonging to a Manager or Administrator if your organization splits campaign ownership across users; those roles see all campaigns in reports (same idea as the admin panel).
Tags can be marked as testing (is_testing flag) in the Filament admin panel. When a message targets recipients only through testing tags:
- Sends are excluded from dashboard statistics (emails sent, opens, clicks, bounces, charts)
- After the send completes, per-recipient send rows and related bounces are removed from the database
- Testing tags and normal tags cannot be mixed on the same message
This allows safe end-to-end testing of the sending pipeline without polluting production metrics.
The admin panel supports six languages: Italian, English, German, French, Spanish, and Portuguese.
- Logged-in users can choose their language from the Profile page
- Before login, the panel detects the browser
Accept-Languageheader and uses the closest supported language (defaults to English) - New users automatically inherit the browser language on account creation
The following routes are available for public use:
| Route | Method | Description |
|---|---|---|
/subscribe |
GET | Subscription form |
/subscribe |
POST | Process subscription |
/subscribe/confirm/{token} |
GET | Confirm subscription (double opt-in) |
/unsubscribe/{subscriber} |
GET | Unsubscribe form |
/unsubscribe/{subscriber}/confirm |
POST | Confirm unsubscribe |
Add the Laravel scheduler to your crontab:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1| Task | Schedule | Description |
|---|---|---|
newsletter:send-scheduled |
Every minute | Sends messages with scheduled date in the past |
newsletter:process-pending |
Every 5 minutes | Re-queues stuck pending sends and completes finished messages |
newsletter:process-bounces |
Every 15 minutes | Processes bounced emails via IMAP (when enabled + package installed) |
backup:run |
Daily at 03:00 | Runs application backup |
backup:clean |
Daily at 04:00 | Cleans old backups |
| Command | Description |
|---|---|
newsletter:seed-data |
Populate database with sample subscribers, campaigns, and messages |
newsletter:send-scheduled |
Manually trigger scheduled message sending |
newsletter:process-pending |
Process pending emails in the queue |
newsletter:process-bounces |
Process bounced emails from IMAP |
newsletter:rate-limits |
Display current rate limit status |
l5-swagger:generate |
Generate or update the OpenAPI specification |
mcp:inspector mcp/newsletter |
Debug the MCP server locally |
Once your app is ready, deploy it to any Ubuntu VPS with cipi.sh — an open-source CLI built exclusively for Laravel.
- Full app isolation — own Linux user, PHP-FPM pool & database per app
- Zero-downtime deploys with instant rollback via Deployer
- Let's Encrypt SSL, Fail2ban, UFW firewall — all automated
- Multi-PHP (7.4 → 8.5), queue workers, S3 backups, auto-deploy webhooks
- AI Agent ready — any AI with SSH access can deploy & manage your server
wget -O - https://raw.githubusercontent.com/andreapollastri/cipi/refs/heads/latest/install.sh | bash→ Learn more at cipi.sh
- Ensure the queue worker is running:
./start-worker.shorphp artisan queue:work - Manually process pending:
php artisan newsletter:process-pending - Check the jobs table:
php artisan tinker --execute="DB::table('jobs')->count()"
php artisan tinker --execute="Mail::raw('Test', fn(\$m) => \$m->to('test@example.com'))"php artisan newsletter:rate-limitsRun npm run build or npm run dev to compile assets.
- Verify the token is valid: check the API tokens page in the admin panel
- Ensure the token has the correct ability (
apifor REST endpoints,mcpfor the MCP server) - Check the OpenAPI docs at
/api/documentationfor request/response format
MIT
Created by Andrea Pollastri