BONE - Backlog Of Notable Emails

User manual

Introduction

BONE is an email-driven report tracker. A report is an email that BONE classifies by its subject label (bug, patch, feature request, announcement); later emails in the thread update its state through a small fixed vocabulary of commands. Maintainers can also write to the monitored inbox to update reports and manage roles. BONE exposes the data as an HTML site and JSON, Org and RSS feeds.

Example

Alice sends this email to a mailing list:

From: alice@email.com
Subject: [BUG] Wrong output
To: project-mailing-list@lists.org
Date: Fri, 6 Mar 2026 16:31:14 +0000

Hi all,

when doing X, I get the wrong output, probably a bug.

Bob replies:

From: bob@email.com
Subject: Re: [BUG] Wrong output
To: alice@email.com
Cc: project-mailing-list@lists.org
Date: Fri, 6 Mar 2026 16:38:42 +0000

> when doing X, I get the wrong output, probably a bug.

Confirmed, I see this too, this should not happen.

Eve, the maintainer, concludes:

From: eve@email.com
Subject: Re: [BUG] Wrong output
To: project-mailing-list@lists.org
Date: Fri, 6 Mar 2026 16:42:03 +0000

>> when doing X, I get the wrong output, probably a bug.

> Confirmed, I see this too, this should not happen.

Fixed in commit bc12c0c, thanks to both of you!

Alice reported a bug, Bob confirmed it, Eve closed it. The mailing list is the conversation; BONE indexes it into a queryable database of reports, states, authors and threads.

Principles

Single source
Reports and discussions live on the mailing list.
Email-native
Every state change is an email from the user's mail client.
Reproducible state
The mailbox is the source of truth; you can rebuild the database from the archive at any time.
Fixed vocabulary
The set of states is closed: acked, owned and closed for status; urgent and important for priority.

As a user

Bug, Patch, Request

The subject label sets the report type: mainly [BUG], [PATCH] and [FR/TODO/POLL] (feature requests); see report types.

BONE creates a patch report when:

  1. the subject starts with [PATCH], directly or as Re: [PATCH] ... with a .patch or .diff attachment (the v2/v3 reply workflow);
  2. the email attaches a git format-patch file whose inner Subject: starts with [PATCH].

Inline or quoted diffs, and git diff > x.patch attached to an unrelabelled reply, stay regular replies: attach a real git format-patch file or relabel with [PATCH] instead. BONE stores a patch attached to a [BUG] or [FR] email as metadata and leaves the type unchanged.

Updating reports

A command is a case-sensitive keyword starting a line: in a reply, Confirmed. acks a bug, Applied. or Fixed. closes a patch, Canceled cancels a request. A command ends with punctuation (. , ; : ? !), whitespace or end of line; Urgent and Important require punctuation or end of line (see priority commands).

Triggers change state and apply only to the closest parent report, never to their own email (Acked in a new bug does not confirm it). Annotations set properties on the report the email creates, or else on the closest parent (Urgent. in a new bug raises its priority). See the complete list of commands.

Retracting updates

Retract a state or clear a property with the matching "Not"/"No" command:

Not acked.
Not owned.
Not closed.
Not urgent.
Not important.
No deadline.
No expiry.
No topics.
Not superseded-by: <new-message-id@example.com>
Not supersedes: <old-message-id@example.com>
Not duplicate-of: <target-message-id@example.com>
Not related-to: <other-message-id@example.com>

Like every command, retraction is open to everyone unless a per-command :scope tightens it (see Commands); each update references its email, so you can audit and reverse it.

Crediting a third party

Anyone can act on behalf of someone else with a -by line (bare address or Name <addr>) sent to the BONE inbox:

Acked-by: a-user@example.com
Owned-by: a-user@example.com
Closed-by: a-user@example.com
Owned-by: A User <a-user@example.com>

The credited address holds the state; BONE keeps the posting email for audit.

Setting topics

A report may carry several topics (short strings without whitespace). You can set them from the subject:

A Topics: reply adds each word as a topic; -word removes one, wherever it came from:

Topics: easy.
Topics: -parser lexer.

On a report opened as [BUG parser input], the second line leaves "input" and "lexer". Spaces separate words and a comma does not, so BONE ignores a prose line like Topics: a, b, and c.. Matching is case-insensitive; re-adding a present topic does nothing. A word consists of letters, digits, _ and -, possibly joined by . or / (org-demo.html, net/mlx5e); you cannot remove subject topics with other characters this way. No topics. removes all topics.

Setting a deadline

Deadline: 2026-03-10.

