Postfix + ActionMailbox - integrating into existing postfix server by using aliases + curl command

5th November 2024 – 1005 words

In the past, I built several e-Mail processing features using Ruby that predate the official ActionMailbox - Such as: Bounce processing, e-mail notifications, newsletters, order confirmations, forwardings etc. So before, I always used a IMAP client run by a great mail_room to fetch emails from a mailbox and process them by directly supplying them to Sidekiq queue. The internal Mail routing (Which email should be processed and shown to which customer etc.) was handled internal, as well as bounces etc. So it was time, that I try out the whole ActionMailbox stack instead, which is a more standardized solution and has great testing support.

We are using Postfix so handle our own e-mail servers - set up, using Mail-In-A-Box. Here I will explain, how to add an additional alias to postfix alongside your existing mail-aliases.

Rails / ActionMailbox Setup

ActionMailbox is installed by default in newer Rails apps. If you do not use require "rails/all" you might have to require it manually in your application.rb:

require "action_mailbox/engine"

Enable relay ingress in the environment, where you want to process/test it config/environments/development.rb / production.rb:

config.action_mailbox.ingress = :relay

If you want to test it in development, you need to find a way to make your server reachable from the mail receiving server - VPN, Proxy, or remote/cloud development machine. In our case, we are using development machines in the cloud, so we can just use the public hostname per app.

Now, add a default routing, in our example we want to handle everything sent to input@, but you can later add identification markers, such as input+9332eas@ or even per-customer aliases.

# app/mailboxes/application_mailbox.rb
class ApplicationMailbox < ActionMailbox::Base
  routing /input@/i => :support_input
  # routing /something/i => :somewhere
end
# app/mailboxes/support_input_mailbox.rb
class SupportInputMailbox < ApplicationMailbox
  def process
    # do something with `mail` or `inbound_email`
    binding.irb
  end
end

Also, set a secret credential for ingress_password BTW: you can run rails secret to generate a secret key any time.

#  rails credentials:edit --environment development
 action_mailbox:
    ingress_password: 0565be1430a7f2868f123ofoaosdjosiio23eoiqoio23o123p120493i90ASDihquwdas

Don’t forget to restart the server.

Postfix Setup

As mentioned, we have an existing alias-mapping supplied by a sqlite3 database (Managed by Mail-In-A-Box). But, postfix supports having multiple virtual_alias_maps and will look up from the left, until any DB has a match:


# before:
virtual_alias_maps=sqlite:/etc/postfix/virtual-alias-maps.cf
# after:
virtual_alias_maps=sqlite:/etc/postfix/virtual-alias-maps.cf,regexp:/etc/postfix/app_postback

And create the file /etc/postfix/app_postback file with the mapping of emails to a new alias. Here you can also use regex to match wildcard customer mails or process identification codes (order-id, customer-id, etc.)

/^input@mydomain.de/ upload_to_dev@localhost
/^bounce\+.*@mydomain.de$/ upload_to_dev@localhost
/^customer-.*@mydomain.de$/ upload_to_dev@localhost

Anytime you change the /etc/postfix/app_postback you need to run the postmap command to update the regexp file:

postmap /etc/postfix/app_postback

You can test the matching against an email address by using the postmap with the -q flag:

$ postmap -q "bounce+29d9@domain.de" regexp:/etc/postfix/app_postback
# if you get upload_to_dev@localhost, it's working, otherwise the output will be empty
> upload_to_dev@localhost

Now we also need to define the upload_to_dev alias in /etc/aliases:

upload_to_dev: "|/usr/bin/curl -u actionmailbox:0565be1430a7f2868f123ofoaosdjosiio23eoiqoio23o123p120493i90ASDihquwdas -H 'Content-Type: message/rfc822' --data-binary @- http://my-cloud-dev-instance.com/rails/action_mailbox/relay/inbound_emails"

And, this file to, needs to be hashed by postfix every single time you change it:

postalias /etc/aliases
service postfix reload

Now, tail the mail.log, send a testmail and wait the binding.irb to show up. If you are using ActiveJob adapters in development, you will also need to start the processor (sidekiq/solid-queue), because the mail is always processed asynchroneously.

Improving failure mode

When your Rails-app is down, the mail will be lost, as there is no retry mechanism in place. The sending party will probably receive a DSN (Delivery Status Notification) by Postfix, though.

We could improve this by wrapping the curl command in a small script, that outputs a “temporary delivery error” on CURL failure, so Postfix responds to the sending e-mail server with a temporary failure, instead of a permanent. Most e-mail server will then retry delivery later on.

#!/bin/bash

/usr/bin/curl -u actionmailbox:0565be1430a7f2868f811fbf82543642975a0a08b6eff1d812d6096a0626 \
  -H 'Content-Type: message/rfc822' \
  -f --silent \
  --data-binary @- \
  http://dev.our-app.com/rails/action_mailbox/relay/inbound_emails

if [[ $? -ne 0 ]]; then
    # Print a 4.4.1 temporary failure message for Postfix logging
    echo "4.4.1 Temporary failure: Unable to reach the Action Mailbox server"
    exit 1  # Signal a temporary failure to Postfix
fi

echo "2.0.0 Successfully delivered"
exit 0
vim /etc/postfix/upload_to_dev
chmod +x /etc/postfix/upload_to_dev

And change the alias to:

upload_to_dev: "|/etc/postfix/upload_to_dev"

Don’t forget to run postalias /etc/aliases and service postfix reload

Alternatively, you can also try: