Disposable Email Addresses with Postfix

As I thought about setting up my website and email, I wanted to have a way to give out disposable email addresses. That way, I can give Widget Co an email address unique to them, and I can know if they sell my email because I will get emails from Sprockets Inc. at my address for Widget Co. In that case, I can trash all email sent to that address, eliminating that spam.

A Possible, but not Ideal Option

I know of a couple people who use the following system: they set up a catch-all email for their domain, and point it at their main inbox. Then, they give name@domain.com to people they want to communicate with, and they give business_name@domain.com to businesses they need to communicate with. In my example, they would give widget@domain.com to Widget Co.

However, there are a couple problems with this system.

  1. Spam. They receive all the spam that is sent to any address at their domain. Granted, they could use spam filtering to solve this, but wouldn’t it be better if it just bounced?
  2. Plausible deniability for the company. It is conceivably possible that a spammer could have made up that address from a dictionary, or someone could have done so deliberately. I would prefer to have a stronger reason to claim that a company sold my email.

A Better Option

Another option would be to set up an alias pointing to the main email for every company you want to communicate with. This has the advantage of dramatically reducing the spam problem, and depending on how creative you get with the addresses you give out, it could potentially address #2 above as well.
However, this option has a flaw that caused me to write it off. That is that you have to manually create an alias for every address you want to give out. The huge advantage of the first option is that you don’t have to pre-plan or keep a list of the addresses you give out.

My “Ideal” Option

As I thought about it (this is before I even had a domain name, so it was purely theoretical), I came up with the idea that maybe I could combine the company name with some sort of hash of the name and a constant secret, so that the disposable email would only be delivered to my inbox if the hash matched its expected value.

That seems hard to read, so I’ll describe the process. Let’s say that we want to give Widget Co an address. We choose widget as the name for the email. We have pre-picked the secret "S3cr3t!!". So to determine the address, we take hash(name + secret), in this case, hash(widgetS3cr3t!!) and get something like 7ab5w9274abe (made up). We then make the email address name+hash@example.com, which in this case is widget+7ab5w9274abe@example.com.

When the mail server gets a message like this, it first checks the regular address list, and if it doesn’t match, it checks the disposable addresses. It performs the hash the same way we did above, and accepts the message if it matches, and bounces the message if it does not match.

This solves the #1 problem from above, and also partially addresses #2, depending on the strength of the hash.

Weak Points

  • Length of the email address. If you already have 6 characters, and you add more to it, you end up with a really long address. I worry this may cause problems with length limits on web forms.
  • Potential brute force. There is not enough room in the email address to use a full hash. That would really be too long, making the above problem worse. Bitcoin, for instance, to mine a block, requires one to find something with a hash with a certain number of zeros at the beginning. I think they’re up to 6 or 7 at this point, which means it is possible to brute force the first 6 or 7 digits of a hash (whichever one Bitcoin uses). This means that 6 or 7 digits of a hash is brute forcible, making this scheme brute forcible.

My Final Choice

I ended up implementing a variation on the above choice, using the first six hex digits of a SHA-256 hash. My choice of six digits is an attempt at a decent tradeoff between the two weak points from above. Yes, it is probably possible to brute force the secret, but the reward for someone using brute force is very low; I will just delete their spam. If your email is more important than mine, you may want a better option altogether.

I also added the concept of a “prefix” to the above option. That enables me to have different sets of disposable addresses if I need to, with potentially different secrets or mailboxes.

So the final address ends up being the concatenation of the prefix, the name, a plus sign (for easy separation), and the first 6 characters of the SHA-256 hash of the name plus the secret associated with that prefix. For example: q1_widget+0d3343@example.com.

Implementation

When I set up my mail server, I made this a secondary concern. I wanted email to work, then I would worry about fancy stuff. I had already decided to use Ubuntu Server, because I was already familiar with it. I found two decent tutorials, and I combined what in my opinion were the best parts of them. I used the tutorial Lee Hutchinson wrote for Ars Technica, How to run your own e-mail server with your own domain, along with the tutorial from Linode, Email with Postfix, Dovecot, and MySQL. I mostly used the Ars Technica tutorial with the exception that I followed the advice from Linode’s tutorial to store your users, password, and aliases in a MySQL database. There is merit to both options, but I was already going to need MySQL for WordPress, so it didn’t cost me a lot of resources.

I’m not going to rehash those tutorials, because they did a far better job, and it is out of the scope for this (already long enough) blog post, but I will explain the parts relevant to disposable addresses.

I’d like to start off with a disclaimer that this is a really ugly way to do this, and there is probably a better way to do it, but it does work.

If you follow the advice from the Linode tutorial I linked, you will have a table called virtual_domains that tracks domains, a table called virtual_users that tracks all your real email accounts, and a table called virtual_aliases that tracks your aliases (PSA: don’t forget to alias postmaster, webmaster, abuse, etc. to an actual account!).

I added a table called email_secrets to track the information associated with disposable addresses. It has four columns: id (auto_increment primary key), user_id (so Postfix can know where to put email for this set of disposable addresses), prefix (to match addresses with rows in this table), and secret (as mentioned earlier).

There is a weakness in this setup: it is not domain specific. If you try to handle multiple domains with one server, the disposable addresses will work at any domain, I think. If this is your use case, then modifying my setup according is left as an exercise to the reader.

However, now we need to get Postfix to use this. In the Linode tutorial, you created a file to inform Postfix about the email_aliases table. Then, you linked to it in the main.cf. We are going to add a second file to that rule. I called mine mysql-virtual-disposable.cf. It looks like this (usernames/passwords changed):

user = username
password = password
hosts = 127.0.0.1
dbname = your_database_name
query = SELECT vu.email FROM virtual_users vu INNER JOIN email_secrets es ON vu.id = es.user_id WHERE (instr("%s", concat(es.prefix,"_")) = 1) AND (mid(sha2(concat(mid("%s",char_length(es.prefix)+2,locate("+", "%s")-char_length(es.prefix)-2),es.secret),256),1,6)) = (mid("%s", locate("+", "%s") +1, locate("@", "%s") - locate("+", "%s")-1));

The important part there is the query, which I will explain in a minute.

Then, I had to change the main.cf line to

virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf, mysql:/etc/postfix/mysql-virtual-disposable.cf

This tells Postfix to first run the query in the mysql-virtual-alias-maps.cf file, and if it doesn’t get a match, to run the query in the mysql-virtual-disposable.cf file before bouncing it if neither match.

That query…

SELECT vu.email FROM virtual_users vu INNER JOIN email_secrets es ON vu.id = es.user_id
WHERE (instr("%s", concat(es.prefix,"_")) = 1)
AND (mid(sha2(concat(mid("%s",char_length(es.prefix)+2,locate("+", "%s")-char_length(es.prefix)-2),es.secret),256),1,6))
= (mid("%s", locate("+", "%s") +1, locate("@", "%s") - locate("+", "%s")-1));

I’m going to try to explain that query line by line. I must confess, I’m not super familiar with SQL. I don’t even know for sure whether you’re allowed to break the lines the way I just did, however, it works for purposes of explanation.

First of all, %s refers to the email address in question. I don’t know if Postfix uses prepared statements or not, but hopefully Postfix mitigates SQL injection… Someone please comment if you know.

Postfix expects to run this query and get one row and one column back. The result should be the email address for the real inbox the email should be delivered to. If it receives no results, it will bounce the email.

Line 1: we want to work with data from the tables virtual_users and email_secrets. We want the rows from email_secrets joined to the rows from virtual_users if the id’s match.

Line 2: This tests each row to see if the email starts with the prefix.

Line 3: This extracts the name from between the prefix that matched in line 2 and the +. It then concatenates the secret corresponding with the prefix and takes the SHA-256 hash. Then, it takes only the first 6 characters. (I apologize for the magic numbers. They account for off by one errors. This is true of the next line as well. Obviously the 256 refers to a SHA-256 hash, and the 1 and 6 refer to taking 6 characters, starting with the 1st position.)

Line 4: This starts with the equals sign, so that means that the result of line 3 (the first 6 characters of the hash) needs to equal whatever this finds. This line pulls out the hash by pulling the text between the + and the @.

If all of this succeeds, the WHERE clause will be true, and MySQL will select virtual_users.email (as we asked for in line 1) and return it to Postfix.

Use

So, how does this work, in practice?

To start out, you need to add a row to the email_secrets table. You will need to include the user_id for the mailbox you want the email to be delivered to. You need to supply a secret and a prefix as well. Keep the secret reasonably secret; it allows you to generate disposable addresses.

Io generate an email address, you first put the prefix and name, then you take SHA256(name+prefix), for instance, SHA256("widgetS3cr3t!!") and put a plus sign, then the first 6 hex digits of the hash, then @yourdomain.com. For instance, q1widget+3eda42@example.com.

To do this, you will need to somehow take that hash. Personally, I use the Linux shell and do:

echo -n "widgetS3cr3t!!" | sha256sum

Conclusion

I have outlined a method for creating unlimited amounts of disposable email addresses if Postfix is your mail server.

If this helps you, or if you have a question, please comment below. I moderate comments, so it may take a day or two for your comment to appear.