WordPress + MailKite
WordPress sends mail through PHP mail() by default — unauthenticated,
often blocked, and invisible when it fails. Point it at MailKite and every
notification, password reset, and form submission goes out over your own
DKIM-signed domain, with a log that tells you what happened.
What you need
- A verified domain with SPF + DKIM published
- Your API key (
mk_live_…) — the plugin can create an account for you if you don't have one yet
Our own plugin — free and open source. Sending, a searchable email log, failover, and inbound in one place. Download the zip, then Plugins → Add New → Upload Plugin. WordPress 6.2+, PHP 8.1+.
https://github.com/mailkite/mailkite-smtp/releases/latest/download/mailkite-smtp.zip Same plugin, one command — handy for staging and deploy scripts.
wp plugin install https://github.com/mailkite/mailkite-smtp/releases/latest/download/mailkite-smtp.zip --activate Already running WP Mail SMTP or Fluent SMTP? Point it at our relay instead — choose Other SMTP and use your API key as the password.
// wp-config.php — or paste into WP Mail SMTP settings UI
define('WPMAILSMTP_MAILER', 'other');
define('WPMAILSMTP_HOST', 'smtp.mailkite.dev');
define('WPMAILSMTP_PORT', '587');
define('WPMAILSMTP_ENCRYPTION', 'tls');
define('WPMAILSMTP_USERNAME', 'mailkite');
define('WPMAILSMTP_PASSWORD', 'mk_live_...');
define('WPMAILSMTP_FROM_EMAIL', 'hello@yourdomain.com');
define('WPMAILSMTP_FROM_NAME', 'Your Site');
define('WPMAILSMTP_SET_RETURNPATH', 'false'); Hook PHPMailer directly. Nothing to install, nothing to keep updated — and no log.
// functions.php — for sites that bypass WP Mail SMTP
add_action('phpmailer_init', function ($phpmailer) {
$phpmailer->isSMTP();
$phpmailer->Host = 'smtp.mailkite.dev';
$phpmailer->Port = '587';
$phpmailer->SMTPSecure = 'tls';
$phpmailer->Username = 'mailkite';
$phpmailer->Password = getenv('MK_API_KEY');
$phpmailer->setFrom('hello@yourdomain.com', 'Your Site');
}); The MailKite SMTP plugin
Free, open source (GPL-2.0), and no paid tier gating the useful parts:
- Send through MailKite, any SMTP host, or a provider API — SendGrid, Brevo and Mailgun are built in, bring your own key.
- A real email log. Every message, its status, and the error when there was one. Most plugins charge for this.
- Automatic failover. If an API send fails, the next mailer takes it — and the log says plainly that it fell back, rather than reporting a success you didn't get.
- Instant failure alerts by email, Slack, Discord, or webhook.
- Inbound email. Receive mail at your domain, read it in wp-admin, and reply in-thread. The webhook is registered on your MailKite account automatically.
- Domain health. SPF and DMARC checked on a schedule, with an alert when a record drifts.
Test it
The plugin has a Send Test tab that reports the mailer used and the error verbatim when one comes back. Or from WP-CLI:
wp eval 'wp_mail("you@yourdomain.com", "Test from MailKite", "It works!");' Give your users real mailboxes
MailKite Mailboxes is a companion
plugin: it gives the people who use your site a real address on your domain —
jane@yoursite.com — that works in Apple Mail, Thunderbird, or any IMAP
client, plus an Inbox screen in wp-admin and a [mailkite_inbox] shortcode
for the front end. New mail appears without a page reload.
Everything is off until you switch it on, and addresses can be reserved so nobody
claims admin@. It needs MailKite SMTP installed first — that's where the
account connection and domain live.
Download MailKite Mailboxes · releases
Act on inbound email
With the plugin installed, inbound mail is already received, verified, and logged. To do something with it — open a ticket, create a post, notify a team — hook the action it fires:
// functions.php — runs for every inbound message the plugin receives.
// The plugin has already verified the signature and logged the mail.
add_action('mailkite_smtp_inbound', function (array $message) {
// $message is the event payload: id, threadId, subject, text, html, attachments,
// and from/to as objects — $message['from']['address'], not a plain string.
$subject = $message['subject'] ?? '';
$sender = $message['from']['address'] ?? '';
if (str_contains($subject, '[support]')) {
wp_insert_post([
'post_type' => 'ticket',
'post_title' => $subject,
'post_content' => $message['text'] ?? '',
'post_status' => 'publish',
'meta_input' => [
'from' => $sender,
'thread_id' => $message['threadId'] ?? $message['id'] ?? '',
],
]);
}
});
Not running the plugin? Verify the signature yourself. The header carries two
fields and the HMAC covers "<t>.<raw body>" — sign the raw bytes, because
re-encoding the decoded payload changes whitespace and key order and will never match:
<?php
// Inbound webhook, verified by hand. See /docs/webhook-security.
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_MAILKITE_SIGNATURE'] ?? ''; // t=1750000000000,v1=4f1a9c…
$secret = getenv('MK_WEBHOOK_SECRET'); // whsec_… for this route
$parts = [];
foreach (explode(',', $header) as $pair) {
[$k, $v] = array_pad(explode('=', $pair, 2), 2, '');
$parts[trim($k)] = trim($v);
}
$t = $parts['t'] ?? '';
$sig = $parts['v1'] ?? '';
// Sign the RAW bytes: json_decode + json_encode changes whitespace and key order,
// and the signature will never match again.
$computed = hash_hmac('sha256', $t . '.' . $raw, $secret);
if (!$t || !hash_equals($computed, $sig)) {
http_response_code(401);
exit;
}
// Reject replays: t is milliseconds, and moves on every delivery attempt.
if (abs(time() - (int) $t / 1000) > 300) {
http_response_code(401);
exit;
}
$payload = json_decode($raw, true);
error_log("MailKite inbound: {$payload['from']['address']} — {$payload['subject']}");
http_response_code(200);
echo '{"ok":true}'; See Inbound webhooks for the payload and signature verification for the full scheme.
Troubleshooting
- Emails not sending — confirm the
Fromaddress is on a verified domain. The plugin's Email Log records the reason the provider gave. - 535 Authentication failed — over SMTP the password is your
mk_live_…API key, not a separate SMTP password. - Mail sent but never arrives — check the log for “sent via fallback”. That means the primary mailer refused it and a backup took over, which is worth fixing even though the mail went out.
- Delayed delivery — WP-Cron only runs when someone visits the site, so a quiet site sends late. Set
define('DISABLE_WP_CRON', true);and have your host callwp-cron.phpon a real schedule. - Two plugins fighting over
wp_mail()— only one can win. Deactivate the other SMTP plugin rather than configuring both.
See the SMTP relay docs for the full connection reference, or all integrations for other platforms.