Skip to main content

Sending Emails from Web Apps

Some of the kasad.com web apps (currently Vikunja, Bitwarden, Nextcloud, and BookStack) have the capability to send emails. All of these apps are hosted on their own subdomains, so it makes the most sense if emails sent by those apps originate from their respective subdomains.

Expected behavior

  1. All kasad.com email users will be allowed to send mail from (and only from) <username>@kasad.com or <username>+<anything>@kasad.com.
  2. Some special users (the mail accounts for the web apps) will be allowed to send emails from <anything>@<subdomain>.kasad.com as well.

In the above, <username> represents the account's login username. <anything> represents any sequence of letters, numbers, +, ., -, or _. <subdomain> represents a subdomain (e.g. tasks for tasks.kasad.com).

Implementation

The kasad.com mail server runs Postfix for SMTP, so we will focus on configuring that. By default, if authenticated users are allowed to send mail, they can use any From address, including ones they don't own and even ones with a different domain from the server.

Note: Dovecot is used for local delivery and IMAP, but since none of the web apps need to receive mail, we can ignore this.

The smtpd_sender_login_maps option

We can constrict the allowed From addresses using the smtpd_sender_login_maps option in Postfix's main.cf file (/etc/postfix/main.cf). This option allows us to specify a lookup table of email addresses that map to login usernames.

We don't want to hard-code each user's allowed email addresses. Even if we did, it wouldn't handle the <username>+<anything>@kasad.com case or the <anything>@<subdomain>.kasad.com case. To handle these arbitrary addresses, we need to use regular expressions.

Postfix supports regular expression mapping tables. We can use one like this:

smtpd_sender_login_maps = pcre:/etc/postfix/login_maps

This means Postfix will look in /etc/postfix/login_maps to map From addresses to the usernames which are allowed to send mail from those addresses.

Populating the login map table

The login map table maps addresses to usernames in that order. Since we're using regular expressions, the regular expression will match the address. For the username column, we can either hard-code usernames or use capture groups from the regular expression. This will make more sense once you see the example:

/^(\w+)(\+[a-z0-9_+.-]+)?@kasad\.com$/  $1

/^[a-z0-9_+.-]+@cloud\.kasad\.com$/	nextcloud
/^[a-z0-9_+.-]+@auth2\.kasad\.com$/	authentik
/^[a-z0-9_+.-]+@wiki\.kasad\.com$/	bookstack
/^[a-z0-9_+.-]+@bw\.kasad\.com$/	vaultwarden

Each regular expression must be surrounded by slashes. And since we want to match the entire address, we'll put a ^ at the beginning and a $ at the end of each pattern. Postfix automatically uses case-insensitive regular expressions. Adding the i flag here would actually make the patterns case-sensitive.

Explaining the first map entry

The first line contains two capture groups (a pattern enclosed in parentheses). The first capture group captures one or more alphanumeric characters (A-Z and 0-9). This will be the username part of the address. The second capture group captures a plus (+) followed by one or more letter, number, _, +, ., or - character. This is the extension part of the address. The second capture group is followed by a question mark (?), meaning the whole second group is optional. This means it will match both <username>@kasad.com and <username>+<anything>@kasad.com while capturing the username in the first capture group. Finally, the pattern ends with @kasad\.com which will match the literal text @kasad.com.

Next, we'll look at the second column. We know that this will evaluate to the account's username, since the second column contains the username that is allowed to send an email from the address matched in the first column. But $1 is not a username. Except it is! $1 means the contents of the first capture group that was matched. And the first capture group contains the account's username. So the first pattern will map both <username>@kasad.com and <username>+<anything>@kasad.com to <username>.

This accomplishes Expected Behavior #1.

Explaining the rest of the map entries

The rest of the entries are almost exactly the same. They are a bit simpler, since we don't need capture groups. We know that each web app that sends mail has its own subdomain. Since these subdomains are manually assigned, we must hard-code the entries in the login map table.

Each of the remaining patterns begins with [a-z0-9_+.-]+, which will match one or more letter, number, _, +, ., or - character. It's then followed by @<sub>.kasad.com, where <sub> is replaced by a specific subdomain.

The second column then contains the username for the mail account for that subdomain's web app. For example, tasks.kasad.com is the domain for Vikunja, so the vikunja user is authorized for any so-called local part (what comes before the @ symbol) on that domain.