1Overview
What Mailing is, and how its pieces fit together.
Mailing is a single-tenant admin dashboard for running email newsletters. It stores everything — your admin account, subscribers, campaigns, and delivery stats — in one SQLite database file, and sends mail using its own built-in SMTP client (a small PHP class that talks to your provider's SMTP server directly over sockets). There's no external mail API and no Composer dependencies to install.
The moving parts are:
- The web app — where you manage subscribers, write campaigns, and configure settings. Clicking Send delivers a batch of emails right there in the same request — no separate process required.
- The database — a single file at
storage/database.sqlite, created automatically on install. - The worker — the same sending logic, also available as a command-line script (
worker.php) for anyone who wants a cron job to keep very large lists draining automatically. Entirely optional. - Tracking endpoints — small public scripts (
track.php,unsubscribe.php) that record opens/clicks and handle unsubscribe requests.
2Requirements
- PHP 8.2 or newer, with the
pdo_sqliteandopensslextensions enabled (both are on by default in almost every hosting environment). - A web server that can run PHP (Apache, Nginx + PHP-FPM, LiteSpeed, etc.).
- The ability to write to a
storage/folder on disk. - SMTP credentials from an email provider (your own mailbox, or a transactional provider like SendGrid, Mailgun, Postmark, Amazon SES, etc).
- The ability to schedule a cron job is entirely optional — only useful if you want very large lists to keep sending unattended without you clicking "Continue sending." See Sending & large lists.
3Installation
From zip file to logged-in dashboard.
- Upload the files.Upload the entire contents of the app folder to your server, e.g. the document root or a subfolder such as
/mailing. - Make
storage/writable.PHP needs to createstorage/database.sqlitethe first time it runs. On shared hosting, permissions of755or775on the folder are usually enough. - Open
/install.phpin your browser.If the app is already installed, this automatically redirects to the login page instead — it's safe to visit even after setup. - Create your admin account.Enter the email and password you'll use to log in. The password must be at least 8 characters.
- Log in at
/login.php.Visiting any page before installation is complete will automatically redirect you to the installer, so there's no way to get stuck on a blank error page. - Set up SMTP.Go to Settings and fill in your SMTP details so campaigns can actually send. See SMTP setup below.
install.php won't let anyone create a second one — it redirects straight to login. Still, it's good practice to delete or rename install.php after installation, since it's one less script publicly reachable on your server.4Dashboard
The dashboard (index.php) is a quick-glance overview: total active subscribers, total campaigns, total emails sent, and a table of your five most recent campaigns with their status and recipient counts. Click any campaign to jump to its detailed stats.
5Subscribers
Add, import, search, and remove the people on your list.
Adding one at a time
Use the "Add subscriber" form at the top of the Subscribers page: enter an email (required) and a name (optional), then submit. Duplicate emails are silently ignored rather than creating a second row.
Importing a CSV
Use the "Import CSV" form to bulk-add subscribers. Requirements:
- The file needs a header row.
- It must include a column named
email(case-insensitive) — this is the only required column. - A column named
nameis picked up automatically if present. - Rows with an invalid or missing email are skipped. Emails already on the list are skipped too — only genuinely new subscribers are counted and added.
email,name
ada@example.com,Ada Lovelace
grace@example.com,Grace HopperExporting
The Export link on the Subscribers page downloads every subscriber as a CSV with email, name, status, created_at columns — useful for backups or moving to another tool.
Searching
The search box filters by email or name (partial match). Clear the box and search again to return to the full list.
Status, unsubscribing & deleting
Each subscriber has a status:
| Status | Meaning |
|---|---|
| subscribed | Will receive campaigns you queue. |
| unsubscribed | Opted out (by themselves or by you) — excluded from all future sends. |
You can unsubscribe someone manually from the list, or delete them entirely. Deleting is permanent and asks for confirmation first, since it removes their row (and their send history) completely — unsubscribing is the safer, reversible option if you just want to stop emailing them.
6Campaigns
Writing, previewing, testing, and queuing a newsletter.
Creating a campaign
From the Campaigns page, click New campaign. Every campaign has a name (for your own reference), a subject line, and an HTML content editor. Saving without sending keeps it as a draft you can come back to.
Template variables
Three placeholders in your HTML get replaced per-recipient when the email actually sends:
| Variable | Replaced with |
|---|---|
{{name}} | The subscriber's name (blank if none was given). |
{{email}} | The subscriber's email address. |
{{unsubscribe_url}} | A one-click unsubscribe link unique to that subscriber. |
{{unsubscribe_url}} somewhere in every campaign (the default template already does). Besides being expected by recipients, it's also sent as a machine-readable List-Unsubscribe header, which mailbox providers use as a positive deliverability signal.Preview & test send
Use Preview email to see the rendered HTML with placeholder sample data, right in your browser. Use the Send test box to email yourself (or anyone) a real copy — this goes out immediately over SMTP, without touching your subscriber list or queue, so it's the fastest way to sanity-check formatting and deliverability before a real send.
Sending
Clicking Send campaign (after confirming) does three things: saves the campaign, creates one queued recipient row for every currently-subscribed contact, and immediately sends the first batch right then and there. For a typical-sized list this means the campaign is fully delivered by the time the page reloads — no waiting, nothing else to configure.
Deleting a campaign
You can delete a campaign either from the row's Delete link on the Campaigns list, or from the Delete campaign button inside the editor itself. This works for drafts as well as campaigns that have already been sent or are still partway through sending.
7Sending & large lists
What happens once a list is bigger than one batch, and the fully-optional cron path.
Every send processes one batch at a time (size set by Batch size in Settings, default 20) — this keeps a single click from having to hold a connection open for hundreds of emails in a row, and lets a failed send retry automatically on the next attempt instead of blocking everyone after it.
If a campaign has more recipients than fit in one batch, whatever's left simply stays queued, and you'll see a Continue sending button — on both the Campaigns list and the campaign's own stats page — showing exactly how many are left. Click it whenever you're ready to send the next batch; there's no time pressure and nothing else running in the background.
On each batch, sending:
- Marks each successful send as sent and records the timestamp.
- On failure (e.g. a temporary SMTP error), increments an attempt counter and leaves the recipient queued for retry — until 4 attempts have failed, at which point it's marked failed and stops retrying.
- Marks the whole campaign sent once every recipient has either succeeded or permanently failed.
Optional: cron for fully unattended sending
If you're regularly sending to lists much larger than one batch and don't want to click Continue sending a few times, you can point a cron job at the bundled worker.php — it does the exact same batch-sending logic, just triggered on a timer instead of by a click:
* * * * * /usr/bin/php /path/to/mailing/worker.php >/dev/null 2>&1You can also run it manually from a terminal any time: php worker.php. It prints a one-line summary of what it sent, which is handy while testing.
8Opens, clicks & unsubscribes
Every campaign gets a stats page (click a campaign name from the Campaigns list) showing delivered count, open rate, and click rate. Two things need to be true for these numbers to be accurate:
- Set the App URL in Settings. Tracking links and the open-tracking pixel are absolute URLs pointing back at your install. If App URL is blank, sending guesses a URL from whatever request context it happens to run in — which isn't reliable, especially if you ever use the optional cron worker. Setting it explicitly avoids broken tracking links entirely.
- Recipients need images enabled and to actually click links for opens/clicks to register — this is a limitation of email tracking in general, not specific to this app, and typically under-reports true engagement somewhat.
Unsubscribing is self-service: the link in every sent email points to a public page where the recipient confirms and is instantly marked unsubscribed, excluding them from all future campaigns without needing you to do anything.
9Account settings
The Account card on the Settings page lets you change the admin email and/or password you log in with. For security, any change — even just the email — requires re-entering your current password. Leave the new-password fields blank to change only the email.
10SMTP setup
Where campaigns actually get sent from.
Mailing needs its own SMTP credentials — the same kind you'd type into any email client. Fill these in under Settings:
| Field | What it's for |
|---|---|
From name / From email | The name and address recipients see as the sender. |
App URL | Your install's public URL — needed for accurate tracking (see previous section). Leave blank only if you don't care about open/click stats. |
SMTP host | Your provider's SMTP server address, e.g. smtp.gmail.com or smtp.sendgrid.net. |
Port | Commonly 587 for STARTTLS, or 465 for SSL/TLS. |
Encryption | Match whatever your provider recommends for the port you're using — STARTTLS, SSL/TLS, or None for local/unencrypted testing setups. |
SMTP username / SMTP password | Your provider's SMTP credentials. Leave the password field blank when saving other settings to keep the one already stored — it's never shown back to you in the page for security. |
Batch size | How many emails are sent per batch — whether triggered by clicking Send/Continue sending, or by the optional cron worker (see Sending). |
Common providers
Host
smtp.gmail.com, port 587, STARTTLS. Requires an app password, not your regular Google password.Host
smtp.sendgrid.net, port 587. Username is literally apikey; password is your API key.Host
smtp.mailgun.org, port 587. Username/password come from your domain's SMTP credentials page.Host is region-specific, e.g.
email-smtp.us-east-1.amazonaws.com, port 587. Needs dedicated SMTP credentials, separate from your AWS login.Testing your setup
After saving, use the Send a test email box further down the Settings page to fire off a real message immediately — no need to create a campaign first. If it fails, the error message returned is usually the raw response from your SMTP server, which is the fastest way to diagnose what's wrong (see Troubleshooting).
11Security notes
- Passwords are hashed with PHP's
password_hash()— never stored in plain text. - Every form submission is protected by a CSRF token tied to your session.
- Session cookies are marked
HttpOnlyandSameSite, and markedSecureautomatically when served over HTTPS. - All database queries use prepared statements.
- Unsubscribe links use long random tokens, not guessable subscriber IDs.
- The bundled
.htaccessblocks direct web access to the SQLite database file and toconfig.phpon Apache — if you're on Nginx or another server, add an equivalent rule yourself, since.htaccessonly applies to Apache.
storage/database.sqlite. Keep that file outside your web root if possible, or make sure your server config actually enforces the .htaccess rule above — treat the whole storage/ folder as sensitive.12Troubleshooting
I'm locked out — forgot my password
There's no self-service "forgot password" email flow (sending it would itself require working SMTP). If you're locked out, someone with direct server/file access can reset it by connecting to storage/database.sqlite with any SQLite tool and updating the password_hash column for your row in the users table, using a hash generated by PHP:
php -r "echo password_hash('your-new-password', PASSWORD_DEFAULT);"
Paste the output into password_hash for your user row, then log in with the plain password you chose.
"Test send failed: SMTP connection failed"
The app couldn't open a socket to your SMTP host/port at all. Usually means: wrong host or port, your server's outbound firewall blocks that port, or the provider requires a different port/encryption combination than what you entered.
"SMTP error: ..." during send
The connection worked, but the server rejected something — most often wrong username/password, a "from" address that isn't verified with your provider, or hitting a sending rate limit. The exact server response is shown in the error, which is the best clue.
Nothing happened after I clicked Send
Sending happens synchronously right in that click, so a failure usually shows up immediately as an error message rather than silently. Check the campaign's stats page for a "Continue sending" button — if it's there, some recipients are still queued and just need another click. If sends are failing outright, check your SMTP settings (see below).
Open/click rates seem stuck at 0%
Make sure App URL is set in Settings — this matters even more if you've set up the optional cron worker, since there's no browser request context to guess a URL from in that case. See Opens, clicks & unsubscribes.
13FAQ
Can more than one admin log in?
The app is built around a single admin account. Multiple people can share those credentials, but there's no separate accounts or permission levels.
Can I edit a campaign after sending it?
You can still open and edit it, but changes only affect recipients that haven't been sent to yet — if a list was bigger than one batch and some are still queued, anyone already sent to keeps the version they received.
Does it support attachments?
No — campaigns are HTML-only email, no file attachments.
Can I schedule a campaign for a future date?
Not currently — clicking Send delivers it (or starts delivering it) right away, there's no delayed/scheduled send time.