How to have your own exim SMTP server only deliver at set hours

I've been thinking that something like the Thunderbird Send Later add-on would be better handled on the server side (running your own private SMTP server) so you don't have to keep your desktop running until the scheduled delivery time. I'm using Debian and exim with the split configuration option so the files referenced will directly apply to that. If you don't have a configured exim server yet, set it up with the dpkg configuration tool and add remote SMTP authentication to it if you want to use SMTP from an email client.

I ended up choosing a solution where I control the delivery time thru a custom header that can be set in the email client. I add a header I named X-Delivery-At containing a simple time like "19:00" when sending the mail. Here's a guide for the Thunderbird email client explaining how to add custom headers.

To get exim to do what we ask for in this header we need to add a router to exim and some embedded perl. If you are on Debian like me, install the exim-daemon-heavy package and add this to /etc/exim4/conf.d/main/00_local_settings.

perl_startup =  do '/etc/exim4/exim.pl'
perl_at_start

This will instruct exim to load the perl file containing a subroutine needed for the router when we later create it.

Let's start with the actual router. Again in the split configuration scheme of Debian, create the file /etc/exim4/conf.d/router/050_exim4-defer_delivery and put this in it:

# This router defers delivery until the check_delivery_time perl function
# makes the condition false.
defer_delivery:
   driver = redirect
   domains = ! +local_domains
   condition = ${perl{check_delivery_time}}
   allow_defer
   data = :defer: Deferring delivery until $header_x-deliver-at:
   no_verify

The router wil defer the delivery if the perl subroutine check_delivery_time used as condition returns 'yes'. The router will be skipped otherwise.

Next add the function to /etc/exim4/exim.pl:

use strict;

# compare the value of X-Deliver-At header to local time if the header exists
# Defer mail delivery if header exists and the specified time has not passed.
sub check_delivery_time {
   
    (my $sec, my $min, my $hour) = localtime(time());
    my $header = Exim::expand_string('$header_x-deliver-at:');
   
    if( defined $header ) {
        my @t = $header =~ /(\d{2}):(\d{2})/;
       
        # minutes need not be exact and later during the same day is ok
        if( ($hour == $t[0] && $min >= $t[1]) || $hour > $t[0] ){
            return 'no';
        } else {
            return 'yes';
        }
    }

    return 'no';

}

The subroutine will check for the existence of the header and check whether the time defined in it has passed. It's a simple solution and could have some issues if the scheduled delivery time is just a little bit before midnight for example. It doesn't validate the header input in any way either. It probably should even for just personal use. The actual delivery time will not be exact as it depends on when exim retries delivery.

Now we have a working system for scheduled delivery. Still a few things remain. The custom header isn't something we want to be visible in the final delivered emails and they will still show the time of the client as the time they were sent at without additional modifications. I added a rewrite of the date header and some cleanup to the remote smtp transport at /etc/exim4/conf.d/transport/30_exim4-config_remote_smtp:

headers_remove = Date:X-Deliver-At
headers_add = Date: $tod_full

Doing it there does mean that each and every (remotely) sent email will get their Date header rewritten to be the current time of the server even when not using the scheduled delivery option but that doesn't really matter.

Now the emails will appear to be sent at the time the exim server actually delivers them and the custom header will be gone. To make sure the emails get delivered reasonably close to the specified time there is two more things to do.

Go to /etc/exim4/conf.d/retry/30_exim4-config and change the retry there to something like this:

*                      *           F,20h,3m; F,4d,6h

I admit this is a bit ugly because the rule will make exim retry the delivery of all emails every 3 minutes for the first 20 hours. Unfortunately there's no way to distinguish retries caused by other errors from retries caused by the defer router. You can tweak this to whatever you're comfortable with. A longer interval between attempts will mean that the specified delivery time of your email will be followed less closely.

The second thing affecting the delivery time is how often exim's queue runner is invoked. Set QUEUEINTERVAL in /etc/default/exim4 to a low value for timely delivery, maybe something like "1m" for a queue run every minute.

After these steps you can now schedule your emails to be sent whenever during the next 24 hour period you desire. The scheduling could be better, the actual time that your email client sends the email is still present in the headers and could be hidden. Exim isn't really perfect for this purpose but it works. Let me know if you have any improvements or a better solution.

Tagit: 

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Fill in the blank.