Emails in development, or how Mailcatcher saves the day
on
Most developers that have worked on web applications or websites have had to implement email sending functionality. Emails are extremely useful, as it allows your application to reach its users, even when they haven't visited it in a while, or to let them know that something important has happened. Unfortunately, testing email delivery is, or was, a harder problem to solve.
Now that professional Drupal shops are increasingly using multiple environments (dev, staging, live, etc.), it is important to have facilties to test email delivery. In the past, the recommended method was add to code that would either reroute emails sent from your system to a developer's email address or some "dev-null" type of endpoint (shout-out to /dev/null as a Service). However, this introduces a lot of complexity and introduces a whole to kind of issue: what if the reroute system gets misconfigured and the test email entitled "Test I am a hax0r!!!11!one!" (please don't do that) actually gets sent? Someone will be getting a stern talk-to, at the very least.
What if we could instead replace the email delivery system with something that accepts deliveries, but holds them for review? We could make sure that the email sending code is correct, we could inspect the sent emails, and we could do so without fear that the end-user will mistakenly receive one of these emails. Enter MailCatcher. MailCatcher presents, on one side, an SMTP server, and on the other, an HTTP interface to view and inspect submitted emails.
For a basic installation, the only thing you require is ruby and rubygems. On Ubuntu, this is as simple as issuing the following commands as root (or sudo):
$ apt-get install ruby rubygems $ gem install mailcatcher
Once you have that, you are ready to start using MailCatcher! If you want to start and stop MailCatcher for every project, you can just call mailcatcher
to start a daemonized process, which by default listens on 1025 for SMTP and 1080 for the HTTP UI. However, if you wish to have a server/computer-wide instance of MailCatcher, read on. The first step is to create an init file to start MailCatcher on boot. We built a plain old init shell script, as there's some movement in the Linux init daemons, and shell scripts are a common denominator. The following init script is based on the skeleton script, available on most distros at /etc/init.d/skeleton
:
#!/bin/sh ### BEGIN INIT INFO # Provides: mailcatcher # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: MailCatcher, an SMTP server that holds your mail # Description: MailCatcher provides an SMTP server that will hold # all sent mail for later inspection. ### END INIT INFO # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin DESC="MailCatcher SMTP server" NAME=mailcatcher DAEMON=/usr/local/bin/$NAME DAEMON_ARGS=" --http-ip 0.0.0.0" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.2-14) to ensure that this file is present # and status_of_proc is working. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile --background --exec $DAEMON -- -f \ $DAEMON_ARGS \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME return 0 } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) status_of_proc -p $PIDFILE "$DAEMON" "$NAME" && exit 0 || exit $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac exit 0 # vim: syntax=sh ts=4 sw=4 sts=4 et
The only point of value in this script is the do_start
function. Because MailCatcher forks when started as a daemon and there is no support for having it write its own pid to a file, we have to make start-stop-daemon
fork and write its PID, then have it run MailCatcher in the foreground. There is an issue open on github to add support for a --pidfile
option, but until then, this is the best method to start and control the service.
To enable the service on boot, use your init system's install command, which for Ubuntu, is update-rc.d
(run as root/sudo):
$ update-rc.d mailcatcher defaults
Using this script, you now have an SMTP server that starts on boot, is controlled with the usual daemon utilities, will never deliver mail to the recipients, but still allows you to inspect sent emails, and does so without requiring any changes within the application itself. Woohoo!
EDIT: MailCatcher has a nice script, catchmail
that can replace sendmail
for the default mail()
calls in drupal. Simply set the sendmail_path
to "/usr/local/bin/catchmail"
in your php.ini, and you're done.
Add new comment