Mail Engine v1.0
Part of CloudWizard Email Suite
Mail Engine is the delivery layer of the suite — campaigns, templates, a cron-driven send queue, open/click tracking, and GDPR tooling. It owns no subscriber identity of its own: recipients come from Audience Manager when installed, or from CSV import when run standalone.
Requirements
| Requirement | Detail |
|---|---|
| PHP | 8.1 or higher |
| MariaDB / MySQL | 5.7+ / 10.3+ |
| Web server | Nginx or Apache |
| Outgoing mail | SMTP server reachable from the box (Postfix locally, or any external SMTP) |
| Cron access | A way to trigger a URL or run a PHP script on a schedule — Virtualmin, cPanel, or plain crontab |
Step 1 — Install the Controller
Mail Engine runs inside the CloudWizard Controller. The controller handles authentication, shared database config, and module detection. Install it first.
Upload the controller
Unzip the download. It holds one folder, CW/, with the controller and Mail Engine inside. Upload that whole folder to a directory on your server — typically public_html/CW/. Its top level:
public_html/CW/
index.php ← sends /CW/ to the admin login
bootstrap.php
u.php ← unsubscribe link wrapper
o.php ← open-tracking pixel wrapper
c.php ← click-tracking link wrapper
vendor/ ← bundled PHPMailer
admin/
MailEngine/
Run the setup wizard
Visit https://yourdomain.com/CW/admin/setup.php. Fill in database credentials, table prefix (default cw_), SMTP credentials, admin password, and your site URL, then click Complete setup.
Log in
You're redirected to the login page. Enter your admin password to reach the CloudWizard dashboard.
Step 2 — Install Mail Engine
Upload the module
The module is already inside the zip you just uploaded, at CW/MailEngine/. Adding it to an existing CloudWizard install instead? Don't upload the whole CW/ folder again: copy this module's MailEngine/ folder into your existing controller directory, together with the short-URL files u.php, o.php and c.php from the root of the zip, and the vendor/ folder if you do not already have CW/vendor/, then reload the dashboard. Its file layout:
public_html/CW/MailEngine/
module.php
cron.php
unsub.php ← one-click unsubscribe endpoint
track-open.php
click.php
worker.php
import.php
controller.php
config.example.php
schema.sql
admin/
_bootstrap.php
index.php ← Overview / dashboard
campaigns.php
campaign_edit.php
campaign_import.php
campaign_send.php
templates.php
stats.php
import.php ← Quick Import
gdpr.php
check.php ← diagnostics
Also confirm the three short public-URL wrappers are at the controller root. They ship in the zip: CW/u.php (unsubscribe links, requires MailEngine/unsub.php), CW/o.php (the open-tracking pixel) and CW/c.php (tracked links). Every campaign's links and pixel point at them, so if o.php or c.php is missing, every tracked link and image in that campaign is broken.
Mail Engine sends via a shared PHPMailer library at CW/vendor/ — installed once with the controller, used by every module. No separate Mail Engine dependency to worry about.
Verify detection
Reload the controller dashboard. The Mail Engine card should show Open →. If it still shows "Not installed", confirm MailEngine/module.php is present and readable.
Step 3 — Activate your license
Get your license key
Whether you purchase through cloudwizard.eu or via Gumroad, your license key is shown on the confirmation page and emailed to you.
Enter the key
In the controller go to Settings → License key, paste it, and click Save settings.
Step 4 — Set up the cron job
Nothing sends without this. Mail Engine's queue is processed by cron.php, which needs to run on a schedule — it does nothing on its own between runs.
Open Mail Engine → Overview for the exact commands generated for your install, under Cron job — one-time setup:
| Option | When to use it |
|---|---|
A — URL (wget) | Works on any host with outbound HTTP access from cron; the URL includes a secret key so it can't be triggered by anyone else |
| B — PHP (direct) | Runs the script directly via the PHP CLI binary — no key needed here, since reaching it requires shell access to the server already |
Add whichever option fits under Virtualmin → Scheduled Cron Jobs, set to run every minute. The script self-limits its run time (cron_run_seconds in config, default 50s) so overlapping runs can't stack up.
Overview
Mail Engine → Overview is the landing page: queue status counts (Pending, Sending, Sent, Failed, Bounced, Suppressed — each color-coded so problems stand out at a glance), sender pause/resume, and the cron setup block above.
Pause sending stops the queue from processing without touching anything already queued — resume picks up where it left off. Useful if you spot a mistake in a campaign that's already sending.
Templates
Mail Engine → Templates shows your saved templates as a list, with + Add template top-right. Every template needs:
| Field | Notes |
|---|---|
| Name | Internal label |
| HTML body | Must contain an {{unsubscribe}} (or the older [unsubscribe]) placeholder — the save is rejected without one |
| Unsubscribe link text/style | Optional — falls back to the site-wide default if left blank |
| Tracker image URL | Optional — the logo/image served for opens. Leave blank for an invisible 1×1 tracking pixel instead |
Available placeholders: {{preheader}} {{headline}} {{message}} {{tracker}} {{unsubscribe}} {{cta_url}} {{cta_text}} {{from_name}} {{from_email}} — these are filled in per-campaign at save time. The older square-bracket form ([preheader], [tracker], etc.) still works in any existing template, so nothing built before this changed — new templates should use the curly-brace form. {{cta_url}}/{{cta_text}} only render as a real button if present in the template HTML (see the seeded "Announcement — With CTA button" template for the pattern); {{from_name}}/{{from_email}} reflect the campaign's own Sender override if set, or the install-wide default from Settings otherwise. Copy duplicates a template as a starting point; Preview opens it in a new tab with dummy tokens for tracker/unsubscribe links.
Campaigns
Mail Engine → Campaigns lists every campaign with its status and, for sending/sent campaigns, a live progress bar (auto-refreshes every 30 seconds while any campaign is sending).
Click + New campaign to open the editor:
| Field | Notes |
|---|---|
| Campaign name | Internal — not visible to recipients |
| Template | Required unless you have none saved yet |
| AI draft | Optional — pulls in a draft generated by the AI Writer module; see Using an AI Writer draft |
| Subject / Preheader / Headline | Merged into the template at save time |
| CTA button | Only shown for templates containing a {{cta_url}} (or [cta_url]) placeholder |
| Sender | Optional — overrides the install-wide From name/email for this campaign only. Leave blank to use the default from Settings. Useful for sending different campaigns under different brand names from one install |
| Message | Rich-text editor, pasted into {{message}} |
The plain-text version is auto-generated from your HTML shortly after you stop typing — it's editable, but regenerates if you clear it back to the auto-generated text. Saving a new campaign takes you to Import next (or straight to Send if the campaign came from Audience Manager's Audience Builder with a list already selected).
Duplicate on the campaigns list opens the editor with a copy of the campaign already filled in: template, subject, preheader, headline, message, CTA button and sender (name and email). Nothing is saved until you click Save, so cancelling leaves no extra campaign behind. The copy starts with an empty recipient list.
Duplicate + audience does the same and also carries over the Audience Builder lists and filters the original was sent to, so the new campaign gets the same recipients. People who are suppressed (bounced, unsubscribed, complained) are still skipped when you send. The editor also has a Copy from a recent campaign drop-down (the 100 latest) if you want to start from an older campaign.
Warm-up (daily limits per provider)
A new sending IP or domain has no reputation. If you send thousands of emails on day one, big providers such as Mail.ru, Yahoo or Gmail will refuse or junk them. Mail Engine → Warm-up lets you set a daily limit per mailbox provider and raise it step by step.
| Item | What it does |
|---|---|
| Use daily limits | Master switch. Off = no limits at all. |
| Daily limit | Maximum emails to that provider per day. Providers: Gmail & Google Workspace, Microsoft, Yahoo & AOL, Apple iCloud, Mail.ru, Yandex, GMX/WEB.DE/mail.com, Rambler, Ukr.net, I.UA, and All other domains. Blank = unlimited. |
| Sent today / Left today / Waiting in queue | Live numbers. Mail over the limit stays in the queue and is retried every 30 seconds, so it goes out as soon as you raise the limit or the next day starts. Other providers keep sending meanwhile. |
| Check queued domains now | Looks up which provider handles each queued domain (for example company domains hosted at Google or Microsoft are counted as Gmail/Microsoft). |
| History | Sent per provider for recent days. |
Suggestion for tomorrow
Each provider gets a suggestion based on yesterday: raise (by up to 30%) after a clean day where at least 80% of the limit was used, hold when little was sent, and drop when there were spam refusals or hard bounces. Click Apply per provider or Apply all. Suggestions compare against the limit that applied yesterday, so changing today's limit does not distort them.
Sending & test sends
From a draft campaign's Send screen: a pre-flight check flags missing HTML, unresolved placeholders, or a missing subject before you can send. Send test email accepts up to 5 comma-separated addresses and prepends [TEST] to the subject — this bypasses the queue entirely and sends immediately via PHPMailer, so check your inbox before sending to the full list.
Clicking Send compiles the pending recipients into the send queue, filters against mail_suppression, and starts delivery via the cron loop. This step cannot be undone — recipients already compiled into the queue stay there even if you pause sending afterward.
Stats
Mail Engine → Stats lists every sent/sending campaign with sent count, unique opens, open rate, unique clicks, and click rate. Click Clicks → on any campaign for a per-URL breakdown (unique clicks, total clicks, last click time).
Quick Import
Mail Engine → Import is a standalone shortcut for a simple, one-off send: campaign name, subject, HTML/plain-text body, and a CSV (needs an email column; name optional) — queued directly, no template or tracking setup involved.
Using an AI Writer draft
If the AI Writer module is installed and has generated drafts, the campaign editor's AI draft field shows a Browse drafts picker. Selecting one fills in Subject, Preheader, Headline, and Message from that draft.
Unsubscribe handling
The [unsubscribe] placeholder in every template resolves to a link pointing at the short public URL u.php?uid={token} at the controller root. Clicking it, or a mail client's one-click List-Unsubscribe action, does all of the following immediately:
- Resolves the contact behind the token via
contact_identity - Adds the address to
mail_suppression, so future campaigns skip it automatically - If the address is a subscriber in Audience Manager, marks all their list memberships
unsubscribed - Logs the event to
unsubscribe_eventsfor audit purposes - Shows a simple confirmation page — English, Russian, or German depending on a
langparameter set per-template
List-Unsubscribe email header, which uses the same contact-level token.GDPR erasure
Mail Engine → GDPR Erasure handles Art. 17 right-to-erasure requests for a single email address. Look it up first — it shows exactly what will be deleted (queue rows, open/click records, the contact identity record) before you confirm.
mail_suppression afterward so future sends are still blocked — that record is deliberately kept, since removing it would defeat the point of the erasure.Separately, an automatic cron-driven purge nulls campaign body content (HTML, text, recipient name) on terminal queue rows after queue_retention_days (default 30) — this runs on its own, no action needed unless you want to change the retention period in config.
Running standalone
Mail Engine works fully without Audience Manager installed — recipients come from CSV import (Quick Import or per-campaign upload) instead of a managed list. Multiple CSV files can be uploaded against the same draft campaign before sending; duplicates across uploads are automatically deduped.
Every import writes a stable identity token through the suite's shared contact_identity table, so unsubscribe links stay valid for a contact across campaigns even without Audience Manager tracking them as a subscriber.
Security
- Always use HTTPS. Campaign content and recipient data should never travel over plain HTTP.
- The cron URL contains a secret key — don't share it publicly. Anyone with it can trigger the send queue to process.
- Test before sending to a full list. There's no undo once a campaign compiles into the queue.
- Restrict admin access by IP if possible, in Nginx:
allow 1.2.3.4; deny all;
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| Campaigns stay "sending" forever, count never moves | Cron isn't running | Recheck the cron job under Overview — confirm the command matches exactly and the schedule is active. |
| "No cron run in over 5 min" warning | Cron job missing, misconfigured, or blocked | Test the URL manually with curl, or run the PHP command directly to see the raw error. |
| Template won't save — "must contain an [unsubscribe] or {{unsubscribe}} placeholder" | The HTML body is missing either placeholder form | Add {{unsubscribe}} somewhere in the template HTML, typically the footer. |
| Campaign has no HTML body / unresolved placeholders warning on Send | No template selected, or campaign wasn't re-saved after a template change | Go back to Edit campaign, select a template, and save again. |
| AI draft picker doesn't appear | AI Writer module isn't installed, or has no drafts yet | Install AI Writer and generate at least one draft — the picker appears automatically once drafts exist. |
| Unsubscribe link shows "invalid or expired" | The uid token doesn't match any row in contact_identity — usually a stale/manually-edited link |
Confirm the link was generated by Mail Engine itself, not hand-typed. Tokens never expire under normal operation, so this points to a wrong or truncated token. |
| Someone unsubscribed but still receives campaigns from a different domain | Suppression context is scoped per sending domain — see Unsubscribe handling |
Expected in a multi-domain install unless you want global suppression — confirm this matches your intent. |
| Test send fails | SMTP misconfigured in the controller's shared settings | Verify SMTP host/port/credentials under Settings; test with a known-working external SMTP if self-hosted Postfix is unverified. |
| Recipients don't receive campaigns despite queue showing "sent" | Deliverability issue (SPF/DKIM/DMARC, sender reputation) rather than a Mail Engine bug | Check Google Postmaster Tools and your domain's authentication records — this is outside Mail Engine's control once a message leaves the queue. |
| Module not detected on dashboard | module.php missing or not readable |
Confirm CW/MailEngine/module.php exists. Restart PHP-FPM: systemctl restart php8.3-fpm |
| License key not accepted | Wrong key format or server can't reach cloudwizard.eu | Key format: XXXX-XXXX-XXXX. Check outbound HTTPS: curl -s https://cloudwizard.eu/license/ping.php |
| 500 error on admin pages | DB error, missing table, or file permissions | tail -20 /home/yourusername/logs/php_log |
CloudWizard Email Suite — Mail Engine v1.0
cloudwizard.eu —
support@cloudwizard.eu