Documentation
M SMTP
M SMTP replaces WordPress's built-in PHP mail() delivery: every message sent through wp_mail(), whether from WordPress core, WooCommerce, a form plugin or your own code, goes out through an SMTP server you control. It also records each message in a local log with its delivery status, offers re-send and forwarding, optional open and click tracking, conditional routing between several SMTP servers, rate limits and a background sending queue. It is written for the administrator of a single site or a small network who wants email delivery to be verifiable rather than hopeful.
Installing and activating
M SMTP requires WordPress 6.5 or newer and PHP 7.4 or newer. Activation creates two database tables, wp_msmtp_emails and wp_msmtp_events (with your own table prefix), and schedules two WP-Cron events: msmtp_process_queue on a custom one minute interval registered as "Every minute (M SMTP)", first run one minute after activation, and msmtp_purge_logs on the standard daily schedule, first run 24 hours after activation. On multisite, network activation installs the tables and cron on every existing site, and any site created later is set up automatically. Schema upgrades run on the init hook rather than only in wp-admin, so a network subsite is not left behind after a plugin update; a lock option named msmtp_upgrade_lock keeps concurrent requests from running the upgrade more than once.
- Upload the plugin to /wp-content/plugins/m-smtp/, or install the zip from the Plugins screen.
- Activate it. Nothing is sent through SMTP yet.
- Go to M SMTP, then Settings, and enter your SMTP server details.
- Go to M SMTP, then Tools, and run Test Connection followed by Send Test Email.
- Live from the first minute: logging of every wp_mail() message, with the full body stored (Log message body is on by default), and a 30 day retention window.
- Off until you switch it on: open and click tracking, Optimized sending (the background queue), every rate limit (all set to 0), Force From email, Force From name, and Delete data on uninstall.
- Empty until you fill it in: SMTP Host. While the host is empty and nothing is being queued, M SMTP leaves PHPMailer's settings alone and WordPress keeps sending with PHP mail(); messages are still logged. That stops being true the moment a rate limit is exceeded or Optimized sending is ticked, because the send is then diverted into a queue that has no server to deliver through. Section 12 covers this, and it is the first thing there for a reason.
Where the screens live
M SMTP adds one top level menu named M SMTP, with an envelope icon, sitting below Settings in the admin sidebar. The interface is in English even on a Lithuanian or Polish site, so the labels below are the ones you will actually see on screen. All five screens require the manage_options capability. On a single site that means Administrator. On multisite it means Super Admin and every site Administrator of that subsite as well: WordPress does not take manage_options away from site Administrators in a network, it takes away the plugin, theme, user and core update capabilities instead. Anyone who can open Email Log can read the stored message bodies of that site, which matters for the warning in Section 5. Developers can change the requirement with the msmtp_capability filter. Every plugin screen carries a Help tab called "Background sending" in the top right corner of wp-admin, which explains the WP-Cron caveat and prints a ready made cron line for your site.
- M SMTP, then Settings: the SMTP server, sender address, log, tracking, queue and rate limits. URL slug msmtp.
- M SMTP, then Email Log: every message with status, source plugin and engagement. URL slug msmtp-log.
- M SMTP, then Reports: sent and failed counts with open and click rates. URL slug msmtp-reports.
- M SMTP, then Routing: the conditional rule builder. URL slug msmtp-routing.
- M SMTP, then Tools: connection test, test email, manual queue run and manual log purge. URL slug msmtp-tools.
Settings: Primary SMTP Server
This is the connection every email uses unless a routing rule sends it elsewhere. The shipped defaults are: Port 587, Encryption TLS, Auto TLS on, Authentication on, and Host, Username and Password all empty. The port preset buttons (25, 465, 587, 2525) fill in the port and also select the matching encryption: 25 gives None, 465 gives SSL, 587 and 2525 give TLS. The connection timeout is fixed at 10 seconds and is not exposed as a setting. The password is stored in the database encrypted with AES-256-CTR, using a key derived from your site's security keys, the auth salt written in wp-config.php. That cipher carries no integrity check, so a key that no longer matches does not produce an error: it produces a different password, which the plugin then sends to your mail server. Section 12 explains what that looks like in the log, because it looks like nothing to do with encryption. If OpenSSL is unavailable the plugin can only obfuscate the password with base64 and says so in a notice on its own screens. Any of the seven fields can be locked from wp-config.php; a locked field is greyed out in the form and labelled "Defined in wp-config.php". The constants apply to the primary connection only and never to additional connections.
- SMTP Host: empty by default. As long as it is empty, mail goes out through PHP mail() and an amber notice appears on the plugin screens.
- Port: 587 by default.
- Encryption: None, SSL (SMTPS, usually port 465) or TLS (STARTTLS, usually port 587). TLS by default.
- Auto TLS: on by default. Upgrades the connection opportunistically when the server offers STARTTLS.
- Authentication: on by default. Unticking this option hides the username and password fields and sends the credentials as empty.
- Username and Password: empty by default. Leaving the password field blank on save keeps the stored one; it is never printed back into the form.
define( 'MSMTP_SMTP_HOST', 'smtp.example.com' );
define( 'MSMTP_SMTP_PORT', 587 );
define( 'MSMTP_SMTP_ENCRYPTION', 'tls' ); // none | ssl | tls
define( 'MSMTP_SMTP_AUTOTLS', true );
define( 'MSMTP_SMTP_AUTH', true );
define( 'MSMTP_SMTP_USER', 'user@example.com' );
define( 'MSMTP_SMTP_PASS', 'your-smtp-password' );
Settings: From Address
Two fields, From Email and From Name, each with its own "Force this From ... on all outgoing mail" checkbox. Both fields are empty by default and both Force checkboxes are off. There is a distinction here that the screen does not spell out: without the Force checkbox ticked, the value is used only to fill in the sender shown in the Email Log when the message carried no From header of its own. The outgoing message itself is rewritten only when Force is ticked. If you want every plugin on the site to send as the same address, tick Force. If the forced address is not valid it is silently ignored and the original sender is kept.
Settings: Email Log
Log message body is on by default: the full message content is stored so that emails can be read in the log and re-sent later. Retention is 30 days by default; older entries are deleted by the daily msmtp_purge_logs cron event, and 0 means keep forever. Queued mail is never removed by retention, only delivered or failed mail is. There is one more limit with no control on the screen: log_body_max, a per message cap of 1048576 bytes (1 MB) held in the msmtp_settings option. A body larger than that is stored cut short and flagged as truncated, which means it can still be read but not re-sent or forwarded. Turning Log message body off does not stop logging: subject, recipients, headers, attachment names, status and errors are all still recorded. But the screen's wording about content being discarded after delivery describes only half of what happens. On the ordinary immediate send path the body is never written to the database at all. The delayed drop applies only to messages held for the queue, whose body is kept verbatim because that row is the only copy of the message, and is cleared once the message has been delivered, failed permanently or expired.
With body logging on, and it is on by default, your database holds a readable copy of every password reset link, login link, invoice and customer message the site has sent in the last 30 days. Anyone with the manage_options capability can open them in the Email Log, which on a network means every site Administrator of that subsite and not only the Super Admin, and anyone with a copy of a database backup can read them offline. Reading is not the whole of it: the Forward button will deliver one of those stored reset links to any address typed into the box at that moment, and it leaves nothing behind but a Forwarded line on the original row. A stored reset link stays usable until WordPress expires it, and the Re-send button will happily deliver it to the original recipient again. On sites handling personal or payment data, shorten the retention window, or turn Log message body off and accept that Re-send and Forward stop being available. If you do shorten it, keep it at least as long as your longest rate limit window: the limiter counts sent rows that are still in the log, and the purge deletes the rest, so a short retention quietly guts a long limit (Section 12).
Settings: Open & Click Tracking
Tracking is off by default and has two checkboxes. The first enables an invisible one pixel image and rewrites links in the message; the second additionally stores the visitor's IP address against the open or click. Be precise about what gets rewritten: the rewriter matches quoted href attributes on <a> elements and nothing else. An unquoted href, a bare URL typed into the text, and a URL carried by any other element are all left alone, so plainer HTML mail often ends up with the pixel and no rewritten links at all. Tracking applies only to messages whose content type contains text/html; plain text mail is left untouched. Both the pixel and the rewritten links point at your own site's home URL with the query string msmtp=o or msmtp=c, a token, and an HMAC signature derived from the site's security keys (auth salt). The endpoint answers 403 only when the signature does not match, when the encoded destination cannot be read, or when the token belongs to no logged message. A request that simply lacks those parameters, or carries a token of the wrong shape, is ignored and the page loads as normal. One more thing worth knowing before you enable it: the recipient's user agent string is recorded on every open and click whenever tracking is on, whether or not you enable IP storage. Only the IP is governed by the second checkbox.
- Rewritten links route the recipient through your site before reaching the real destination. If the site is down or slow, so is every link in every email you have already sent.
- If the site's security keys are rotated, tracking links and pixels in mail already delivered start answering 403, because the signature is checked against the new salt. Moving the site to a different domain fails differently: the signature covers only the type, the token and the destination, never the host, so a link that still reaches the site keeps working, and a link that does not gets whatever now answers on that address, or no answer at all.
- There is no exemption for authentication mail. On any site where outgoing mail is declared as HTML, and that is most sites running WooCommerce or a form plugin, a password reset or login link is rewritten into a redirect through your own site. An already delivered reset email therefore stops working exactly when the site does. Leave tracking off on sites with one or two administrators; if you are locked out with tracking on, the way back is WP-CLI or a direct password change in the database, not email.
- Tracking is personal data processing. Mention email open and click tracking in your privacy policy.
Settings: Sending & Rate Limits
Optimized sending is off by default. When on, every message is written to the queue instead of being delivered during the page request, and the msmtp_process_queue cron event drains it in batches of 20 every minute. Rate limits are five separate numbers, per minute, per hour, per day, per week and per month, all 0 by default, and 0 means no limit. They are rolling windows counted against messages already marked sent in the log, and "per month" means a rolling 30 days rather than a calendar month. The limits are global rather than per connection: the counter looks at every row marked sent in the log, whichever SMTP server delivered it, so a newsletter pushed through a second connection eats the same allowance as your order emails. Anything over a limit is queued with a wait until the oldest counted message drops out of the window, never shorter than 60 seconds. Two things override the queue: messages with attachments skip Optimized sending, because only file paths are stored and the file may be gone by the time the queue runs, and any message whose body exceeds 4 MB is always sent immediately, even over a rate limit, because a copy that large cannot be stored intact. Rate limits still apply to attachment mail, and when one bites, those attachments must stay on disk until the queue delivers them. If a file is gone by then, the queue does not hold the message back: the missing attachment is noted on the timeline and the message is sent without it, with the row still reading Sent. In the queue a message gets at most five delivery attempts in total, the first one plus up to four retries spaced 60 seconds, 5 minutes, 30 minutes and 2 hours apart, and is then failed permanently. That is roughly two and a half hours from the first attempt, not days. The code also carries a 6 hour step, but the fifth attempt is already terminal, so that step never runs. Do not enable either feature while SMTP Host is empty; Section 12 explains why.
define( 'DISABLE_WP_CRON', true );
* * * * * curl -s https://example.com/wp-cron.php > /dev/null
The queue is only as reliable as WP-Cron, and WP-Cron only runs when someone visits the site. Turn on Optimized sending, or set any rate limit, on a low traffic site with no real cron job, and password resets, order confirmations and new user notifications stop arriving with no error anywhere. Seven days past their due time, queued messages are marked failed with "Expired in the queue before it could be delivered". Whether the content survives that depends on your settings: with Log message body on and the body under the 1 MB cap the copy is kept and the message can still be re-sent, but with body logging off the content is destroyed at that moment and the message is gone for good. If the only administrator account needs a password reset while the queue is stuck, then nobody gets back in by email. Before enabling either feature, disable WP-Cron in wp-config.php and call wp-cron.php from a real system cron every minute, using the snippet above with your own domain.
Additional connections and Smart Routing
Below the main settings form, the Additional SMTP Connections block lets you define extra servers, each with its own label, host, port, encryption, Auto TLS, authentication, username and password. New rows default to port 587, TLS, Auto TLS on and authentication on, exactly like the primary. These have their own Save Connections button, separate from Save Settings. The Routing screen then decides which connection each message uses. Rules are evaluated from top to bottom and the first match wins; a message matching nothing goes through the primary connection. Each rule is a set of conditions combined with either "all conditions match" or "any condition matches", pointing at one connection. A condition compares a field against a value with contains, does not contain, equals or starts with, and all comparisons are case insensitive. The available fields are Subject, Message body, From address, To address and Source plugin. Source values are written as plugin:slug, theme:slug or wp-core, for example plugin:woocommerce.
- Saving Additional SMTP Connections rewrites the whole list from what the form submitted. A row you removed in the browser is gone after saving, and so is any row whose Host you cleared.
- Conditions with an empty value are discarded when you save, and a rule left with no conditions is discarded with them.
- A rule pointing at a connection that no longer exists is skipped at send time. Its email quietly falls through to the next matching rule, or to the primary connection.
- The MSMTP_SMTP_* constants never apply to additional connections, and MSMTP_SMTP_PASS rescues the primary connection only. Every additional connection always resolves its password from the encrypted value in the database, with no constant that can stand in for it. After the site's security keys change, each additional connection needs its password typed in again followed by Save Connections. Until you do that, routing rules keep handing their mail to a connection that cannot authenticate, because a rule is skipped only when its connection id is missing, never when its credentials are wrong.
The Tools screen
Tools holds four actions, all of which run through the plugin's own REST namespace msmtp/v1 and require the same capability as the screens. Test Connection opens a connection to the chosen server, negotiates encryption and authenticates without sending anything, and prints the full SMTP conversation below the buttons. Send Test Email sends a real HTML message, pre-filled with the address of the logged in administrator. Process queue now runs one queue batch immediately instead of waiting for cron, and reports how many were sent, failed and deferred. Purge old logs now runs exactly the same code as the daily cron event: in one pass it first expires queued mail that has been due for more than seven days, then deletes entries older than the retention window. A queued message that has been stuck for longer than the retention window is therefore expired and deleted by the same click, leaving no row behind at all. The transcript shown by the two test actions deliberately omits the message payload and never contains credentials: PHPMailer's debug level is capped at 3, below the only level at which it would print the authentication exchange in the clear.
The Email Log and Reports screens
Email Log lists every message with Subject, To, Source, Status, Opens / Clicks and Date, twenty rows per page, and that number is fixed: no Screen Options control for it is registered. Above the table are the views All, Sent, Failed and Queued, a search box covering subject, recipient and sender, a source dropdown and a date range filter. Opening a row shows the full record: addresses, headers, attempts, attachment names, the SMTP transcript of the last failure, a timeline of every event, and the message itself rendered in a sandboxed frame with tracking removed, plus the raw HTML source underneath. Re-send and Forward are offered only when the status is Sent or Failed and a complete copy of the body was stored; queued and in flight messages do not get the buttons, because the queue worker owns them. The two actions then behave differently, and the difference matters. Re-send copies the row into a new log entry and delivers that copy, so the record of the first delivery survives and the rate limiter counts the new one. Forward sends the stored message to one address, skipping CC and BCC, and writes no log row at all: the only trace is a Forwarded entry on the original message's timeline. A forward is therefore invisible to the Sent view, to Reports and to the rate limiter, even though your provider counted it as a real message. It also goes out carrying the original row's tracking URLs, so an open of the forwarded copy is credited to the original message. Rate limits change what the buttons do as well: when a window is exhausted, Forward refuses outright and sends nothing, while Re-send reports success but has actually only placed a copy in the queue for later. Reports shows the last 7, 30 or 90 days as four tiles, Sent, Failed, Open rate and Click rate, plus a daily chart. The two rate tiles only mean anything if tracking is on.
The Re-send bulk action sends real email to the original recipients. There is no test mode, no preview and no undo; the only thing standing between you and a real send is one browser confirm box, "Re-send the selected emails now?". The log holds twenty rows per page, so selecting the page checkbox, clicking through that box and picking Re-send by mistake delivers twenty duplicate order confirmations, invoices or password reset links to real customers; two hundred is the ceiling a single request will accept, not what one page holds. The request is also fully synchronous, one complete SMTP transaction per message, so on shared hosting it can hit max_execution_time part way through the loop: an unknown number of duplicates has already gone out, the browser only says the request failed, and the summary count is lost. Re-send in small batches, and read the Email Log rather than the on screen message to find out how far it actually got. The Delete bulk action is equally final: it removes the log rows and their event history from the database at once, guarded by nothing more than the same kind of browser prompt.
First run, in order
Configure the plugin in this order and you will never be more than one step from a working setup. Do not turn on the queue, rate limits or tracking on the first day: get plain SMTP delivery proven first, then add one feature at a time and send yourself a test after each.
- On M SMTP, Settings, fill in SMTP Host, then click the port preset your provider documents (587 for STARTTLS, 465 for SSL). Confirm the encryption it selected matches what your provider says.
- Leave Auto TLS and Authentication on, enter the Username and Password, and click Save Settings.
- Go to Tools, leave the connection set to Primary, and click Test Connection. Read the transcript: it should end with a successful authentication.
- Still on Tools, click Send Test Email and check the inbox, including the spam folder.
- Back on Settings, set From Email and From Name, and tick both Force checkboxes if you want them applied to outgoing mail rather than just to the log.
- Leave Log message body on and Retention at 30 days unless you have a reason to change them. Save.
- Send yourself something real, a password reset for a test account, and open it in the Email Log to confirm the status is Sent and the source is recognised.
- Only now consider Optimized sending or rate limits, and only once Test Connection passes and a real system cron is calling wp-cron.php every minute. Neither is safe while SMTP Host is empty.
- Add extra connections and routing rules last, testing each new connection from Tools before any rule points at it.
Dangerous operations and how to recover
M SMTP does not filter traffic, block logins or firewall anything, so it cannot lock a visitor out of the site directly. What it can do is stop email leaving, and on a WordPress site email is how you get back in when you are locked out. Everything below is a real way to break delivery or destroy data, with what actually undoes it. Read the first two items even if you read nothing else: they are the ones that do not look like themselves.
- Turning on Optimized sending, or setting any rate limit, while SMTP Host is still empty. The decision to queue a message is taken before anything checks for a host, and short-circuiting the send stops WordPress falling back to PHP mail(). So a site that was delivering perfectly well through PHP mail() stops delivering at all: the queue worker asks for a connection, gets "No SMTP host is configured", retries four times and fails the message permanently about two and a half hours later. Password resets die with it and nothing outside the log says so. Recovery: on Settings set every rate limit back to 0, untick Optimized sending and save. New mail goes straight back out through PHP mail(). The rows that already failed are a separate problem, because Re-send and Forward always require a working SMTP connection: on a site that never had one they answer "No SMTP connection is configured." and send nothing. So either fill in SMTP Host, pass Test Connection, and only then re-send the failed rows from the Email Log, or give up on those rows and trigger the mail again at source, by requesting a fresh password reset or resending the order email from WooCommerce. Their bodies are still there as long as Log message body is on and the message was under 1 MB, so nothing is lost by configuring SMTP first and trying.
- Rotating the WordPress security keys and salts in wp-config.php while the SMTP password lives in the database. The stored password is encrypted with AES-256-CTR, which has no integrity check, so decrypting it under the new key does not fail: it returns a different string. The plugin hands that string to your real SMTP server, which rejects it, and the log fills with ordinary authentication failures, typically 535 or "Could not authenticate", with a transcript that connected, negotiated encryption and reached AUTH. Nothing in it mentions decryption or security keys. If you have just changed salts and every message suddenly fails to authenticate, that is the cause, however firmly the transcript points at your provider. Tracking breaks at the same moment, and there it is visible: every pixel and rewritten link in mail already delivered starts returning 403, because the HMAC comes from the same salt. Recovery: open Settings, type the SMTP password in again and click Save Settings, then do the same for every row under Additional SMTP Connections and click Save Connections. Defining MSMTP_SMTP_PASS protects the primary connection against the next rotation and does nothing for the additional ones. Old tracking links cannot be restored.
- Leaving mail in the queue for more than seven days. Whatever the cause, a broken cron, a deactivated plugin or a rate limit that never clears, seven days after a message was due it is marked failed with "Expired in the queue before it could be delivered". With Log message body on and the body under 1 MB the copy survives and the row can still be re-sent; with body logging off the content is destroyed at that moment and the message is unrecoverable. Worse, the pass that expires the row is the same pass that enforces retention, so a queued message older than the retention window is expired and then deleted in one run, leaving nothing behind. The only preventive step is Tools, Process queue now before the week is up.
- Setting a rate limit that is too low. Limits apply to every message, including password resets and new user notifications, not just bulk mail. Anything over the limit is queued, and if WP-Cron is not running it stays there until the seven day expiry above. Recovery: set every limit back to 0 and save, then Tools, Process queue now. If you cannot reach wp-admin, delete the msmtp_settings option, which resets all settings to the shipped defaults, including turning every limit off.
- Turning on Optimized sending without a real cron job. Every message goes to the queue and nothing leaves the site. The symptom is a growing Queued view in the Email Log with no errors anywhere. Recovery: untick Optimized sending and save, then click Tools, Process queue now. One click sends one batch of twenty, so click it as many times as it takes; if hundreds are waiting, set up a real system cron first and only then try again.
- Deactivating the plugin while the queue is not empty. Deactivation clears both cron events on the site it runs on, so anything still queued there stops moving; on a network-wide deactivation every other subsite keeps both events scheduled with nothing answering them, and its queue stops just the same. Recovery: reactivate, which reschedules both events, then run the queue from Tools, but only if you do it inside the seven day window above. And if the plugin was "deactivated" by renaming its folder over SFTP, WordPress never runs the deactivation hook at all: both events stay on the schedule with nothing answering them, and the queued mail keeps ageing towards expiry.
- Enabling tracking on a site with one or two administrators. Every HTML message the site sends, password resets and login links included, then carries links that travel through the site itself. If the salts have been rotated those links answer 403; if the site is down or gone they answer nothing at all. Either way the email already sitting in your inbox will not let you back in. Recovery is WP-CLI or a direct password change in the database. Links already delivered stay broken.
- Shortening Retention. The daily purge deletes log entries older than the window permanently, with no undo and no export first. Dropping 30 days to 1 discards almost the entire log at the next daily run. Purge old logs now does the same thing immediately. It also quietly weakens your rate limits: the limiter counts sent rows that are still in the log, so a window longer than the retention period can never see its own history. Retention of 7 days with a per month limit of 5000 means the monthly cap counts at most seven days of sending and stops protecting the provider account long before it is reached. Keep Retention at least as long as your longest rate limit window.
- Deleting log entries. The Delete row action and the Delete bulk action remove the message and its event history from the database at once.
- Bulk Re-send. Real messages go to the original recipients again, up to two hundred per request, with no undo, and a request that times out part way through has still delivered everything up to that point.
- Clearing an additional connection's Host, or removing its row before saving. The connection disappears, and routing rules pointing at it are silently ignored, so their email quietly moves to the primary server. Nothing warns you.
- Enabling Delete data on uninstall and then deleting the plugin. Both tables are dropped and every option removed. Only a database backup brings the log back.
The single most damaging mistake is changing the salts in wp-config.php on a site whose SMTP password lives in the database, and the reason it costs days is that it does not announce itself. There is no decryption error. The plugin quietly sends a wrong password, your mail server answers with an authentication failure, and every piece of evidence in the transcript points at the provider. So whenever authentication starts failing on a connection that was working and nothing changed on the provider's side, check the salts first. Putting the password in MSMTP_SMTP_PASS prevents this for the primary connection only; additional connections have no such escape and have to be re-entered by hand.
Emergency recovery without wp-admin
If mail has stopped and you cannot get into wp-admin, work from the shell or from SFTP. The plugin ships no WP-CLI commands of its own, so the commands below are core WP-CLI acting on the option names and cron hooks it uses. It writes five options in all, msmtp_settings, msmtp_connections, msmtp_routing_rules, msmtp_db_version and, briefly during a schema upgrade, msmtp_upgrade_lock; the block below touches three of them. Deleting msmtp_settings really is safe: the plugin rebuilds it from its shipped defaults on the next request, which turns every rate limit off, turns Optimized sending off, turns tracking off and turns body logging back on. You lose the configuration, not the tables or the log. Deleting msmtp_connections is not in that class. It is the only store for the primary connection and every additional one, so it takes the hosts, usernames and encrypted passwords with it and each credential has to be typed in again; routing rules pointing at a connection that no longer exists are silently skipped at send time. It also does not restore PHP mail() when MSMTP_SMTP_HOST is defined in wp-config.php, because the constant is laid back over the primary connection on the very next request; the stored passwords are destroyed either way. If you have no shell either, rename the plugin folder over SFTP: WordPress deactivates a plugin whose file has disappeared and mail returns to PHP mail(). No data is touched, but the deactivation hook does not run, so msmtp_process_queue and msmtp_purge_logs stay scheduled against a hook nothing answers and anything already queued keeps ageing towards its seven day expiry. Rename the folder back and run Tools, Process queue now inside a week.
# Reset every M SMTP setting to its shipped default
# (turns off all rate limits, Optimized sending and tracking)
wp option delete msmtp_settings
# WARNING: this also destroys every stored SMTP password,
# for the primary connection and for all additional ones.
# It does NOT restore PHP mail() if MSMTP_SMTP_HOST is defined:
# the constant puts the host straight back on the next request.
wp option delete msmtp_connections
# Drop all conditional routing rules
wp option delete msmtp_routing_rules
# Send ONE batch of 20 from the queue, right now
# (run it again for the next 20)
wp cron event run msmtp_process_queue
# Turn the plugin off completely; wp_mail() reverts to PHP mail()
wp plugin deactivate m-smtp
# No shell? Rename the folder over SFTP and WordPress deactivates it
# (silently: the deactivation hook does NOT run, cron events stay):
# wp-content/plugins/m-smtp -> wp-content/plugins/m-smtp.off
Troubleshooting
Almost every report comes down to one of the items below. Start at the Email Log: the status column, the error under a failed row and the SMTP transcript in the detail view will usually name the cause outright. The one case where the log points in the wrong direction is a salt rotation, so it gets its own entry.
- An amber notice says M SMTP is not configured yet. SMTP Host is empty and WordPress is still using PHP mail(). Fill in the host, and do not tick Optimized sending or set any rate limit until you have.
- Test Connection fails. The port and encryption almost always disagree: 465 needs SSL, 587 and 2525 need TLS, 25 needs None and is blocked by most hosts anyway. The transcript shows exactly how far the conversation got.
- Every message suddenly fails with an SMTP authentication error, typically 535 or "Could not authenticate", and the transcript reaches AUTH. If nothing changed at your provider, the salts in wp-config.php changed. The stored password now decrypts to something else and is sent as-is, with no decryption error anywhere. Re-enter the SMTP password on Settings, and on every additional connection, then save. Define MSMTP_SMTP_PASS to protect the primary connection from the next rotation.
- A message fails with an error naming the stored password and decryption. That is a corrupted or truncated stored value, or OpenSSL disappearing from the server after the password was encrypted. It is not what a salt change looks like. Re-enter the password.
- Messages sit in Queued and never move. WP-Cron is not running. Click Tools, Process queue now to confirm they send, then set up a real cron job.
- Queued messages fail with "No SMTP host is configured". Optimized sending or a rate limit pushed them into the queue while SMTP Host was still empty. Setting every limit to 0 and unticking Optimized sending puts new mail back on PHP mail(), but it does not bring the failed rows back: Re-send and Forward always require a working SMTP connection and answer "No SMTP connection is configured." while the host is empty. To recover those rows, fill in the host and pass Test Connection first; otherwise trigger the mail again at source.
- Messages go out immediately even though Optimized sending is on. They carry attachments, or the body is over 4 MB. Both cases bypass the queue by design.
- A delivered message arrived without its attachment. The file was gone from disk by the time the queue ran. The message was sent anyway and the row still says Sent; the only record is one line on the detail view timeline. Check the timeline of any attachment message that passed through the queue.
- A forwarded message is nowhere in the log or in Reports. It never will be. Forward writes no row of its own, only a Forwarded entry on the original message's timeline, and the rate limiter does not count it either.
- The Re-send button is missing. The stored copy is incomplete: body logging was off when the message was sent, the body was over the 1 MB storage cap, or the row is currently Queued or Sending.
- A row is failed with "Send was intercepted by another plugin". Another mail plugin short-circuited the send before M SMTP could hand it to SMTP. Deactivate the other mailer.
- A row is failed with "Delivery status unknown". The request died between logging the message and the mailer reporting a result, and the row sat in Sending for over 15 minutes. Whether it arrived cannot be determined from the log.
- Open rate is 0 in Reports. Tracking is off, the mail is plain text, the links were not in a form the rewriter matches, or the recipients' clients block remote images, which is the norm.
- An admin notice says OpenSSL is not available. The password can only be obfuscated, not encrypted. Move it into MSMTP_SMTP_PASS.
Privacy and what is stored
M SMTP makes no outbound connections other than to the SMTP server you configure. There are no external services, no licence checks and no analytics. Everything it knows lives in your own database. The wp_msmtp_emails table holds, per message, the recipients including CC and BCC, Reply-To, the sender, subject, message body when body logging is on, the custom headers, the names, paths and sizes of attachments (the files themselves are never copied), the delivery status and error, the SMTP transcript of the last failure, which plugin or theme sent the message, and the created, sent, opened and clicked timestamps. The wp_msmtp_events table holds the timeline: queued, attempt, sent, failed, resent, forwarded, open and click, with the clicked destination URL. The transcript never contains the message payload or the authentication exchange, by design.
- Options written to wp_options: msmtp_settings, msmtp_connections, msmtp_routing_rules, msmtp_db_version and, briefly during a schema upgrade, msmtp_upgrade_lock. One transient, msmtp_plugin_names, caches plugin names for one hour.
- The SMTP password is stored encrypted with AES-256-CTR. Defining MSMTP_SMTP_PASS keeps it out of the database entirely, which is the recommended arrangement on any site whose database is backed up off site. It covers the primary connection only.
- With tracking on, each open and click records the recipient's user agent string. The IP address is recorded only when you additionally tick the IP option, which is off by default.
- Retention defaults to 30 days, after which delivered and failed entries and their events are deleted. Queued mail is exempt from retention until it expires, at which point it becomes a failed entry and is subject to it like any other.
- A forwarded message leaves only a Forwarded event on the original row. There is no separate record of who it went to beyond that event's detail.
- Message content is what makes the log sensitive. Everything else is metadata; the body may contain reset links, invoices and personal data.
Uninstalling
Deactivating and deleting are two different things here. Deactivation clears both scheduled events, msmtp_process_queue and msmtp_purge_logs, and stops the plugin touching wp_mail(), so WordPress goes back to PHP mail() immediately. On a network that clearing reaches only the site it runs on: activation loops over every site, deactivation does not, so after a network-wide deactivation both events stay scheduled on every other subsite with nothing answering them, exactly as in the SFTP rename case in Section 12. The tables, the log, the settings and the connections all stay exactly where they are, and reactivating picks up where you left off, subject to the seven day queue expiry in Section 12. Deleting the plugin from the Plugins screen runs the uninstall routine, and what that routine does depends entirely on one checkbox: Settings, Uninstall, "Delete all settings and email logs when the plugin is uninstalled", which is off by default. When it is not ticked, deleting the plugin removes the files and leaves every table and option behind, so a later reinstall finds the old log intact. When it is ticked, both tables are dropped, the five options and the cached transient are deleted, and both cron events are cleared. On multisite the uninstall routine runs for every site in the network, and the checkbox is read separately for each of them.
There is no export before the drop. Once the uninstall routine runs with "Delete all settings and email logs" ticked, wp_msmtp_emails and wp_msmtp_events are gone, along with every message body, every delivery record and every tracking event. Take a database dump first if the log has any evidential value to you, for instance as proof that an order confirmation was actually delivered.
Automatic updates
The plugin checks majevski.com for new releases and offers them through the normal WordPress update screen — the same prompt, changelog popup and one-click install as any other plugin. Checks are cached, never slow a page down, and fail open: if majevski.com cannot be reached, the site simply carries on and tries again later. An update package is only ever accepted from majevski.com over HTTPS, and an older version is never offered. Versions before 1.1.0 predate the update channel, so they cannot see new releases — install 1.1.0 or newer manually once, and every later release arrives on its own.
Want something like this built?
Everything on this page was designed, built and is run by one person. If you need the same for your business, tell me what you have in mind.
Book a call opens in a new tab