Introduction

This is the documentation for Inbucket. It is a work in progress; please report mistakes or missing information via GitHub issues.

Intended Usage

Inbucket can be used in many different scenarios. It was designed to be used during software development and testing, and is not intended for storing mail of any significance.

Local Development

graph TD;
    subgraph Workstation
    app1["App 1"]-->Inbucket
    app2["App 2"]-->Inbucket
    end

Inbucket can be run without installing, after extracting the release for your platform, start in a terminal inside the directory containing the extracted files and run ./inbucket or inbucket.exe. This will launch an server listening for SMTP messages on port 2500, and the web interface will be served on port 9000.

Docker Compose

Companion Mail Server

Installation

Version Selection

In general, Inbucket pre-release (beta and release-candidate) versions are very stable. If you can accept some risk, it is recommended to install a recent pre-release over a very old stable release.

Release notes can be found on GitHub: https://github.com/inbucket/inbucket/releases

The Inbucket news page often summarizes major changes and risks for a particular release.

Docker Build

Inbucket images are automatically built and pushed to Docker Hub and the GitHub Container Registry. The main Inbucket repo contains the Dockerfile, which allows you to build your own Docker image.

Running

docker run -d --name inbucket -p 9000:9000 -p 2500:2500 -p 1100:1100 inbucket/inbucket

Then point your browser at http://localhost:9000/.

Image Tags

  • latest tracks the most recent stable release
  • edge tracks the potentially unstable main branch
  • versions are also tagged, ie 3.1.0-beta2

Volumes

  • /config will contain the default greeting.html file (after first start,) which may be customized for your users
  • /storage holds emails received by Inbucket

Default Configuration

The initial configuration is set by the ENV statements in the Configuration section of our Dockerfile. You may use Docker environment variables to override or further customize Inbucket.

Linux Package Installation

Obtain Package File

Inbucket releases are published on GitHub: https://github.com/inbucket/inbucket/releases

For a Debian or Ubuntu based distribution, download the appropriate .deb file for your CPU architecture.

If you are running a RedHat or Fedora based distribution, download the appropriate .rpm file for your CPU architecture.

Install Package

The commands below should be run as the root user.

Debian

dpkg -i inbucket_VER_linux_amd64.deb

RedHat

rpm -ivh inbucket_VER_linux_amd64.rpm

Enable & Start Inbucket

As root, start the daemon and check for errors:

# Set the daemon to start at boot
systemctl enable inbucket

# Manually start the daemon
systemctl start inbucket

# Confirm Inbucket stayed running
systemctl status inbucket

# Check system logs for startup errors
journalctl -u inbucket

Access Web Interface

Confirm that Inbucket is listening on the default port: http://localhost:9000/.

Customize Inbucket Configuration

Inbucket is configured via environment variables, please see config.md for more information; or try our Configurator web app. You'll need to modify the Environment= statements in /etc/systemd/system/multi-user.target.wants/inbucket.service for the configuration changes to become permanent.

If something goes wrong, you may refer to the default systemd unit on GitHub.

Inbucket Configuration

Inbucket is configured via environment variables. Most options have a reasonable default, but it is likely you will need to change some to suite your desired use cases.

Running inbucket -help will yield a condensed summary of the environment variables it supports:

