When delivery fails for a given message, Kumo handles it by retrying with exponential backoff and eventually giving up and firing an Expiration log event and with bounces, we the bounce event is fired.
These are happening at message-level, but to won’t prevent sending more messages to the same email address from the same domain. I’m trying to implement this sending functionality in my own backend by looking at the log event and if there a Bounce/OOB/Expiration event, add the email to a suppression list for the sending domain (in a sqlite database). I need to check the (recipient, sending domain) combination with this list in Kumo and if they are listed, accept the message and send the log event (so that I can show it to the end user in their panel, saying that the email was suppressed because of previous errors), but don’t actually try to deliver the message.
My thinking is if the message should be suppressed, it should go into a separate queue, but I don’t have enough information in get_queue_config to do the actual check. The check probably needs to happen in smtp_server_message_received and http_message_generated where I have access to the message object.
Is this the best way of doing this? If so, how do I share the suppression flag from smtp_server_message_received / http_message_generated to get_queue_config?
the smtp_server_message_received/http_message_generated events are the best place to make policy decisions on a per-recipient basis. The get_queue_config hook is scoped to the overall queue; from the perspective of your use-case, that hook happens “too late” to be useful to you, as the message has already been placed into a queue.
Usually, suppression results in rejection of the message rather than queuing it. Either through kumo.reject with an appropriate reason, or by accepting and discarding the message using the null queue (note that kumomta will log a reception but not a Bounce in that case)
From a “protect the infra” mindset: you don’t want to clog up your queues with mail that won’t get sent, so anything you can do to push back to the injector and tell them about issues, the less burden there is on your MTA infra
this probably won’t work, but this is what I have in mind right now:
kumo.on('get_queue_config',
function(domain, tenant, campaign, routing_domain)
if campaign ~= "suppressed"
-- Is this the null queue? or should I return nil?
return kumo.make_queue_config {}
end
end
)
kumo.on('smtp_server_message_received', function(msg)
if cached_is_suppressed(msg:recipient(), msg:from_header().domain)
msg:set_meta("campaign", "suppressed")
end
queue_helper:apply(msg)
dkim_signer(msg)
end)
if you do decide that you want to generate a bounce log in that case, what you could do is set up a suppressed queue that has a lua delivery handler that simply does a kumo.reject with a 500 status code and something about suppression.
kumo.on('smtp_server_message_received', function(msg)
if cached_is_suppressed(msg:recipient(), msg:from_header().domain)
-- discard the mail by assigning it to the null queue
msg:set_meta("queue", "null")
end
end)
I probably don’t want to return a reject response to the user anyway, as that might cause put the message in a retry queue. I’ll just accept it, capture the reception log in my backend, but not queue it for delivery.
FWIW, I would advocate for kumo.reject’ing this with a permfail error; it’s the least fuss, highest signal, lowest resource, lowest effort way to handle this. If the injector doesn’t respect the permfail response and retries anyway, it sounds very bad and should not be something you accept from your users.
kumo.on('make.suppression', function(domain, tenant, campaign)
local connection = {}
function connection:send(message)
return kumo.reject(
550,
"recipient is suppressed (too many errors)"
)
end
return connection
end)