Implementing Postfix Mail Forwarding with Copy Retention Using MySQL Backend


2 views

When moving from Plesk to iRedMail (Postfix/Dovecot/Amavis stack), one critical feature often missing is the "redirect with saved copy" functionality. In Plesk, this was straightforward, but with Postfix using MySQL virtual alias maps, we need a more technical solution.

Your current configuration uses:

virtual_alias_maps = mysql:/etc/postfix/mysql_virtual_alias_maps.cf

With a typical query like:

SELECT destination FROM virtual_aliases WHERE source = '%s'

1. Recipient Delimiter + Catch-all

Modify your MySQL query to handle both forwarding and local delivery:

SELECT 
    CONCAT(original_user, '+forwarded@example.com') AS destination 
FROM 
    virtual_aliases 
WHERE 
    source = '%s'
UNION
SELECT
    original_user AS destination
FROM
    virtual_aliases
WHERE
    source = '%s' AND keep_copy = 1

2. Using Transport Maps

Create a transport map for special handling:

# /etc/postfix/transport
save_and_forward@example.com  local:user@example.com,smtp:forward@external.com

3. Advanced: Using Sender-Dependent Transport

Combine sender_dependent_relayhost_maps with transport_maps:

# postfix/main.cf
sender_dependent_relayhost_maps = mysql:/etc/postfix/mysql_sender_dependent.cf
transport_maps = mysql:/etc/postfix/mysql_transport_maps.cf

Here's a tested solution using recipient_canonical_maps:

# /etc/postfix/main.cf
recipient_canonical_maps = mysql:/etc/postfix/mysql_recipient_canonical.cf

# mysql_recipient_canonical.cf
user = postfix
password = yourpassword
hosts = 127.0.0.1
dbname = postfix
query = SELECT CONCAT('%u', ' ', '%u', ' ', destination) 
        FROM forwarding_table 
        WHERE source = '%s' AND keep_copy = 1

Extend your virtual_aliases table:

ALTER TABLE virtual_aliases 
ADD COLUMN keep_copy TINYINT(1) DEFAULT 0,
ADD COLUMN delivery_method ENUM('local','remote') DEFAULT 'remote';

When implementing these solutions:

  • Add proper indexes on source/destination columns
  • Consider using MEMORY tables for high-volume forwarding
  • Monitor postfix queue size during migration

Always check:

postmap -q user@domain.com mysql:/etc/postfix/mysql_virtual_alias_maps.cf
postconf -n | grep alias
tail -f /var/log/mail.log

When migrating from Plesk to iRedMail, one common pain point is replicating the "redirect with saved copy" functionality. The default Postfix + MySQL virtual alias implementation doesn't natively support this hybrid behavior where an email must both be forwarded externally AND delivered locally.

First, let's extend your MySQL table structure to accommodate the copy retention requirement. Add these columns to your aliases table:

ALTER TABLE virtual_aliases 
ADD is_copy_retained TINYINT(1) DEFAULT 0,
ADD original_recipient VARCHAR(255) DEFAULT NULL;

Update your mysql_virtual_alias_maps.cf to handle the new logic:

user = postfixadmin_user
password = your_secure_password
hosts = 127.0.0.1
dbname = postfixadmin
query = SELECT 
  CASE 
    WHEN is_copy_retained = 1 THEN CONCAT(original_recipient, ',', destination)
    ELSE destination
  END 
  FROM virtual_aliases 
  WHERE source = '%s' AND is_active = 1

For emails that need both forwarding and local copy, insert records like this:

INSERT INTO virtual_aliases 
  (domain, source, destination, is_copy_retained, original_recipient) 
VALUES 
  ('example.com', 'user@example.com', 'external@forward.com', 1, 'user@example.com');

Ensure your main.cf includes these critical settings:

recipient_delimiter = +
virtual_alias_domains = mysql:/etc/postfix/mysql_virtual_domains.cf
virtual_alias_maps = mysql:/etc/postfix/mysql_virtual_alias_maps.cf
local_recipient_maps = $virtual_alias_maps

To prevent mail loops when forwarding back to your own domain, add this transport map:

/etc/postfix/transport:
example.com    smtp:[127.0.0.1]

Then in main.cf:

transport_maps = hash:/etc/postfix/transport

After implementing, test with:

postmap -q user@example.com mysql:/etc/postfix/mysql_virtual_alias_maps.cf

This should return both addresses separated by comma when copy retention is enabled.