KEY                                 DEFAULT             DESCRIPTION
INBUCKET_LOGLEVEL                   info                debug, info, warn, or error
INBUCKET_LUA_PATH                   inbucket.lua        Lua script path
INBUCKET_MAILBOXNAMING              local               Use local, full, or domain addressing
INBUCKET_SMTP_ADDR                  0.0.0.0:2500        SMTP server IP4 host:port
INBUCKET_SMTP_DOMAIN                inbucket            HELO domain
INBUCKET_SMTP_MAXRECIPIENTS         200                 Maximum RCPT TO per message
INBUCKET_SMTP_MAXMESSAGEBYTES       10240000            Maximum message size
INBUCKET_SMTP_DEFAULTACCEPT         true                Accept all mail by default?
INBUCKET_SMTP_ACCEPTDOMAINS                             Domains to accept mail for
INBUCKET_SMTP_REJECTDOMAINS                             Domains to reject mail for
INBUCKET_SMTP_REJECTORIGINDOMAINS                       Domains to reject mail from
INBUCKET_SMTP_DEFAULTSTORE          true                Store all mail by default?
INBUCKET_SMTP_STOREDOMAINS                              Domains to store mail for
INBUCKET_SMTP_DISCARDDOMAINS                            Domains to discard mail for
INBUCKET_SMTP_TIMEOUT               300s                Idle network timeout
INBUCKET_SMTP_TLSENABLED            false               Enable STARTTLS option
INBUCKET_SMTP_TLSPRIVKEY            cert.key            X509 Private Key file for TLS Support
INBUCKET_SMTP_TLSCERT               cert.crt            X509 Public Certificate file for TLS Support
INBUCKET_POP3_ADDR                  0.0.0.0:1100        POP3 server IP4 host:port
INBUCKET_POP3_DOMAIN                inbucket            HELLO domain
INBUCKET_POP3_TIMEOUT               600s                Idle network timeout
INBUCKET_WEB_ADDR                   0.0.0.0:9000        Web server IP4 host:port
INBUCKET_WEB_BASEPATH                                   Base path prefix for UI and API URLs
INBUCKET_WEB_UIDIR                  ui/dist             User interface dir
INBUCKET_WEB_GREETINGFILE           ui/greeting.html    Home page greeting HTML
INBUCKET_WEB_MONITORVISIBLE         true                Show monitor tab in UI?
INBUCKET_WEB_MONITORHISTORY         30                  Monitor remembered messages
INBUCKET_WEB_PPROF                  false               Expose profiling tools on /debug/pprof
INBUCKET_STORAGE_TYPE               memory              Storage impl: file or memory
INBUCKET_STORAGE_PARAMS                                 Storage impl parameters, see docs.
INBUCKET_STORAGE_RETENTIONPERIOD    24h                 Duration to retain messages
INBUCKET_STORAGE_RETENTIONSLEEP     50ms                Duration to sleep between mailboxes
INBUCKET_STORAGE_MAILBOXMSGCAP      500                 Maximum messages per mailbox

The following sections will describe each of these in more detail.

Global Configuration

These options affect multiple parts of Inbucket.

Log Level

  • Name: INBUCKET_LOGLEVEL
  • Default: info
  • Values: one of debug, info, warn, or error

This setting controls the verbosity of log output. A small desktop installation should probably select info, but a busy shared installation would be better off with warn or error.

Lua Script

  • Name: INBUCKET_LUA_PATH
  • Default: inbucket.lua
  • Values: relative or fully qualified path to a file

This is the path to the (optional) Inbucket Lua script. If the specified file is present, Inbucket will load it during startup. Ignored if the file is not found, or the setting is empty.

Mailbox Naming

  • Name: INBUCKET_MAILBOXNAMING
  • Default: local
  • Values: one of local or full or domain

The mailbox naming setting determines the name of a mailbox for an incoming message, and thus where it must be retrieved from later.

Prior to the addition of the mailbox naming setting, Inbucket always operated in local mode. Regardless of this setting, the + wildcard/extension is not incorporated into the mailbox name.

Naming Modes

local ensures the domain is removed, such that:

  • james@inbucket.org is stored in james
  • james+spam@inbucket.org is stored in james

full retains the domain as part of the name, such that:

  • james@inbucket.org is stored in james@inbucket.org
  • james+spam@inbucket.org is stored in james@inbucket.org

domain ensures the local-part is removed, such that:

  • james@inbucket.org is stored in inbucket.org
  • matt@inbucket.org is stored in inbucket.org
  • matt@notinbucket.com is stored in notinbucket.com

SMTP Service Configuration

Address and Port

  • Name: INBUCKET_SMTP_ADDR
  • Default: 0.0.0.0:2500

The IPv4 address and TCP port number the SMTP server should listen on, separated by a colon. Some operating systems may prevent Inbucket from listening on port 25 without escalated privileges. Using an IP address of 0.0.0.0 will cause Inbucket to listen on all available network interfaces.

Greeting Domain

  • Name: INBUCKET_SMTP_DOMAIN
  • Default: inbucket

The domain used in the SMTP greeting:

220 domain Inbucket SMTP ready

Most SMTP clients appear to ignore this value.

Maximum Recipients

  • Name: INBUCKET_SMTP_MAXRECIPIENTS
  • Default: 200

Maximum number of recipients allowed (SMTP RCPT TO phase). If you are testing a mailing list server, you may need to increase this value. For comparison, the Postfix SMTP server uses a default of 1000, it would be unwise to exceed this.

Maximum Message Size

  • Name: INBUCKET_SMTP_MAXMESSAGEBYTES
  • Default: 10240000 (10MB)

Maximum allowable size of a message (including headers) in bytes. Messages exceeding this size will be rejected during the SMTP DATA phase.

Default Recipient Accept Policy

  • Name: INBUCKET_SMTP_DEFAULTACCEPT
  • Default: true
  • Values: true or false

If true, Inbucket will accept mail to any domain unless present in the reject domains list. If false, recipients will be rejected unless their domain is present in the accept domains list.