The value is an ISO date (yyyy-MM-dd) or a duration (2d, 3w, 1m, 1y) counted from the sending day (2d sent June 1 > June 3); trailing punctuation is optional. BONE shows the deadline in JSON, as an Org =DEADLINE: line, and in notification emails. No deadline. removes it.

Setting an expiry date

A per-report expiry date schedules automatic closing:

Expiry: 2026-06-15.
Expiry: 30d.

Expiry: takes the same values as Deadline:; BONE closes the report once that day is over (UTC). The expiry date appears in JSON as :expiry, as an Org :EXPIRY: property, and as a date-column tooltip in the HTML index. No expiry. removes it. To expire reports at their deadline, use :inactive-after :deadline in the source-level expiry rules (see report expiry).

Superseding a report

A report can replace another (newer patch version, better bug report). In the old report's thread, write:

Superseded-by: <new-message-id@example.com>

In the new report's thread (reply or opening mail), write:

Supersedes: <old-message-id@example.com>

A message-id is <id@host>, bare id@host, or the last path segment of an archive URL such as https://list.orgmode.org/orgmode/id@host/. Both reports must have the same type (bug, patch or request).

A report has at most one superseder: among repeated Superseded-by:, Supersedes: or Duplicate-of: lines in one email (both closing commands compete), the last wins; against Not closed. in the same email, the closure wins. On a closed report, a new closure replaces the old one: reason, credit and relation follow the last command. Undo with Not superseded-by: (old thread) or Not supersedes: (new thread).

Supersession of patches

A new patch report automatically supersedes an earlier open patch report in its thread with the same base subject (ignoring Re:=/=Fwd:, [TAG] brackets, parenthesized version markers, case and punctuation), covering re-sent unversioned patches:

[PATCH] A new feature          : patch report (1)
  Re: [PATCH] A new feature    : patch report (2), supersedes (1)
    Re: [PATCH] A new feature  : patch report (3), supersedes (2)

Only (3) stays open. The reply must itself be a patch report (see above); a plain Re: without a patch is discussion. Versioned patches ([PATCH v2]) match on version and topic instead; see Patch series.

Resolving a bug or request with a patch

A .patch or .diff reply to an open bug or request (nearest parent) credits you Acked and Owned on it (only Owned if you reported it). Closing that patch with Applied. or Fixed. closes the parent with reason :resolved; cancelling the patch retracts the implicit credit; superseding it transfers Owned to the new author, while Acked stays with whoever first confirmed the parent.

Marking a report as a duplicate

Duplicate-of: <target-message-id@example.com>

This command takes the same message-id forms and has the same type constraint as Superseded-by:. BONE closes the duplicate with reason :canceled and links it to the target; undo with Not duplicate-of:.

Cross-referencing reports

Related-to: <other-message-id@example.com>

This command links any pair of reports and takes the same message-id forms as Superseded-by:; several lines post several links. Undo with Not related-to:.

Voting on requests

Requests ([POLL], [TODO] and aliases) accept votes in replies:

Each voter has one vote per report; a new vote replaces the old one.

As a maintainer

Maintainers use the same email channel as users, with broader permissions and extra report types.

Announcements, releases, changes

Maintainers can post announcements ([ANN]), releases ([REL]) and upcoming changes for a future release ([CHG]). A [REL v2.0] report closes every open [CHG v2.0] report with reason :resolved and cross-links them to the release.

Events (ICS calendar support)

When an [ANN] report has an attached .ics file or inline VCALENDAR content, BONE stores the calendar data with the report and exports:

[ANN event] sets the topic to "event" for filtering, but BONE includes any announcement with ICS content whatever its topic.

Managing roles and permissions

Report commands need no permission (see Commands); only role controls require one:

Write to the BONE inbox, with a bare address or the Display Name <addr> form:

Add maintainer: dev@example.com        (any maintainer)
Remove maintainer: dev@example.com     (lead maintainer only)
Add maintainer: Dev User <dev@example.com>

State-changing actions (commands and role controls) must go through the source's public channel -- mailing list or alias -- so every BONE instance on the same source sees the same state.

Report types

BONE detects a report's type from its subject label:

TagType
[BUG <topic>]bug
[PATCH <topic> <version> <n/m>]patch
[POLL <topic>] or [TODO <topic>]request
[ANN <topic>]announcement
[REL <topic> <version>]release
[CHG <topic> <version>]change

BONE also accepts the long-form aliases [ANNOUNCEMENT], [RELEASE] and [CHANGE]. Labels are case-sensitive and need whitespace (or the end of the subject) after the closing ]; BONE ignores malformed labels.

By default, announcements, releases and changes require maintainer permission; set :restricted-types per source to change this (see Source options).

