Warning: Lua support in Inbucket is still in a preview state, please expect the API and features to evolve before a full release.
By default Inbucket will load inbucket.lua, but you may use the INBUCKET_LUA_PATH environment variable to change that.
Logging
Inbucket allows Lua scripts to write log entries via the built-in logger package -- API provided by loguago.
Each logging call must include a level. Only log entries greater than or equal to global INBUCKET_LOGLEVEL environment variable will be output. In order of least to most severe, the available log levels are: debug, info, warn, and error. Inbucket will output the level info and higher by default.
Usage: logger.<level>(message, fields)
The first parameter is the log message; you may use Lua's string.format to interpolate values into the message, if needed. The second parameter is a table of fields that will be included in the log entry JSON data. While you are not required to add fields, the current API requires at least an empty table parameter.
local logger = require("logger")
-- The following functions all have the same signature but different names to
-- allow for log leveling.
logger.debug("message at debug level", {})
logger.info("message at info level", {})
logger.warn("message at warn level", {})
logger.error("message at error level", {})
-- Example with formatting and fields.
local orig_addr = "input@example.com"
local new_addr = "output@example.com"
logger.info(string.format("Mapping address to %q", new_addr), {address = orig_addr})
Console log output for the example above:
$ env INBUCKET_LOGLEVEL=debug ./inbucket
1:49PM INF Inbucket starting buildDate=undefined phase=startup version=undefined
1:49PM INF Loading script module=lua path=inbucket.lua phase=startup
1:49PM DBG message at debug level module=lua
1:49PM INF message at info level module=lua
1:49PM WRN message at warn level module=lua
1:49PM ERR message at error level module=lua
1:49PM INF Mapping address to "output@example.com" address=input@example.com module=lua
Example JSON output:
$ env INBUCKET_LOGLEVEL=debug ./inbucket -logjson
{"level":"info","phase":"startup","version":"undefined","buildDate":"undefined","time":"2023-11-13T13:54:01-08:00","message":"Inbucket starting"}
{"level":"info","module":"lua","phase":"startup","path":"inbucket.lua","time":"2023-11-13T13:54:01-08:00","message":"Loading script"}
{"level":"debug","module":"lua","time":"2023-11-13T13:54:01-08:00","message":"message at debug level"}
{"level":"info","module":"lua","time":"2023-11-13T13:54:01-08:00","message":"message at info level"}
{"level":"warn","module":"lua","time":"2023-11-13T13:54:01-08:00","message":"message at warn level"}
{"level":"error","module":"lua","time":"2023-11-13T13:54:01-08:00","message":"message at error level"}
{"level":"info","module":"lua","address":"input@example.com","time":"2023-11-13T13:54:01-08:00","message":"Mapping address to \"output@example.com\""}
Event trigger: before mail accepted
This event fires when Inbucket is evaluating an SMTP "MAIL FROM" command.
Denies mail that is not from james*@example.com:
function inbucket.before.mail_accepted(from_localpart, from_domain)
print(string.format("\n### inspecting from %s@%s", from_localpart, from_domain))
if from_domain ~= "example.com" then
-- Only allow example.com mail
return false
end
if string.find(from_localpart, "james") ~= 1 then
-- Only allow mailboxes starting with 'james'
return false
end
return true
end
Event trigger: after message deleted
Prints some info when a message is deleted:
function inbucket.after.message_deleted(msg)
print(string.format("\n### deleted ID %s (subj %q) from mailbox %s", msg.id, msg.subject, msg.mailbox))
end
Event trigger: before message stored
This event fires after Inbucket has accepted a message, but before it has been stored.
Changes the destination mailbox to test from swaks, and does not store mail for
alternate.
local logger = require("logger")
-- Original mailbox name on left, new on right.
-- `false` causes mail for that box to be discarded.
local mailbox_mapping = {
["swaks"] = "test",
["alternate"] = false,
}
function inbucket.before.message_stored(msg)
local made_changes = false
local new_mailboxes = {}
-- Loop over original recipient mailboxes for this message, building up list
-- of new_mailboxes.
for index, orig_box in ipairs(msg.mailboxes) do
local new_box = mailbox_mapping[orig_box]
if new_box then
logger.info(string.format("Mapping mailbox %q to %q", orig_box, new_box), {})
new_mailboxes[#new_mailboxes+1] = new_box
made_changes = true
elseif new_box == false then
logger.info(string.format("Discarding mail for %q", orig_box), {})
made_changes = true
else
-- No match, continue using the original value for this mailbox.
new_mailboxes[#new_mailboxes+1] = orig_box
end
end
if made_changes then
-- Recipient mailbox list was changed, return updated msg.
logger.info(
string.format("New mailboxes: %s", table.concat(new_mailboxes, ", ")),
{count = #new_mailboxes})
msg.mailboxes = new_mailboxes
return msg
end
-- No changes, return nil to signal inbucket to use original msg.
return nil
end
Event trigger: after message stored
Prints metadata of stored messages to STDOUT:
function inbucket.after.message_stored(msg)
print("\n## message_stored ##")
print(string.format("mailbox: %s", msg.mailbox))
print(string.format("id: %s", msg.id))
print(string.format("from: %q <%s>",
msg.from.name, msg.from.address))
for i, to in ipairs(msg.to) do
print(string.format("to[%s]: %q <%s>", i, to.name, to.address))
end
print(string.format("date: %s", os.date("%c", msg.date)))
print(string.format("subject: %s", msg.subject))
end
Makes a JSON encoded POST to a web service:
local http = require("http")
local json = require("json")
BASE_URL = "https://myapi.example.com"
function inbucket.after.message_stored(msg)
local request = json.encode {
subject = string.format("Mail from %q", msg.from.address),
body = msg.subject
}
assert(http.post(BASE_URL .. "/notify/text", {
headers = { ["Content-Type"] = "application/json" },
body = request,
}))
end
Writes data to temporary file and runs external shell command:
function inbucket.after.message_stored(msg)
local content = string.format("%q,%q", msg.from, msg.subject)
-- Write content to temporary file.
local fnam = os.tmpname()
local f = assert(io.open(fnam, "w+"))
assert(f:write(content))
f:close()
local cmd = string.format("cat %q", fnam)
print(string.format("\n### running %s ###", cmd))
local status = os.execute(cmd)
if status ~= 0 then
error("command failed: " .. cmd)
end
print("\n")
end