Accepted Recipient Domain List

  • Name: INBUCKET_SMTP_ACCEPTDOMAINS
  • Default: None
  • Values: Comma separated list of recipient domains
  • Example: localhost,mysite.org

List of domains to accept mail for when INBUCKET_SMTP_DEFAULTACCEPT is false; has no effect when true.

Rejected Recipient Domain List

  • Name: INBUCKET_SMTP_REJECTDOMAINS
  • Default: None
  • Values: Comma separated list of recipient domains
  • Example: reject.com,gmail.com

List of domains to reject mail for when INBUCKET_SMTP_DEFAULTACCEPT is true; has no effect when false.

Rejected Origin Domain List

  • Name: INBUCKET_SMTP_REJECTORIGINDOMAINS
  • Default: None
  • Values: Comma separated list of origin domains
  • Example: reject.com,gmail.com

List of domains to reject mail from. This list is enforced regardless of the INBUCKET_SMTP_DEFAULTACCEPT value.

Enforcement takes place during evalation of the MAIL FROM SMTP command, the origin domain is extracted from the address presented and compared against the list. It does not take email headers into account.

Default Recipient Store Policy

  • Name: INBUCKET_SMTP_DEFAULTSTORE
  • Default: true
  • Values: true or false

If true, Inbucket will store mail sent to any domain unless present in the discard domains list. If false, messages will be discarded unless their domain is present in the store domains list.

Stored Recipient Domain List

  • Name: INBUCKET_SMTP_STOREDOMAINS
  • Default: None
  • Values: Comma separated list of recipient domains
  • Example: localhost,mysite.org

List of domains to store mail for when INBUCKET_SMTP_DEFAULTSTORE is false; has no effect when true.

Discarded Recipient Domain List

  • Name: INBUCKET_SMTP_DISCARDDOMAINS
  • Default: None
  • Values: Comma separated list of recipient domains
  • Example: recycle.com,loadtest.org

Mail sent to these domains will not be stored by Inbucket. This is helpful if you are load or soak testing a service, and do not plan to inspect the resulting emails. Messages sent to a domain other than this will be stored normally. Only has an effect when INBUCKET_SMTP_DEFAULTSTORE is true.

Network Idle Timeout

  • Name: INBUCKET_SMTP_TIMEOUT
  • Default: 300s
  • Values: Duration ending in s for seconds, m for minutes

Delay before closing an idle SMTP connection. The SMTP RFC recommends 300 seconds. Consider reducing this significantly if you plan to expose Inbucket to the public internet.

TLS Support Availability

  • Name: INBUCKET_SMTP_TLSENABLED
  • Default: false
  • Values: true or false

Enable the STARTTLS option for opportunistic TLS support

TLS Private Key File

  • Name: INBUCKET_SMTP_TLSPRIVKEY
  • Default: cert.key
  • Values: filename or path to private key
  • Example: server.privkey

Specify the x509 Private key file to be used for TLS negotiation. This option is only valid when INBUCKET_SMTP_TLSENABLED is enabled.

TLS Public Certificate File

  • Name: INBUCKET_SMTP_TLSCERT
  • Default: cert.crt
  • Values: filename or path to the certificate key
  • Example: server.crt

Specify the x509 Certificate file to be used for TLS negotiation. This option is only valid when INBUCKET_SMTP_TLSENABLED is enabled.

POP3 Service Configuration

Address and Port

  • Name: INBUCKET_POP3_ADDR
  • Default: 0.0.0.0:1100

The IPv4 address and TCP port number the POP3 server should listen on, separated by a colon. Some operating systems may prevent Inbucket from listening on port 110 without escalated privileges. Using an IP address of 0.0.0.0 will cause Inbucket to listen on all available network interfaces.

Greeting Domain

  • Name: INBUCKET_POP3_DOMAIN
  • Default: inbucket

The domain used in the POP3 greeting:

+OK Inbucket POP3 server ready <26641.1522000423@domain>

Most POP3 clients appear to ignore this value.

Network Idle Timeout

  • Name: INBUCKET_POP3_TIMEOUT
  • Default: 600s
  • Values: Duration ending in s for seconds, m for minutes

Delay before closing an idle POP3 connection. The POP3 RFC recommends 600 seconds. Consider reducing this significantly if you plan to expose Inbucket to the public internet.

Web Interface Configuration

Address and Port

  • Name: INBUCKET_WEB_ADDR
  • Default: 0.0.0.0:9000

The IPv4 address and TCP port number the HTTP server should listen on, separated by a colon. Some operating systems may prevent Inbucket from listening on port 80 without escalated privileges. Using an IP address of 0.0.0.0 will cause Inbucket to listen on all available network interfaces.

Base Path

  • Name: INBUCKET_WEB_BASEPATH
  • Default: None