All <...> parts are optional and positional. For [REL] and [CHG], a single value is the <version> ([REL 2.0] > version "2.0"); give both for topic and version: =[REL parser 2.0]. For [PATCH], <version> is a v\d+ token (e.g. v2) and <n/m> a sequence marker (e.g. 3/5).

Patch series

A patch series groups patches sharing a sequence marker (<n/m>). A cover letter (0/m) or a fresh 1/m patch from the same sender and topic closes the previous series and starts a new one: thus [PATCH parser v2 0/3] supersedes the earlier v1 series.

Cover letter commands

A command in a reply to the cover letter [PATCH 0/m] applies to every patch in the series: Closed. closes the whole series, Deadline: 30d sets the deadline on every patch. The exceptions are Supersedes: and Related-to: (and their Not variants), which name a specific target and stay on the cover only.

Review trailers

BONE collects git person trailers from replies to a patch: unquoted Acked-by:, Reviewed-by:, Tested-by:, Reported-by:, Suggested-by:, Signed-off-by: and Co-developed-by: lines that start a line and carry an email address. BONE exports them verbatim in the report JSON's trailers field, for patch-applying clients (e.g. gnaw.el) to fold into the commit message, as b4 does.

Cover-letter trailers apply to every patch. Reviewed-by: and Acked-by: are also commands (they ack the report, crediting the named address); other keys never change the report's status.

Customising subject labels

Override the default labels globally or per source in config.edn through :labels; unlisted types keep their defaults. The precedence order is per source > global > built-in.

;; Global override (applies to all sources)
:labels {:bug     ["BUG" "DEFECT"]
         :request ["POLL" "FR" "TODO" "RFE"]}
;; Per-source override (inside a source map)
{:name "my-list"
 :labels {:announcement ["ANN" "NEWS"]}}

Commands

A command is a keyword at the start of a line in a thread reply.

Triggers and annotations

Colon-prefixed commands (Topics:, Deadline:, Expiry:, Acked-by:, Owned-by:, Closed-by:, Superseded-by:, Supersedes:, Duplicate-of:, Related-to:) need whitespace after the colon: Topics: parser works, but BONE silently ignores Topics:parser.

Status commands

Command keywordEffect on report
Acked Confirmed Approvedacked
Ownedowned
Canceled Cancelledclosed (canceled)
Expiredclosed (expired)
Resolved Applied Fixed Closed Completedclosed (resolved)

A report's status combines three states, which BONE also expresses as a number:

:acked
someone took the next sensible action -- confirmed a bug, reviewed a patch, approved a request.
:owned
someone claimed the report.
:closed
the report is resolved, canceled, or expired.
(un)acked(0)1
(un)owned(0)2
(un)closed(4)0

For example, an acked, unowned and open report has status 1+0+4 = 5.

You can ack or own only bugs, patches and requests; you can still mark announcements, releases and changes urgent or important, and close them.

Priority commands

Command keywordEffect on report
Urgent.Mark as urgent
Important.Mark as important

Urgent and Important need punctuation (. , ; : ? !) or end of line after them; BONE rejects a bare space. BONE expresses priority as a number:

(un)important(0)1
(un)urgent(0)2

For example, an important and urgent report has priority 1+2 = 3.

Multiple commands

Put several keywords in one email, one per line:

From: x@y.z
Date: Thu, 19 Mar 2026 16:45:00 +0100
Subject: Re: [BUG] Crash on startup

> Crash on startup with the new build.

Confirmed.
Urgent.
Important.

Propagation

Some updates propagate through threads and series:

All commands

Report commands are open to everyone by default; per-command :scope overrides in config.edn can restrict them to :maintainer (or :setter-or-maintainer for retraction commands). Role controls always require maintainer rights.

The six patterns

ShapeMeaningExample
X.set a stateAcked., Closed., Urgent.
Not X.retract a stateNot acked., Not urgent.
X: <value>set an annotationTopics: parser, Deadline: 30d
No X.clear an annotationNo deadline., No topics.
X-by: <addr>credit the update to XAcked-by: a@x, Closed-by: a@x
X: <mid> / Not X: <mid>pose or retract a relationSuperseded-by: <m@x>, Related-to: <m@x>

"Not" negates a state; "No" removes a property (deadline, expiry, topics). Every word setting acked, owned, urgent or important -- built-in or per-source :words synonym -- has a Not <word> form: Not confirmed. undoes Confirmed.. Not closed. is the only reopening form.

Triggers

This table lists triggers in the order they typically appear in a report's life:

