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

RequirementDetail
PHP8.1 or higher
MariaDB / MySQL5.7+ / 10.3+
Web serverNginx or Apache
Outgoing mailSMTP server reachable from the box (Postfix locally, or any external SMTP)
Cron accessA 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.

Controller ready. Continue to install Mail Engine.

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.

License active. Go to Mail Engine and set up the cron job.

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:

OptionWhen 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.

If Overview shows no cron run yet, or a stale "X min ago" warning, the cron job isn't reaching the server — check the exact command was pasted correctly and that outbound requests aren't blocked.

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:

FieldNotes
NameInternal label
HTML bodyMust contain an {{unsubscribe}} (or the older [unsubscribe]) placeholder — the save is rejected without one
Unsubscribe link text/styleOptional — falls back to the site-wide default if left blank
Tracker image URLOptional — 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:

FieldNotes
Campaign nameInternal — not visible to recipients
TemplateRequired unless you have none saved yet
AI draftOptional — pulls in a draft generated by the AI Writer module; see Using an AI Writer draft
Subject / Preheader / HeadlineMerged into the template at save time
CTA buttonOnly shown for templates containing a {{cta_url}} (or [cta_url]) placeholder
SenderOptional — 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
MessageRich-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.

ItemWhat it does
Use daily limitsMaster switch. Off = no limits at all.
Daily limitMaximum 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 queueLive 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 nowLooks up which provider handles each queued domain (for example company domains hosted at Google or Microsoft are counted as Gmail/Microsoft).
HistorySent 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.

Typical start: 40–100 per day for the strictest providers (Mail.ru, Yahoo), growing about 20% per clean day. Spam refusals ("550 spam message rejected") are never treated as dead addresses — see Bounce Manager.

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).

Open rates are inflated by mail-client image pre-fetching (Apple Mail Privacy Protection and similar) — unique openers is the more reliable number. Click rates require real user action and are accurate.

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.

For tracked sends with templates and unsubscribe links, use Campaigns instead — Quick Import skips both.

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.

Mail Engine only reads from AI Writer's draft table here — it has no generation feature of its own. Install the AI Writer module if you want to write campaigns with AI.

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:

The token carries only the contact's identity, not which campaign sent it — this matches Mail Engine's List-Unsubscribe email header, which uses the same contact-level token.
Suppression is scoped per sending domain for unsubscribes only — an unsubscribe from one domain doesn't block sends from another, since it reflects a preference about that specific sender. Hard bounces and GDPR erasures are suppressed globally, since those reflect the email address itself being dead or requiring full removal, not a per-domain preference.

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.

Confirming is permanent. The address is added to 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

Troubleshooting

ErrorCauseFix
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