Base path prefix for UI and API URLs. This option is used when you wish to root all Inbucket URLs to a specific path when placing it behind a reverse-proxy.

For example, setting the base path to prefix will move:

  • the Inbucket status page from /status to /prefix/status,
  • Bob's mailbox from /m/bob to /prefix/m/bob, and
  • the REST API from /api/v1/* to /prefix/api/v1/*.

This setting will not work correctly when accessing Inbucket via the parcel development server.

UI Directory

  • Name: INBUCKET_WEB_UIDIR
  • Default: ui/dist
  • Values: Operating system specific path syntax

This directory contains the templates and static assets for the web user interface. You will need to change this if the current working directory doesn't contain the ui directory at startup.

Inbucket will load templates from the templates sub-directory, and serve static assets from the static sub-directory.

Greeting HTML File

  • Name: INBUCKET_WEB_GREETINGFILE
  • Default: ui/greeting.html

The content of the greeting file will be injected into the front page of Inbucket. It can be used to instruct users on how to send mail into your Inbucket installation, as well as link to REST documentation, etc.

Monitor Visible

  • Name: INBUCKET_WEB_MONITORVISIBLE
  • Default: true
  • Values: true or false

If true, the Monitor tab will be available, allowing users to observe all messages received by Inbucket as they arrive. Disabling the monitor facilitates security through obscurity.

This setting has no impact on the availability of the underlying WebSocket, which may be used by other parts of the Inbucket interface or continuous integration tests.

Monitor History

  • Name: INBUCKET_WEB_MONITORHISTORY
  • Default: 30
  • Values: Integer greater than or equal to 0

The number of messages to remember on the server for new Monitor clients. Does not impact the amount of new messages displayed by the Monitor. Increasing this has no appreciable impact on memory use, but may slow down the Monitor user interface.

This setting has the same effect on the amount of messages available via WebSocket.

Setting to 0 will disable the monitor, but will probably break new mail notifications in the web interface when I finally get around to implementing them.

Performance Profiling & Debug Tools

  • Name: INBUCKET_WEB_PPROF
  • Default: false
  • Values: true or false

When true, Go's pprof package will be installed to the /debug/pprof URI. This exposes detailed memory and CPU performance data for debugging Inbucket internals.

If this option is enabled, please make sure the URI is not exposed to the public internet, as its use can significantly impact performance.

For more information and example usage, please see the pprof pkg docs.

Mail Storage Configuration

Storage Type

  • Name: INBUCKET_STORAGE_TYPE
  • Default: memory
  • Values: file or memory

Selects the storage implementation to use. Currently Inbucket supports two:

  • file: stores messages as individual files in a nested directory structure based on the hash of the mailbox name. Each mailbox also includes an index file to speed up enumeration of the mailbox contents.
  • memory: stores messages in RAM, they will be lost if Inbucket is restarted, or crashes, etc.

File storage is recommended for larger/shared installations. Memory is better suited to desktop or continuous integration test use cases.

Storage Parameters

  • Name: INBUCKET_STORAGE_PARAMS
  • Default: None
  • Examples: maxkb:10240 or path:/tmp/inbucket

Parameters specific to the storage type selected. Formatted as a comma separated list of key:value pairs.

file type parameters

  • path: Operating system specific path to the directory where mail should be stored. $ characters will be replaced with : in the final path value, allowing Windows drive letters, i.e. D$\inbucket.

memory type parameters

  • maxkb: Maximum size of the mail store in kilobytes. The oldest messages in the store will be deleted to enforce the limit. In-memory storage has some overhead, for now it is recommended to set this to half the total amount of memory you are willing to allocate to Inbucket.

Retention Period

  • Name: INBUCKET_STORAGE_RETENTIONPERIOD
  • Default: 24h
  • Values: Duration ending in m for minutes, h for hours, or 0 to disable

If set, Inbucket will scan the contents of its mail store once per minute, removing messages older than this. This will be enforced regardless of the type of storage configured. In most cases, the configured value should be significantly longer than one minute.

Retention Sleep

  • Name: INBUCKET_STORAGE_RETENTIONSLEEP
  • Default: 50ms
  • Values: Duration ending in ms for milliseconds, s for seconds

Duration to sleep between scanning each mailbox for expired messages. Increasing this number will reduce disk thrashing, but extend the length of time required to complete a scan of the entire mail store.

This delay is still enforced for memory stores, but could be reduced from the default. Setting to 0 may degrade performance of HTTP/SMTP/POP3 services.

Per Mailbox Message Cap

  • Name: INBUCKET_STORAGE_MAILBOXMSGCAP
  • Default: 500
  • Values: Positive integer, or 0 to disable

Maximum messages allowed in a single mailbox, exceeding this will cause older messages to be deleted from the mailbox.

Extending Inbucket

Inbucket features a REST API that may be used to read and erase the contents of mailboxes. Outgoing webhook functionality is not included, but can be implemented with Lua.

Lua scripting is the primary way to extend Inbucket's functionality. Event handling functions may be used to react to incoming messages, or alter the way Inbucket processes them.

It is also possible to extend Inbucket in a similar fashion in Go, but due to the lack of a plugin loader, it will requiring building Inbucket from source.

The following chapters cover each of these extension mechanisms.

REST API

Allows clients to retrieve, and delete the contents of mailboxes and messages.

Clients

API v1

List Mailbox Contents

Retrive the list of messages and their metadata for the specified mailbox.

Specification

URI

GET /api/v1/mailbox/{name}

Parameters

  • name - name of the mailbox to list the contents of

Output

A JSON array of message header maps, containing the following fields:

  • mailbox - name of the mailbox the message belongs to
  • id - message identifier, will be unique for this mailbox, but not across mailboxes
  • from - message sender
  • subject - message subject line
  • date - date message was received (not the date specified by SMTP headers) in ISO 8601 format.
  • size - size of headers + message in bytes

Example

Request: curl -i -H "Accept: application/json" http://localhost:9000/api/v1/mailbox/swaks

Response: (JSON reformatted for readability)

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Expires: -1
Content-Length: 686
Date: Tue, 15 Oct 2013 23:54:36 GMT
[
  {
      "mailbox":"swaks",
      "id":"20131015T161202-0000",
      "from":"jamehi03@server.com",
      "subject":"Swaks Plain Text",
      "date":"2013-10-15T16:12:02.231532239-07:00",
      "size":264
  },
  {
      "mailbox":"swaks",
      "id":"20131015T161202-0001",
      "from":"jamehi03@server.com",
      "subject":"Swaks HTML",
      "date":"2013-10-15T16:12:02.292145012-07:00",
      "size":705
  },
  {
      "mailbox":"swaks",
      "id":"20131015T161202-0002",
      "from":"jamehi03@server.com",
      "subject":"Swaks Attachment",
      "date":"2013-10-15T16:12:02.358027334-07:00",
      "size":4802
  },
  {
      "mailbox":"swaks",
      "id":"20131015T161202-0003",
      "from":"jamehi03@server.com",
      "subject":"Test of ȇɲʢȯȡɪɴʛ",
      "date":"2013-10-15T16:12:02.424206527-07:00",
      "size":2946
  }
]

Get Message

Retrieve a specific message, with the headers and MIME data parsed into JSON.

Specification

URI

GET /api/v1/mailbox/{name}/{id}

Parameters

  • name - name of the mailbox the message belongs to
  • id - identifier of the message to retrieve
    • Using an id of latest will retrieve the most recent message in the mailbox

Output

  • The Inbucket version of the message headers (same as returned by List Mailbox Contents)
  • The parsed SMTP headers (not converted to UTF-8)
  • The decoded Text and HTML portions of the MIME body of the email
  • A list of attachments

Example

Request: curl -i -H "Accept: application/json" http://localhost:9000/api/v1/mailbox/swaks/20131016T164638-0001

Response: (JSON reformatted for readability)

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Expires: -1
Content-Length: 810
Date: Thu, 17 Oct 2013 21:51:59 GMT
{
    "mailbox": "swaks",
    "id": "20131016T164638-0001",
    "from": "jamehi03@server.com",
    "subject": "Swaks HTML",
    "date": "2013-10-16T16:46:38.646370568-07:00",
    "size": 705,
    "body": {
        "text": "This is a test mailing.\r\n\r\nThis should be clickable: http://google.com/\r\n",
        "html": "<html>\n<body>\n<p>This is a test mailing <b>in HTML</b></p>\n\n<p>This should be clickable: [...]"
    },
    "header": {
        "Content-Type": [
            "multipart/alternative; boundary=\"----=_MIME_BOUNDARY_000_62717\""
        ],
        "Date": [
            "Wed, 16 Oct 2013 16:46:38 -0700"
        ],
        "From": [
            "jamehi03@server.com"
        ],
        "Mime-Version": [
            "1.0"
        ],
        "Subject": [
            "Swaks HTML"
        ],
        "To": [
            "swaks@inbucket.local"
        ]
    },
    "attachments": [
        {
            "filename": "favicon.png",
            "content-type": "image/png",
            "download-link": "http://localhost:9000/mailbox/dattach/swaks/20131016T164638-0001/0/favicon.png",
            "view-link": "http://localhost:9000/mailbox/vattach/swaks/20131016T164638-0001/0/favicon.png",
            "md5": "a72a7565b6b6587ac15fc35746307d0e"
        }
    ]
}

Get Message Source

Retrieve the unprocessed source of a specific message.

Specification

URI

GET /api/v1/mailbox/{name}/{id}/source

Parameters

  • name - name of the mailbox the message belongs to
  • id - identifier of the message to retrieve

Output

Plain text dump of the message headers and body in SMTP format.

Example

Request: curl -i -H "Accept: application/json" http://localhost:9000/api/v1/mailbox/swaks/20131015T161202-0000/source

Response:

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 264
Date: Tue, 15 Oct 2013 23:52:09 GMT

Date: Tue, 15 Oct 2013 16:12:02 -0700
To: swaks@inbucket.local
From: jamehi03@server.com
Subject: Swaks Plain Text
X-Mailer: swaks v20130209.0 jetmore.org/john/code/swaks/

This is a test mailing.

This should be clickable: http://google.com/

Delete Message

Delete the specified message from a mailbox.

Specification

URI

DELETE /api/v1/mailbox/{name}/{id}

Parameters

  • name - name of the mailbox the message belongs to
  • id - identifier of the message to delete

Output

JSON encoded string "OK"

Example

Request: curl -i -H "Accept: application/json" -X DELETE http://localhost:9000/api/v1/mailbox/swaks/20131015T161202-0000

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Expires: -1
Content-Length: 5
Date: Tue, 15 Oct 2013 23:58:13 GMT

"OK"

Purge Mailbox

Delete all messages from a mailbox.

Specification

URI

DELETE /api/v1/mailbox/{name}

Parameters

  • name - name of the mailbox to purge

Output

JSON encoded string "OK"

Example

Request: curl -i -H "Accept: application/json" -X DELETE http://localhost:9000/api/v1/mailbox/swaks

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Expires: -1
Content-Length: 5
Date: Tue, 15 Oct 2013 23:58:13 GMT

"OK"

Lua API

Lua scripting is the primary way to extend Inbucket's server-side functionality. Lua event handling functions can react to incoming messages, or alter the way Inbucket processes them.

Lua scripting support is new for Inbucket version 3.1.

We will work to avoid breaking changes, but given the API is incomplete, that may not be possible.

At startup, Inbucket will attempt to read the file inbucket.lua from the current directory. The INBUCKET_LUA_PATH environment variable may be used to override both the path and name of the script file.

Once the Lua script has been loaded, Inbucket will look for event handling functions following this naming pattern: inbucket.(before|after).<event name>.

Your First Event Handler

To play along, download and unpack an Inbucket distribution, then change into it's directory. You should see the inbucket (or inbucket.exe) program, as well as the ui directory.

Create a file named inbucket.lua, which will contain a a minimal event handler that prints the address argument passed to MAIL FROM during an SMTP session to STDOUT:

-- A simple "before MAIL FROM accepted" event handler.
function inbucket.before.mail_from_accepted(from_localpart, from_domain)
  print(">>> LUA got MAIL FROM: ", from_localpart, from_domain)
end

Testing

Next, start Inbucket by running ./inbucket or .\inbucket.exe in your terminal. Inbucket should tell you it's loading inbucket.lua:

10:44AM INF Inbucket starting buildDate=undefined phase=startup version=undefined
10:44AM INF Loading script module=lua path=inbucket.lua phase=startup

In another terminal window, use nc (netcat), telnet, or similar, to connect to localhost port 2500:

  • Linux or MacOS
    • nc localhost 2500, or
    • telnet localhost 2500
  • Windows
    • telnet localhsot 2500 (you may need to install telnet)

Then send the following SMTP commands:

HELO localhost
MAIL FROM:<user@example.com>
QUIT

In your first terminal, you should see output from your Lua script mixed in with Inbucket's normal logging:

10:45AM INF Starting SMTP session module=smtp remote=127.0.0.1:46916 session=1
>>> LUA got MAIL FROM:  user    example.com
10:45AM INF Mail from: user@example.com module=smtp remote=127.0.0.1:46916 session=1

Next Steps

  • The sample above used Lua's print function, which does not integrate well with Inbucket's formatted log output. See the Logging section to learn how to output leveled and structured logs.

  • Review the Events section to learn more about the types of Inbucket events your can handle with Lua.

Logging from Lua

Inbucket allows Lua scripts to write log entries via the built-in logger package, with an 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 above 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.

Example

-- Loads the logger module and makes it available via the `logger` variable.
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(
  -- Formats a string for the text of the log entry.
  string.format("Changing address to %q", new_addr),
  -- A Lua table is used for the structured portion of the entry.
  {address = orig_addr})

Console log output for the example above:

$ env INBUCKET_LOGLEVEL=debug ./inbucket
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 using JSON log output:

$ env INBUCKET_LOGLEVEL=debug ./inbucket -logjson
{"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\""}

Lua Events

Inbucket events can be divided into two major categories, before events and after events.

Before Events

Before events allow extensions to intercept events before Inbucket acts on them. This gives an event handler the chance to alter the processing of, or reject the event entirely.

For example, a before event handler could:

  • Immediately reject a MAIL FROM command when the from address matches a particular pattern, returning an error code to the sending SMTP client.
  • Detect when a message was sent from a staging environment, and ensure it is delivered to the staging mailbox in addition to the recipient's mailbox.

Before events block the active SMTP session while they are running; performing expensive operations in the handlers will reduce the perceived performance of Inbucket; SMTP clients will wait longer to deliver messages, and messages will take longer to appear in their destination mailboxes.

After Events

After events allow extensions to take an action after an event has completed.

For example, an after event handler might:

  • Notify an HTTP endpoint that a new message has been received, including the subject line and senders address.
  • Write metadata about a received message to disk, and then allow it to be inspected by a waiting continuous integration test by executing a shell command.

These events are processed asynchronously with respect to the rest of Inbucket's services; performing (reasonable) file or network operations within them will not degrade Inbucket's performance.

Event Flow

The order in which events occur is:

  1. Before MAIL FROM Accepted
  2. Before RCPT TO Accepted
  3. Before Message Stored
  4. After Message Stored
  5. After Message Deleted

Lua Event: Before MAIL FROM Accepted

This event fires when Inbucket is evaluating an SMTP MAIL FROM command.

Example

This example denies mail that is not from james*@example.com:

local logger = require("logger")

function inbucket.before.mail_from_accepted(session)
	local from = session.from.address
	logger.info(string.format("Inspecting mail from %s", from),
		{ remote_ip = session.remote_addr })

	-- Only allow mail from example.com, using pattern matching.
	if not string.match(from, [[@example%.com$]]) then
		logger.warn("Rejecting blocked origin domain", { addr = from })

		-- Returning `smtp.deny()` causes the `MAIL FROM` command to be rejected.
		return smtp.deny()
	end

	-- Only allow sending users starting with `james`.
	if string.find(from, "james") ~= 1 then
		logger.warn("Rejecting blocked sender", { addr = from })

		-- Returning `smtp.deny(<code>, "message")` causes the `MAIL FROM` command
		-- to be rejected with a custom error.
		return smtp.deny(554, "We only accept mail from james")
	end

	-- Returning `smtp.defer()` instructs Inbucket to continue processing this
	-- message, respecting it's built-in logic and environment variable config.
	-- Using `smtp.allow()` would force Inbucket to accept this `MAIL FROM`.
	return smtp.defer()
end

Example SMTP session

220 inbucket Inbucket SMTP ready
HELO localhost
250 Great, let's get this show on the road
MAIL FROM:<bob@example.com>
554 We only accept mail from james

Inbucket log output

1:17PM INF Starting SMTP session module=smtp remote=127.0.0.1:51726 session=1
1:17PM INF Inspecting mail from bob@example.com module=lua remoteip=127.0.0.1
1:17PM WRN Rejecting blocked sender addr=bob@example.com module=lua
1:17PM WRN Extension denied mail from <bob@example.com> module=smtp remote=127.0.0.1:51726 session=1

Lua Event: Before RCPT TO Accepted

This event fires when Inbucket is evaluating an SMTP RCPT TO command.

Denying a particular recipient does not reject the entire message. The previous RCPT TO addresses will be respected, and the SMTP client may continue at its discretion.

Example

This example denies recipient addresses with a local-part of bad.

local logger = require("logger")

function inbucket.before.rcpt_to_accepted(session)
  logger.info(string.format("Inspecting mail from %s", session.from.address), {})

  -- Reject mail from "bad" user.
  for _, to in ipairs(session.to) do
    logger.info(string.format("Inspecting recipient %s", to.address), {})
    if string.find(to.address, "bad@") == 1 then
      logger.warn("Rejecting blocked recipient", { address = to.address })

      -- Returning `smtp.deny(<code>, "message")` causes the `RCPT TO` command
      -- to be rejected with a custom error.
      return smtp.deny(554, "We don't take kindly to bad users here")
    end
  end

  -- Returning `smtp.defer()` instructs Inbucket to continue processing this
  -- message, respecting it's built-in logic and environment variable config.
  -- Using `smtp.allow()` would force Inbucket to accept this `RCPT TO`.
  return smtp.defer()
end

Example SMTP session

220 inbucket Inbucket SMTP ready
HELO localhost
250 Great, let's get this show on the road
MAIL FROM:<james@example.com>
250 Roger, accepting mail from <james@example.com>
RCPT TO:<good@example.com>
250 I'll make sure <good@example.com> gets this
RCPT TO:<bad@example.com>
554 We don't take kindly to bad users here

Inbucket log output

Notice that our function was called twice; once for each RCPT TO command. The second call received both the good@example.com and bad@example.com addresses in the session.to field.

1:34PM INF Starting SMTP session module=smtp remote=127.0.0.1:40300 session=1
1:34PM INF Mail from: james@example.com module=smtp remote=127.0.0.1:40300 session=1
1:34PM INF Inspecting mail from james@example.com module=lua
1:34PM INF Inspecting recipient good@example.com module=lua
1:34PM INF Inspecting mail from james@example.com module=lua
1:34PM INF Inspecting recipient good@example.com module=lua
1:34PM INF Inspecting recipient bad@example.com module=lua
1:34PM WRN Rejecting blocked recipient address=bad@example.com module=lua
1:34PM WRN Extension denied mail to <{ bad@example.com}> module=smtp remote=127.0.0.1:40300 session=1

Lua Event: Before Message Stored

This event fires after Inbucket has accepted a message, but before it has been stored.

Example

This example changes the destination mailbox to newbox from oldbox, and does not store mail destined for dropme.

local logger = require("logger")

-- The mailbox_mapping table controls how mail is routed by this extension:
--
-- Keys (left) hold the original mailbox name.
--
-- Values (right) hold new mailbox name. A value of false causes mail for that
-- box to be discarded.
local mailbox_mapping = {
  ["oldbox"] = "newbox",
  ["dropme"] = false,
}

function inbucket.before.message_stored(msg)
  local made_changes = false
  local new_mailboxes = {}

  -- Loop over each of the original recipient mailboxes for this message,
  -- building up list of new_mailboxes.
  for _, 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.warn(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 the updated msg to Inbucket.
    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 the original msg.
  return nil
end

Inbucket log output:

4:36PM INF Starting SMTP session module=smtp remote=127.0.0.1:52446 session=2
4:36PM INF Mail from: james@localhost module=smtp remote=127.0.0.1:52446 session=2
4:36PM INF Mapping mailbox "oldbox" to "newbox" module=lua
4:36PM WRN Discarding mail for "dropme" module=lua
4:36PM INF New mailboxes: newbox count=1 module=lua
4:36PM INF Message size 312 bytes module=smtp remote=127.0.0.1:52446 session=2

Lua Event: After Message Stored

This event fires after Inbucket has accepted and stored a message.

Examples

This example prints the metadata of stored messages to the console. Using print() instead of logging is not advised, but this demonstrates the fields available on the message paramter.

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))

  -- Loop over each To address.
  for i, to in ipairs(msg.to) do
    print(string.format("to[%s]: %q <%s>", i, to.name, to.address))
  end

  -- Format the unix timestamp in a human readable way.
  print(string.format("date: %s", os.date("%c", msg.date)))

  print(string.format("size: %s", msg.size))
  print(string.format("subject: %s", msg.subject))
end

Example output:

## message stored ##
mailbox: test
id: 8
from: "" <james@localhost>
to[1]: "" <test@inbucket.local>
date: 22 Mar 24 15:09 PDT
subject: Test from Outlook

Webhook style HTTP post as JSON

In this example, message metadata is converted to JSON and a POST request is made to an external web service:

local http = require("http")
local json = require("json")

BASE_URL = "https://myapi.example.com"

function inbucket.after.message_stored(msg)
  local data = {
    sender = string.format("Mail from %q", msg.from.address),
    subject = msg.subject
  }

  -- Encode the data table above as JSON.
  local body = json.encode(data)

  -- POST JSON or fail.
  assert(http.post(BASE_URL .. "/notify/text", {
    headers = { ["Content-Type"] = "application/json" },
    body = body,
  }))
end

Example JSON body:

{"subject":"Test from Outlook","sender":"Mail from \"james@localhost\""}

Write to file

An example that may be relevant for continuous integration testing. It writes metadata to temporary file and runs external shell command on it:

function inbucket.after.message_stored(msg)
  local content = string.format("%q,%q\n", msg.from.address, 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
end

Example output:

### running cat "/tmp/63708877" ###
"james@localhost","Test from Outlook"

Lua Event: After Message Deleted

This example logs some information when a message is deleted:

local logger = require("logger")

function inbucket.after.message_deleted(msg)
  logger.warn(string.format("Deleted ID %s (subj %q)", msg.id, msg.subject),
    { mailbox = msg.mailbox })
end

Example output:

10:14AM WRN Deleted ID 7 (subj "Test from Gmail") mailbox=test module=lua