Intercepting mail with MailHog in Laravel Valet

I’m trying out using Laravel Valet to manage the development environments for both my WordPress and Laravel related work. (This is not the result of any dissatisfaction with VVV, which I’ve happily been using for the WP work, and Laravel Homestead, which I’ve happily been using for Laravel work, but comes as a necessity now that I am using a Mac with Apple’s M1 ARM chip, which doesn’t support Intel Vagrant virtual machines.)

The Valet installation and setup was easy and fast using their instructions, and I’ve been able to successfully move projects over to it without a lot of hassle. The one place where I was really bit was taking for granted how those previous virtual environments where handling the intercepting of email generated by my applications, so that they didn’t actually go out onto the Internet for actual delivery. I got used to spinning up environments with copies of production data and not worrying about real users getting email messages.

Well, you can guess what happened when I did that in Valet, which by default delivers email just like any other email generated from a process running on my Mac. Spinning up a WordPress dev site with a slightly outdated database dump and with functionality that is all about notifying users via email of things that are about to happen or have happened meant…lots of emails going out.

Ugh. Apologies, installing Stop Emails and other cleanup ensued.

Fortunately, the long term fix is pretty easy. Yes, there are various plugins one can install on individual WordPress sites to stop email from going out, or to change default SMTP behavior. But I didn’t want to have to worry about that each time I clone a site. So instead I changed the default SMTP behavior for the php-fpm processes that Valet runs to deliver all mail to MailHog.

First, I installed MailHog:

brew install mailhog
brew services start mailhog

Then, I added a smtp-mailhog.ini file in the PHP conf.d directory for my setup. In my case that was each version in /opt/homebrew/etc/php/X.X/conf.d. The contents of that file are:

; Use Mailhog for mail delivery
sendmail_path=mailhog sendmail

Then I ran valet restart and tested by triggering a password reset email from a WordPress site, and confirmed that it had been intercepted by visiting the MailHog interface at http://127.0.0.1:8025.

Of course this setup may not fit every use case, so adjust your config accordingly. (I submitted a PR to the Laravel docs with this info, but understandably it was probably too specific to my setup.) Even if you don’t change the PHP-level settings, with MailHog installed you could set up individual sites/applications to send to it as needed.

Running WordPress cron on a multisite instance

For a long time I used the WP Cron Control plugin and an associated cron job to make sure that scheduled actions on my WordPress multisite instance were executed properly. (You should never rely on event execution that is triggered by visits to your website, the WordPress default, IMHO.) But after the upgrade to WordPress 5.4 I noticed that some of my scheduled events in WordPress were not firing on time, sometimes delayed by 10-20 minutes. I did some troubleshooting and got as far as suspecting a weird interaction between that plugin and WordPress 5.4, but never got to the bottom of it.

When I reluctantly went in search of a new solution, I decided to try using WP CLI cron commands, executed via my server’s own cron service. Ryan Hellyer provided most of what I needed in this helpful post, and I extended it a bit for my own purposes.

Here’s the resulting script that I use:

#!/bin/bash

# This script runs all due events on every WordPress site in a Multisite install, using WP CLI.
# To be run by the "www-data" user every minute or so.
#
# Thanks https://geek.hellyer.kiwi/2017/01/29/using-wp-cli-run-cron-jobs-multisite-networks/

PATH_TO_WORDPRESS="/path/to/wordpress"
DEBUG=false
DEBUG_LOG=/var/log/wp-cron

if [ "$DEBUG" = true ]; then
        echo $(date -u) "Running WP Cron for all sites." >> $DEBUG_LOG
fi

for URL in = $(wp site list --field=url --path="/path/to/wordpress" --deleted=0 --archived=0)
do
        if [[ $URL == "http"* ]]; then
                if [ "$DEBUG" = true ]; then
                        echo $(date -u) "Running WP Cron for $URL:" >> $DEBUG_LOG
                        wp cron event run --due-now --url="$URL" --path="$PATH_TO_WORDPRESS" >> $DEBUG_LOG
                else
                        wp cron event run --quiet --due-now --url="$URL" --path="$PATH_TO_WORDPRESS"
                fi
        fi
done

Then, in my system crontab:

# Run WordPress Cron For All Sites
*/2 * * * * www-data /bin/bash /path/to/bin/run-wp-cli-cron-for-sites.bash

Yes, I run cron every 2 minutes; there are some sites I operate that require very precise execution times in order to be useful. One implication is that this solution does not scale up very well; if the total execution time of all cron jobs across all sites exceeds 2 minutes, I could quickly run into situations where duplicate jobs are running trying to do the same thing, and that could be bad for performance or worse.

Generic ‘send to Slack’ shell script

On any given server I maintain, I like to set up a generic “send a message to Slack” shell script that can be called from any other tool or service running on that machine. With it I can log information of interest to a Slack channel for reading or maybe action.

Here’s what send-to-slack.sh usually looks like:

#!/bin/bash -e

message=$1

[ ! -z "$message" ] && curl -X POST -H 'Content-type: application/json' --data "{
              \"text\": \"${message}\"
      }" https://hooks.slack.com/services/12345/67890/abcdefghijklmnop

That last line has the “incoming webhook URL” provided by Slack when you set Slack up to receive messages via incoming webhooks, something that is included in even their most basic/free tier.

Running the script and sending a message to the channel is as simple as $ sh send-to-slack.sh 'My message goes here' and the result is what you would expect:

Once that’s in place and tested, I can call the script from wherever I want on that server. Other shell scripts. Custom WordPress functions. Cron jobs. And so on.

There are many other ways this could be customized or extended. It’s worth noting that this is not necessarily a fully secure way to do things if you have untrusted users who can control the input to the script and the message that gets output…please remember to sanitize your inputs and escape your outputs!