IDSyntax / WordsPermissionReport types
:ackedAcked, Confirmed, Approvedany userbug, patch, request
:acked-byAcked-by: addr@host, Reviewed-by: addr@hostany userbug, patch, request
:unackedNot acked, Not confirmed, Not approvedany userbug, patch, request
:ownedOwnedany userbug, patch, request
:owned-byOwned-by: addr@hostany userbug, patch, request
:unownedNot ownedany userbug, patch, request
:closed (resolved)Closed, Resolved, Applied, Fixed, Completedany userall
:closed (canceled)Canceled, Cancelledany userall
:closed (expired)Expiredany userall
:closed-byClosed-by: addr@hostany userall
:unclosedNot closedany userall
vote up+1 or 1+any userrequest
vote down-1 or 1-any userrequest
vote null+0 / 0+ / -0 / 0-any userrequest
:superseded-bySuperseded-by: <msg-id>any userbug, patch, request
:unsuperseded-byNot superseded-by: <msg-id>any userbug, patch, request
:duplicate-ofDuplicate-of: <msg-id>any userbug, patch, request
:unduplicate-ofNot duplicate-of: <msg-id>any userbug, patch, request

Annotations

Priority

IDSyntax / WordsPermissionReport types
:urgentUrgent.any userall
:unurgentNot urgentany userall
:importantImportant.any userall
:unimportantNot importantany userall

Classification

IDSyntax / WordsPermissionReport types
:topicTopics: [-]word ...any userall
:untopicNo topicsany userall

Scheduling

IDSyntax / WordsPermissionReport types
:deadlineDeadline: 2025-03-19 or Deadline: 30dany userall
:undeadlineNo deadlineany userall
:expiryExpiry: 2025-06-15 or Expiry: 30dany userall
:unexpiryNo expiryany userall

Relations

IDSyntax / WordsPermissionReport types
:supersedesSupersedes: <msg-id>any userbug, patch, request
:unsupersedesNot supersedes: <msg-id>any userbug, patch, request
:related-toRelated-to: <msg-id>any userall
:unrelated-toNot related-to: <msg-id>any userall

Sent to a patch-series cover letter, these two relations stay on the cover (see Cover letter commands).

All controls

A control manages a role; you must send it through the source's public channel.

IDSyntax / WordsPermissionChannel
roleAdd maintainer: addr@hostmaintainerpublic
roleRemove maintainer: addr@hostlead maintainerpublic

Exploring reports

BONE exports HTML pages and JSON, Org and RSS feeds.

Exported pages and feeds

bb export all generates, per source:

RSS feeds hold the 50 latest items; maintainer reports carry <category>maintainer</category> and role:maintainer in the description, for filtering. HTML pages advertise their feeds, all.xml first, via <link rel"alternate" type="application/rss+xml">= (RSS autodiscovery); set :base-url to make these links absolute, as the specification recommends.

Export is incremental: it covers only the sources modified since the last export. --force rebuilds everything, and bb export {json|rss|org|html|stats|text|events} builds one format.

FlagPurposeConfigurable
-n <source>restrict to one source--
-p <N>filter by minimum priority--
-s <N>filter by minimum status--
--html-page-size <N>paginate HTML report table (client-side rendering)CLI only
--html-columns <C>HTML table columns to show (CSV subset; default: all)CLI only
--html-columns-sortdefault sort column for the HTML table (default: date)CLI only
--closed-retentionexclude reports closed before given ISO date or durationCLI only
--topics-filterrestrict to topics (comma-separated, case-insensitive)CLI only
--forceforce a full re-exportCLI only

Durations take the forms Nd, Nw, Nm, Ny (--closed-retention 1y keeps reports closed in the last 12 months). A run with -p, -s or --topics-filter is never incremental, and the next run is a full re-export.

Vote transparency

Each vote is an entity recording voter and source email; a per-source votes.json makes them auditable:

{
  "<11@test.org>": {
    "+1": [{"voter": "alice@example.org", "message-id": "<12@test.org>"}],
    "-1": [{"voter": "bob@example.org",   "message-id": "<13@test.org>"}],
    "0":  []
  }
}

A new vote replaces the voter's previous one. BONE derives the per-report JSON counts (votes, votes-up, votes-down, votes-null) from these entities at export time.

Browsing BONE reports online

The HTML site filters reports with key:value tokens: space is AND, | separates OR clauses, a comma in a value is OR. type:bug s:crash | topic:latex keeps bugs whose subject matches "crash", plus all reports with a latex topic. A bare word (or unknown key) searches the subject. A leading - negates the whole token, comma alternatives included: -type:patch, -acked:*.

Double quotes make a value (or bare token) literal: spaces, commas, | and slashes lose their meaning, so subject:"preamble shuffling" matches that phrase. An empty value matches nothing.

