Delay e-mail delivery with Postfix for a relaxing weekend

One good piece of advice is never to read e-mails if you want to have a pleasant and relaxing weekend. Unfortunately, it is not so easy: open-source projects you are contributing to, friends, hobbies, and newsletters are all good reasons to read e-mails once in a while.

But we all have that friend, the one that sends many e-mails, and you don’t want to read them on the weekend. Let’s implement some “delay-until-Monday” feature in Postfix!

Postfix queues

Did you know that Postfix has different queues? In fact, Postfix has:

Theoretically, we may just need to keep the e-mail in the incoming queue until the right moment or move it to the active queue and schedule the delivery later. However, neither approaches are really in line with the description of those queues, and, as far as I know, there is no easy way to do that.

We will use the hold queue instead!

Postfix lookup tables

Here is the plan: we will match the incoming e-mail with some ACL using the HOLD action so that Postfix will move incoming e-mails in the hold queue. Then, we can use some bash script and crontab to release them when the moment comes.

Enters the Postfix lookup tables!

Postfix can look up information in tables by matching specific parts of the e-mail (or the envelope) in them to get back other information, such as actions, alias destinations, allowed hosts, and many more. These tables can be stored in a local file or in some network services. It is very handy, as you can basically link Postfix to a database server to query for e-mails and stuff, and this is basically what everyone (who has a large number of addresses) does.

In our case, we need to use an access table. access tables return actions for a match. Specifically, we are looking for the HOLD action. In theory, a file like this should suffice:

[email protected]    HOLD

If you save a file like this, and you use the file: table type in a specific parameter in the Postfix configuration (as we see later), all e-mails coming from [email protected] will be moved to the hold queue, yay!

The problem is that there doesn’t seem to be any way to assign a time period to this rule. This means that the rule will always match, even if you use postsuper to remove it from the queue! It does not work that well. What else can we use?

It turns out we can use a database for that. If you are already using a database, that’s completely fine. However, I don’t have it, as my Postfix setup is very minimal. In this case, we can just use sqlite as a database and query a local file with some SQL magic.

First, we need a SQLite database. Make sure that Postfix is compiled with the sqlite table type (it is by default) and that you have the sqlite3 executable in your path. Then:

$ sqlite3 /etc/postfix/time_based_access_sqlite.db
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
sqlite>

Perfect! Now, let’s create the database table that we will use in order to store information for the Postfix table:

CREATE TABLE time_access_dow (
  email TEXT,
  day_of_week INTEGER,
  action TEXT
);

Now we can close the database file, and we can create the file that holds the “database connection information”. Let’s say, /etc/postfix/time_based_access:

dbpath = /etc/postfix/time_based_access_sqlite.db
query = SELECT action FROM time_access_dow WHERE email = '%s' AND day_of_week = strftime('%%w', current_date)

As you can see, I am using the “day of the week” (0-6, where 0 is Sunday). We can hold e-mails from [email protected] on Sunday by inserting the following:

INSERT INTO time_access_dow VALUES('[email protected]', 0, 'HOLD');

But we are not finished yet. We need to tell Postfix to query this. We will modify the /etc/postfix/main.cf. Specifically, you want to alter the smtpd_sender_restrictions parameter to add your table. Keep in mind that the configured actions are executed using the exact order you specify in the configuration. So, you want your query to be executed last, or at least after the usual checks for blocklists, unknown recipient domains, etc.

smtpd_sender_restrictions = reject_non_fqdn_sender,
      permit_sasl_authenticated,
      ...
      check_sender_access sqlite:/etc/postfix/time_based_access

Restart Postfix to apply. Now, e-mails from [email protected] on Sundays will be on hold!

Release e-mails on hold

Let’s create a script to release them (create the file, then +x it):

#!/usr/bin/env bash
set -eou pipefail

# Get all e-mail addresses that are configured for HOLD
for email in $(sqlite3 /etc/postfix/time_based_access_sqlite.db "SELECT DISTINCT email FROM time_access_dow"); do
  # Release all e-mails from HOLD for this e-mail
  for MSGID in $(postqueue -j | jq -r "select(.queue_name == \"hold\" and .sender == \"$email\").queue_id"); do
    postsuper -H "$MSGID"
  done
done

Now we can schedule this command to run, say, every morning at 5am:

0 5     * * *   root    /etc/postfix/time_based_access.flush

Unfortunately, postsuper must run as a super-user. However, as good practice, you should already know how to configure a user with the necessary permissions to use sudo and AppArmor.

That’s all. You have your own Postfix-powered system to delay e-mail delivery!

Caveats

As you might have noticed, the SQL query only checks for weekdays. So, if the person sends you an e-mail at 00:01 on Monday, that e-mail will be delivered to your account (also, before the others that will be released at 5am). If your friend is a night owl, you may want to tweak the database structure to store also some time information there.

Future improvements

The next improvement will be querying my calendar for holidays and time off. I have a CalDAV server, so it should not be so challenging to work with (I see you laughing, stop!). Maybe next weekend I can work on th… wait, wasn’t I looking forward to a pleasant weekend?!?