Skip to content
James Hillyerd edited this page Nov 13, 2023 · 13 revisions

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 = "[email protected]"
local new_addr = "[email protected]"

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 "[email protected]" [email protected] 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":"[email protected]","time":"2023-11-13T13:54:01-08:00","message":"Mapping address to \"[email protected]\""}

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