/.../ is a case-insensitive regexp on text fields (from:, subject: and bare words, topic:, source:, mid:, acked:, owned:, closed:): subject:/v[0-9]+/. It spans the whole value (a , is literal), and you cannot use | inside it: OR two regexps with two clauses (s:/a/ | s:/b/).

Long formShortMatches
from:bzgf:from address or name
subject:crash,latexs:subject (a bare word matches subject)
similar:a+b+csubjects sharing 3 of the +-joined words
topic:agendaT:any of the report's topics
source:emacsS:source name (one char: exact)
type:patcht:report type
priority:2p:exact priority (0-3)
mid:<...>m:message-id
acked:bzg owned:* closed:*a: o: c:by person (or * for any)
urgent:* important:falseu: i:marks (set or not)
flags:AOF:Flags letters, all required (A O C R E S)
att:~+A:Att glyphs, all required (. ~ + x @ #)
date:3d date:A..Bd:created (durations Nd/Nw/Nm, ranges)
deadline:2mD:due within / on (forward)
expired:10de:expiring within (forward)
closed:truec:trueinclude closed reports (transient)

A lone date is that exact day; open range ends stay open (date:2026-01-01..). date: looks backward from today, deadline: and expired: forward.

type:, flags: and att: compare literally: no * (type:* matches nothing), quotes or regexps. For "priority at least 2", use p:2,3.

closed:true bypasses the Open filter without toggling the button (clearing the search restores it). Any closed: or flags: token naming a close reason (C R E S) also includes closed reports.

gnaw.el shares this syntax, with Emacs regexps and source: also matching its local one-letter source identifiers.

The gnaw CLI tool and Emacs clients

Notifications

BONE can email digests to the subscribers that the operator declares, with their filters, under :notifications :subscribers (see Notifications (SMTP)). A digest lists the recipient's failed commands, the open bugs, patches and requests they own (upcoming deadlines first), and open ones nobody has acked or owned.

Subscribing or changing preferences

You cannot subscribe by email: ask the operator to edit config.edn to subscribe, unsubscribe or adjust filters (minimum priority, subject match, topic). The operator runs bb notify from cron or a systemd timer (--dry-run prints without sending); it tracks no per-recipient interval.

Deploying BONE

BONE reads a single config.edn; validate it after editing with bb test-config.

Setting up an instance

Here is a minimal configuration, with one IMAP mailbox and one source:

{:mailboxes [{:name     "primary"
              :type     :imap
              :host     "imap.example.com"
              :port     993
              :ssl      true
              :user     "imap-login@example.com"
              :password "secret"
              :folder   "INBOX"}]

 :sources [{:name        "my-list"
            :list        "my-list.example.org"
            :maintainers ["lead@example.org"]}]}

:mailboxes is a vector of maps, each with a unique :name (BONE uses it to key the watermark and prefix logs) and a :type (:imap or :maildir). Optional sections default to :db {:path "data/bone-db"}, stderr logging at :info, and :ingest {:fetch {:limit 50}}; see config.edn.example for the annotated reference.

To keep IMAP/SMTP credentials out of config.edn, use one of these forms:

:password-file "/etc/bone/imap.pwd"   ; trimmed contents read at startup
:password      #bone/env "BONE_IMAP_PWD"  ; resolved from the environment

BONE fails at startup if the file or variable is missing; the two forms exclude each other. The file form suits systemd's LoadCredential=imap-pwd:/etc/bone/imap.pwd and Docker secrets (/run/secrets/imap-pwd).

For Maildir, :path is the parent directory and :folder the subfolder ("" when :path itself holds cur/, new/, tmp/). Watch mode uses filesystem events, not polling.

{:mailboxes [{:name   "local"
              :type   :maildir
              :path   "/home/me/Mail"
              :folder "INBOX"}]
 :sources [{:name "my-list" :list "my-list.example.org"}]}

Reproducing a published instance

For each source, BONE publishes a self-contained reports/config.edn (linked from docs.html under Configuration): it folds in the global :labels, :commands, :expiry, etc. and strips :mailboxes, :db, :logging and :notifications. To reproduce the dashboard, replace its placeholder :mailboxes with your source (a Maildir of your subscription or your IMAP account) and run bb export.

reports/meta.json holds the source identity (:list / :alias / :to, :list-archive, :archive-format-string, :base-url), project links (:website, :contribute-url) if set, and :reports-files (all.json, all-open.json, all-closed.json, per-type files like patches-open.json). gnaw uses its List-Id as the stable key binding a published source to a mailbox path from its local config; gnaw --add-source offers only :reports-files when adding a tracker by base URL.

Multiple mailboxes

:mailboxes can mix IMAP and Maildir, e.g. an upstream list account plus a Maildir fed by forge2mail:

{:mailboxes [{:name     "upstream"
              :type     :imap
              :host     "imap.example.com" :port 993 :ssl true
              :user     "me@example.com"   :password "secret"}
             {:name     "forge"
              :type     :maildir
              :path     "/home/me/Mail/forge"
              :folder   ""}]
 :sources  [{:name "my-project" :list "my-project.example.org"}
            {:name "issues"     :to   "issues@example.com"}]}
  • Batch (clj -M:run, default) walks :mailboxes in order, logging and skipping any that fails to open or fetch; expiry and stale-pending flush run once at the end if at least one succeeded.
  • Watch (clj -M:run -- --watch) runs one thread per mailbox with its own reconnect/backoff; a shared atom runs the daily expire/flush exactly once per 24 hours.

BONE keys watermarks (IMAP UIDs, Maildir baselines) by :name in Datalevin; sources are global, and BONE matches them by List-Id / X-Original-To / Delivered-To whatever the mailbox. Renaming a mailbox orphans its watermark and restarts from the :fetch baseline; clj -M:run -- --fresh wipes the DB, orphans included.

BONE shallow-merges a per-mailbox :ingest map (:fetch, :max-size, :max-attachment-size) over the top-level one; the CLI option --fetch overrides both.

{:mailboxes [{:name "upstream" :type :imap :host "..." :user "..." :password "..."}
             {:name "forge"
              :type :maildir
              :path "/home/me/Mail/forge"
              :folder ""
              ;; Only fetch the last 7 days from the forge bridge,
              ;; while the upstream mailbox keeps the global default.
              :ingest {:fetch {:since "7d"}}}]
 :ingest    {:fetch {:limit 200}}
 :sources   [{:name "..." :list "..."}]}

Trust model and MTA requirements

BONE identifies every actor (author, maintainer, voter, command issuer) by From: and does not verify DKIM, SPF or DMARC: the upstream MTA must filter forged mail. Hence:

We recommend this posture:

Without trusted upstream filtering, treat maintainer changes and announcements as advisory; audit the source Message-Id before acting.

Monitoring your local mail

To watch your own mail on one machine:

For background runs, install the user systemd unit docs/deploy/bone.service (see Running BONE in production).

Source types and matching

A source has exactly one type key, which sets the header it matches:

Matching is exact and case-insensitive, never by substring (:list "my-list" does not match my-list.example.org). BONE checks every value of a repeated header and every address of a comma-separated recipient list, and discards at ingestion the emails that match no source.

Source options

KeyDescription
:nameSource identifier (required)
:list / :alias / :toExactly one: source type and header to match
:maintainersInitial maintainers, e.g. ["dev@example.org"]; the first is lead (Managing roles and permissions)
:list-archiveURL of the list archive (shown in HTML reports)
:archive-format-stringMessage-id URL template (%s replaced), used without Archived-At header
:base-urlBase URL serving this source's public directory (absolute RSS autodiscovery links)
:websiteProject website, linked from the source name in page titles and footer
:contribute-urlContribution guide, target of the "Create new" button on the reports page
:post-addressPosting address shown on docs.html (mailbox sources fall back to :to)
:labelsLabel overrides (customising subject labels)
:commandsCommand overrides (customising commands)
:report-typesRestrict detected and exported types, e.g. #{:bug :patch}
:restricted-typesTypes only maintainers may create (default: #{:announcement :release :change})
:export-formatsOutput formats, e.g. ["json" "rss"] (default: ["json" "org" "rss"])
:expiryAuto-close rules per report type (see below)
:notifications{:enabled false} silences this source's notifications (default: enabled)
:awaiting-delayDuration before flagging "awaiting reply", e.g. "14d" (default: 14 days)
:periodsTime-windowed overrides (Source periods)

Source periods

When a source's vocabulary or maintainers changed over time, declare the history with :periods, a vector of time-windowed overrides. Each entry may set :start / :end (ISO yyyy-MM-dd) plus any of :maintainers, :commands, :command-syntax, :labels, :restricted-types; omitted fields inherit from the source level (the current state). BONE resolves the effective config for each email from its date.

{:name "orgmode"
 :list "emacs-orgmode.gnu.org"
 :maintainers ["current-lead@example.org"]    ;; default + current era
 :periods
 [{:end "2020-01-01"
   :maintainers ["old-lead@example.org"]
   :commands    {:closed {:words ["Done" "Fixed"]}}}
  {:start "2020-01-01" :end "2024-01-01"
   :maintainers ["mid-lead@example.org" "co@example.org"]}
  {:start "2024-01-01"                         ;; last era, no :end
   :maintainers ["current-lead@example.org"]}]}

These rules apply:

Report expiry

BONE can auto-close reports of any type whose age and state match the rules you configure, with close reason "expired". Each entry under :expiry maps a report type keyword to a rule map:

A report expires only when all conditions hold.

:expiry {:announcement {:inactive-after "30d"}
         :release      {:inactive-after "6w"}
         :change       {:inactive-after "3m"}
         :bug          {:inactive-after "1000d"
                        :max-status 0      ;; only open, unacked, unowned
                        :max-priority 0}   ;; only non-urgent, non-important
         :request      {:inactive-after "1000d"
                        :max-status 0}}

You can set :expiry globally or per source (per-source wins).

Tuning ingestion

:ingest {:fetch    {:limit 50}    ;; or {:since "30d"} or {:start "..." :end "..."}
         :max-size 1048576}       ;; skip emails > 1 MB (optional)

Notifications (SMTP)

:notifications
{:enabled true
 :smtp {:host     "smtp.example.com"
        :port     587
        :tls      true
        :user     "notify@example.com"
        :password "secret"
        :from     "bone@example.com"
        :reply-to "lead@example.com"}   ;; optional
 :admin-bcc "ops@example.com"           ;; optional; string or vector
 :subscribers
 {"a@example.org"
  [{:source "my-list"}]
  "b@example.org"
  [{:source "my-list" :min-priority 2}
   {:source "another-list" :topic "release"}]}}

Each :subscribers key maps a recipient address to a vector of subscriptions, one per source: a recipient of three sources gets three emails per bb notify run. BONE blind-copies :admin-bcc (an address or a vector) on every digest, so you can audit deliverability and catch bounces.

Each subscription accepts optional filters:

In operation:

Logging

:logging {:file     "logs/bone.log"
          :level    :warn
          :max-size "10MB"
          :backlog  5
          :email    {:to    "ops@example.com"
                     :level :error}}

Log rotation is automatic. Email alerts require SMTP configuration.

Running BONE in production

Watch mode under systemd
The daemon stays up, reacts to IMAP IDLE or Maildir filesystem events, and reconnects on failure. Install the unit file docs/deploy/bone.service to /etc/systemd/system/bone.service (root) or ~/.config/systemd/user/ (user), then run systemctl [--user] daemon-reload && systemctl [--user] enable --now bone.
Batch mode under cron
clj -M:run connects, fetches mail since the watermark, digests, expires, and exits. Chain bb export and bb notify after it; see docs/deploy/crontab.example.

Either mode can run from an uberjar to avoid Clojure startup cost; see Quick start in README.org.

Customising commands

You can customise command keywords globally or per source.

Overriding words, permissions, and report types

Each entry is a map with at least one of :words, :scope, :report-types:

:commands {:closed   {:words ["Canceled" "Fixed" "Wontfix"]}
           :acked    {:words ["Approved" "LGTM"]
                      :scope :maintainer}               ;; Restrict to maintainers
           :deadline {:report-types
                      #{:bug :patch :request}}          ;; Restrict to actionables
           :unclosed {:scope :setter-or-maintainer}}    ;; Override scope only
  • :scope overrides the command's default permission:
    • :user -- anyone
    • :maintainer -- maintainers only
    • :setter-or-maintainer -- the address that set the attribute, or any maintainer. It is valid only on unset commands whose attribute BONE tracks by a ref to the pose-email: :unacked, :unowned, :unclosed, :unurgent, :unimportant, :untopic, :undeadline, :unexpiry, :unsuperseded-by, :unsupersedes, :unduplicate-of; bb test-config rejects it elsewhere.
  • :report-types replaces the command's default report types (absent => registry default).
  • You can override any trigger or annotation listed in All commands, but not role controls.

The precedence order is per-source :commands > global :commands > built-in defaults.

Syntax mode: :loose vs :strict

:command-syntax sets whether commands need a ! prefix. The default :loose accepts both; :strict requires it on every command, avoiding false positives on common words ("Done.", "Closed.", "Applied.") in ordinary prose.

FormLooseStrict
BarewordClosed. / !Closed.!Closed.
Negative barewordNot acked / !Not acked!Not acked
-by lineAcked-by: a@b.c!Acked-by: a@b.c
Date lineDeadline: 2026-06-01!Deadline: 2026-06-01
TopicsTopics: event!Topics: event
SupersedeSuperseded-by: <mid>!Superseded-by: <mid>
Supersede (sym.)Supersedes: <mid>!Supersedes: <mid>
Role controlAdd maintainer: a@b.c!Add maintainer: a@b.c

You can set it globally or per source (per-source wins):

:command-syntax :strict

Replaying archives under a different convention

If a source's vocabulary or syntax mode changed over time (e.g. it moved from :loose to :strict, or accepted Done. only in one era), declare per-era :commands, :command-syntax and :maintainers in :periods (see Source periods); BONE reads each email with the config in force at its date.

Maintenance

bb maintenance reports and prunes orphan emails -- emails no report references and no maintainer sent. Stop the daemon before running it.

bb maintenance                  # dry run, show orphan counts
bb maintenance --delete         # actually delete orphan emails
bb maintenance --verbose        # list individual orphan message-ids
bb maintenance --failures       # list recent command failures recorded
                                # for maintainers (denied actions, bad
                                # syntax, etc.)
bb maintenance --retention 6m   # override default orphan retention
                                # (90 days). Accepts "30d", "6m", "1y"
                                # or an ISO date.
bb maintenance -n my-source     # scope to a single source

Rebuilding history

You declare a source's history (vocabulary, syntax, maintainers) with :periods (see Source periods). To apply edited periods, or any config change, retroactively, wipe the DB and replay:

clj -M:run -- --fresh   # interactive confirm, then full replay

CLI reference

Export, notification and maintenance tasks run through Babashka:

TaskDescription
bb exportIncremental export of all formats (skip if unchanged)
bb export --forceFull rebuild, ignore timestamps
bb export rootRegenerate top-level index.html only
bb export --html-page-size 100Paginate HTML table (100 reports/page)
bb export html --html-columns "type,subject,date"Show only those HTML table columns
bb export --closed-retention 1yKeep only reports closed within the last 12 months
bb export --topics-filter "a,b"Only export reports with a topic in the list
bb export --only-openAlso export -open files (all-open.json, etc.)
bb export FORMATOne of json, rss, org, html, stats, patches, text, events, root, all
bb notifySend notification emails
bb notify --dry-runPreview notifications without sending
bb notify --debugVerbose filtering diagnostics
bb maintenanceOrphan emails and command failures (Maintenance)
bb cleanRemove all files in public/
bb build-jarBuild target/bone.jar + bin/bone wrapper
bb test-configValidate config.edn
bb test-smtpTest SMTP config (--to addr, --send; default = dry-run)

Export also accepts -n <source> (one source), -p <N> (minimum priority) and -s <N> (minimum status).

Appendix 1: workflow examples

Patch review cycle

A contributor submits a patch series:

From: carol@dev.org
Subject: [PATCH parser v1 0/2] Refactor tokenizer

Cover letter: this series splits the tokenizer into two passes.
From: carol@dev.org
Subject: [PATCH parser v1 1/2] Extract lexer phase

<inline patch>
From: carol@dev.org
Subject: [PATCH parser v1 2/2] Add second pass

<inline patch>

BONE creates a patch series (cover + 2 patches). The maintainer asks for changes:

From: eve@project.org
Subject: Re: [PATCH parser v1 1/2] Extract lexer phase

> <inline patch>

The naming is confusing, can you rename foo to bar?

Carol resubmits as v2:

From: carol@dev.org
Subject: [PATCH parser v2 0/2] Refactor tokenizer

v2: renamed foo to bar as requested.

BONE closes the v1 series and opens a v2 series. The maintainer applies it, which closes v2 (resolved):

From: eve@project.org
Subject: Re: [PATCH parser v2 0/2] Refactor tokenizer

Applied, thanks!

Maintainer triage

A bug has gone unanswered for a while; the maintainer triages it on the list:

From: eve@project.org
To: bugs-list@project.org
Subject: Re: [BUG] Crash on startup

Confirmed.
Owned.
Urgent.

Later, the fix lands:

From: eve@project.org
To: bugs-list@project.org
Subject: Re: [BUG] Crash on startup

Fixed in commit abc123, thanks for reporting!

Role management

Any maintainer can add a maintainer via the list; only the lead maintainer can Remove maintainer:.

From: eve@project.org
To: bugs-list@project.org
Subject: Role update

Add maintainer: newdev@project.org

Automatic expiry and override

Take this config:

:expiry {:bug {:inactive-after "90d" :max-status 0}}

At the next expiry run, BONE closes with reason "expired" a bug whose last email is 91 days old and that is still unacked and unowned. To keep it open, a maintainer marks it owned, lifting it above :max-status 0; No expiry. clears any per-report expiry date previously set:

From: eve@project.org
To: bugs-list@project.org
Subject: Re: [BUG] Old encoding issue

This is still relevant, keeping it open.
No expiry.
Owned.

Appendix 2: credits

Many thanks to Ihor Radchenko for early feedback on the design.