Transport mapping doesn't work for extensions
If a recipient_delimiter is set the transport lookup for a user specific transport fails.
From virtual(8)
TABLE SEARCH ORDER
Normally, a lookup table is specified as a text file that serves as input to the postmap(1) command. The result, an indexed file in dbm or db format, is used for fast searching by the mail system.
The search order is as follows. The search stops upon the first successful lookup.
When the recipient has an optional address extension the
user+extension@domain.tld
address is looked up first.With Postfix versions before 2.1, the optional address extension is always ignored.
The
user@domain.tld
address, without address extension, is looked up next.Finally, the recipient
@domain
is looked up.When the table is provided via other means such as NIS, LDAP or SQL, the same lookups are done as for ordinary indexed files.
Now does the current postfix_transport_map
function return the domain transport if the local part doesn't match a user record. So the lookup for the extension already returns the domain default, but what we want is the user specific value.
Removing the the fallback from the function in order to rely on the lookup order described above doesn't work either, since (from pgsql_table(5))
%u When the input key is an address of the form user@domain, %u is replaced by the SQL quoted local part of the address. Otherwise, %u is replaced by the entire search string. If the localpart is empty, the query is suppressed and returns no results.
%d When the input key is an address of the form user@domain, %d is replaced by the SQL quoted domain part of the address. Otherwise, the query is suppressed and returns no results.
Which means the input @domain
will never execute a query, domain defaults no longer work and fallback to the virtual_transport setting.
Another approach could be parsing the local part inside postfix_transport_map
, though since recipient_delimiter is configurable that is hardly feasible.
The next approach would be passing %s
to postfix_transport_map
instead of %u
and %d
. and do the separation to local part and domain inside it. The gotcha here is that the docs are a bit misleading, example.org
is passed as key, not @example.org
. It's what I'm currently using, here's the modified function:
CREATE OR REPLACE FUNCTION public.postfix_transport_map(recipient character varying)
RETURNS SETOF recipient_transport
LANGUAGE plpgsql
STABLE STRICT
AS $function$
DECLARE
record recipient_transport;
localpart varchar(320) := NULLIF(split_part(recipient, '@', 1), recipient);
the_domain varchar(320) := COALESCE(NULLIF(split_part(recipient, '@', 2), ''), recipient);
did bigint := (SELECT gid FROM domain_name WHERE domainname = the_domain);
transport_id bigint;
BEGIN
IF did IS NULL THEN
RETURN;
END IF;
IF localpart IS NULL THEN
SELECT tid INTO STRICT transport_id
FROM domain_data
WHERE gid = did;
ELSE
SELECT tid INTO transport_id
FROM users
WHERE gid = did AND local_part = localpart;
END IF;
FOR record IN
SELECT recipient, transport
FROM transport
WHERE tid = transport_id
LOOP
RETURN NEXT record;
END LOOP;
RETURN;
END;
$function$;
The last approach I see would be passing multiple maps to transport_maps, the first table just looking up user specific transports with %u
and %d
and the second table just looking up domain specific transports with %s
.