protocol
Configure the delivery protocol. The default is to use SMTP to the domain associated with the queue, but you can also configure delivering to a local maildir, or using custom lua code to process a message
Example of smart-hosting with the SMTP protocol
Since: Version 2023.08.22-4d895015
The functionality described in this section requires version 2023.08.22-4d895015 of KumoMTA, or a more recent version.
Rather than relying on MX resolution, you can provide an explicit list of MX host names or IP addresses to which the queue should deliver. The addresses will be tried in the order specified.
kumo.on('get_queue_config', function(domain, tenant, campaign, routing_domain)
if domain == 'smarthost.example.com' then
-- Relay via some other internal infrastructure.
-- Enclose IP (or IPv6) addresses in `[]`.
-- Otherwise the name will be resolved for A and AAAA records
return kumo.make_queue_config {
protocol = {
smtp = {
mx_list = {
'smart.host.local',
{ name = 'mx.example.com', addr = '10.0.0.1' },
},
},
},
}
end
-- Otherwise, just use the defaults
return kumo.make_queue_config {}
end)
Since: Version 2025.10.06-5ec871ab
The functionality described in this outlined box requires version 2025.10.06-5ec871ab of KumoMTA, or a more recent version.
You may now use port numbers to override the outbound port number.
return kumo.make_queue_config {
protocol = {
smtp = {
mx_list = {
'smart.host.local:2025',
{ name = 'mx.example.com', addr = '10.0.0.1:2025' },
},
},
},
}
Note that a remote_port defined in the egress source will override a port
that you define here. A port number defined here in mx_list overrides a
port number defined by make_egress_path in your shaping configuration.
treat_mx_list_as_secure
Since: Dev Builds Only
The functionality described in this section requires a dev build of KumoMTA. You can obtain a dev build by following the instructions in the Installation section.
When enable_dane is set, DANE requires
that the selection of the destination host be trusted: normally this is
established by DNSSEC-validating the MX RRset. An mx_list bypasses MX
resolution, so by default kumomta does not consider its hosts to be a
secure selection, and DANE will not engage for them.
Set treat_mx_list_as_secure = true to assert that the hosts in mx_list
are a trusted selection (for example, a statically configured internal
relay). When set, DANE may apply to those hosts, provided their address
(A/AAAA) records are themselves DNSSEC-validated.
return kumo.make_queue_config {
protocol = {
smtp = {
mx_list = { 'relay.internal' },
treat_mx_list_as_secure = true,
},
},
}
Danger
Do not set this when mx_list is derived from an untracked DNS lookup.
For example, populating mx_list from
kumo.dns.lookup_mx and unconditionally setting
treat_mx_list_as_secure = true would let a spoofed MX response steer
delivery to an attacker-chosen host that passes DANE, defeating the
downgrade resistance that DANE is meant to provide. If you must build
mx_list from a lookup, propagate the lookup's own secure status
instead:
Example of using the Maildir protocol
kumo.on('get_queue_config', function(domain, tenant, campaign, routing_domain)
if domain == 'maildir.example.com' then
-- Store this domain into a maildir, rather than attempting
-- to deliver via SMTP
return kumo.make_queue_config {
protocol = {
maildir_path = '/var/tmp/kumo-maildir',
},
}
end
-- Otherwise, just use the defaults
return kumo.make_queue_config {}
end)
Note
Maildir support is present primarily for functional validation rather than being present as a first class delivery mechanism.
Failures to write to the maildir will cause the message to be delayed and retried approximately 1 minute later. The normal message retry schedule does not apply.
Specifying directory and file modes for maildir
Since: Version 2025.01.23-7273d2bc
The functionality described in this section requires version 2025.01.23-7273d2bc of KumoMTA, or a more recent version.
If you are sharing the maildir with something like dovecot it can sometimes
be desirable to explicitly control the file permissions of the directory
structure and files that are created. You can achieve this via the dir_mode
and file_mode parameters.
Note
Lua doesn't support native octal literal numbers, so you must use
tonumber as shown in the example below if you wish to specify
the modes in octal
kumo.on('get_queue_config', function(domain, tenant, campaign, routing_domain)
if domain == 'maildir.example.com' then
return kumo.make_queue_config {
protocol = {
maildir_path = '/var/tmp/kumo-maildir',
dir_mode = tonumber('775', 8),
file_mode = tonumber('664', 8),
},
}
end
end)
Advanced Maildir Path
Since: Version 2025.01.23-7273d2bc
The functionality described in this section requires version 2025.01.23-7273d2bc of KumoMTA, or a more recent version.
If you are sharing the maildir with something like dovecot it is desirable to be able to put messages into per-user maildirs. You can achieve this through templated paths.
The maildir_path field supports template expansion.
The following values are pre-defined in the context:
meta- the full set of metadata from the message.queue- the effective queue name of the message.campaign- the campaign associated with the message (may be nil).tenant- the tenant associated with the message (may be nil).domain- the domain portion of the queue name (usually the same thing asdomain_part, but may be different if you are using advanced queue name assignment).routing_domain- the routing domain portion of the queue (may be nil).local_part- the user mailbox portion of the envelope recipient email address. See address.user for the semantics of how this is produced.domain_part- the domain portion of the envelope recipient email address.email- the full envelope recipient email address.sender_local_partthe user mailbox portion of the envelope sender email address. See address.user for the semantics of how this is produced.sender_domain_partthe domain portion of the envelope sender email address.sender_emailthe full envelope sender email address.
In the example below, each recipient domain has its own directory created
(although in this example, we only do this for maildir.example.com), and each
individual user at that domain has their own maildir created.
"created" here means that kumomta will create it if it doesn't already exist, and deliver to it in either case.
kumo.on('get_queue_config', function(domain, tenant, campaign, routing_domain)
if domain == 'maildir.example.com' then
return kumo.make_queue_config {
protocol = {
maildir_path = '/maildirs/{{ domain_part }}/{{ local_part }}',
dir_mode = tonumber('775', 8),
file_mode = tonumber('664', 8),
},
}
end
end)
Using Lua as a delivery protocol
kumo.on('get_queue_config', function(domain, tenant, campaign, routing_domain)
if domain == 'webhook' then
-- Use the `make.webhook` event to handle delivery
-- of webhook log records
return kumo.make_queue_config {
protocol = {
custom_lua = {
-- this will cause an event called `make.webhook` to trigger.
-- You can pick any name for this event, so long as it doesn't
-- collide with a pre-defined event, and so long as you bind
-- to it with a kumo.on call
constructor = 'make.webhook',
},
},
}
end
return kumo.make_queue_config {}
end)
-- This event will be called each time we need to make a connection.
-- It needs to return a lua object with a `send` method
kumo.on('make.webhook', function(domain, tenant, campaign)
-- Create the connection object
local connection = {}
-- define a send method on the connection object.
-- The return value is the disposition string for a successful
-- delivery; that string will get logged in the resulting log record.
-- If the delivery failed, you can use `kumo.reject` to raise the
-- error with an appropriate 400 or 500 code.
-- 400 codes will be retried later. 500 codes will log a permanent
-- failure and no further delivery attempts will be made for the message.
function connection:send(message)
print(message:get_data())
if failed then
kumo.reject(400, 'failed for some reason')
end
return 'OK'
end
return connection
end)
See should_enqueue_log_record for a more complete example.
Where the constructor's (domain, tenant, campaign) come from
The constructor event (e.g. make.webhook above) is fired once per
connection session, before any message is delivered through it. The
(domain, tenant, campaign) arguments come from one of the scheduled
queues that feeds the dispatcher's ready queue, captured at session
start.
Multiple scheduled queues can share the same Lua ready queue.
Scheduled queues converge on the same ready queue when they all
resolve to the same egress source, routing domain, and Lua
constructor name. That can happen for many reasons — different
(domain, tenant, campaign) triples that map to a common
routing_domain, explicit routing via the domain!routing_domain
syntax, scheduled queue names that happen to share components, or
any get_queue_config implementation that yields the same
custom_lua.constructor value for distinct inputs.
When any of those produces fan-in, the constructor's arguments are
representative: they reflect one scheduled queue currently
feeding the session, but other messages delivered through the same
session may have originated from different scheduled queues with
different (tenant, campaign) values.
If the connection setup itself depends on per-message values, use
the constructor arguments only for one-time setup that is invariant
across the queues sharing this constructor, and resolve per-message
values inside send (or send_batch) using message:get_meta or
message:queue_name:
kumo.on('make.webhook', function(domain, tenant, campaign)
local connection = {}
function connection:send(message)
-- Source of truth for this specific message:
local msg_tenant = message:get_meta 'tenant'
local msg_campaign = message:get_meta 'campaign'
-- ... use msg_tenant / msg_campaign for per-message routing ...
return 'OK'
end
return connection
end)