- Zig 99.2%
- Nix 0.4%
- Shell 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
actions/checkout moves to v7.0.1 in both workflows, and the publish workflow no longer sets up the attic cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017i7R7ReKJAHGQpitxGXqrb |
||
| .forgejo/workflows | ||
| .tangled/workflows | ||
| LICENSES | ||
| src | ||
| test | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
notmuch.zig
Zig bindings for the notmuch mail indexer's C
library, libnotmuch.
The bindings wrap the C API in idiomatic Zig: opaque C pointers become structs
with methods, notmuch_status_t return codes become Zig error sets (narrowed
per function, so you only handle the errors a given call can actually return),
C enums become Zig enums that are checked against the header at compile time,
and the various notmuch_*_t list types become iterators with a next method.
The narrowed error sets are based on the status codes each function documents,
plus the undocumented ones it has been observed to return in practice (for
example, opening a missing database yields error.NoDatabase). Because
libnotmuch can still surprise — a newer version may add status codes, and
operations on a closed database return codes no function documents — every
narrowed set also includes error.Unexpected as a catch-all, so an
undocumented status surfaces as a recoverable error rather than a crash.
Status: early. The API still changes without notice. All of the current (non-deprecated)
libnotmuchAPI is wrapped.
Requirements
- Zig 0.16.0-dev.2682+02142a54d or newer
libnotmuch5.6 or newer (notmuch 0.32+), including its headers
build.zig links notmuch as a system library and runs translate-c over
notmuch.h. If the header is not on the default include path, point at it with
the NOTMUCH_INCLUDE environment variable:
$ export NOTMUCH_INCLUDE=/usr/include
Installation
Add the package to your project:
$ zig fetch --save git+https://git.ocjtech.us/jeff/notmuch.zig#main
Then wire the module up in your build.zig:
const notmuch = b.dependency("notmuch", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("notmuch", notmuch.module("notmuch"));
The module links libnotmuch and libc itself, so there is nothing else to add.
Cloning with Radicle
The repository is also published on the Radicle peer-to-peer network under the repository ID
rad:zQWqvMomwbxzKg6pd4ktY65B8oyY
With a local Radicle node running, clone it with:
$ rad clone rad:zQWqvMomwbxzKg6pd4ktY65B8oyY
That fetches the repository, checks out the default branch, and starts seeding
it so other peers can fetch from you. To follow the repository without checking
out a working copy, use rad seed instead:
$ rad seed rad:zQWqvMomwbxzKg6pd4ktY65B8oyY
If you already have a clone from one of the other remotes, you can attach it to the same Radicle repository instead of cloning again:
$ git remote add rad rad://zQWqvMomwbxzKg6pd4ktY65B8oyY
$ git fetch rad
Usage
Opening a database returns a tagged union rather than an error union, because
libnotmuch supplies a human-readable message alongside the status code and
that message has to be freed:
const std = @import("std");
const notmuch = @import("notmuch");
pub fn main() !void {
// With no options, the database and config locations are taken from the
// environment ($NOTMUCH_DATABASE, $NOTMUCH_CONFIG, $NOTMUCH_PROFILE) and
// the usual notmuch configuration files.
const db = switch (notmuch.Database.open(.read_only, .{})) {
.ok => |db| db,
.err => |e| {
defer e.deinit();
std.log.err("unable to open database: {s}", .{e.message() orelse "unknown error"});
return e.err;
},
};
defer db.deinit() catch |err| std.log.err("unable to close database: {t}", .{err});
const query = try db.queryCreate("tag:inbox");
defer query.deinit();
query.setSort(.newest_first);
var messages = try query.searchMessages();
defer messages.deinit();
while (try messages.next()) |message| {
defer message.deinit();
const subject = message.getHeader("Subject") orelse "(no subject)";
std.debug.print("{s}\n", .{subject});
var tags = message.getTags();
defer tags.deinit();
while (tags.next()) |tag| std.debug.print(" +{s}\n", .{tag});
}
}
Everything returned by a query — messages, threads, tags, filenames — is owned
by the object it came from, so the deinit calls above are optional; they just
release memory sooner than the enclosing query would.
Helpers
Beyond the wrappers, notmuch.helpers composes the bindings with the Zig
standard library. MessageWriter streams an email into the mail store through
a std.Io.Writer and indexes it: a unique, timestamp-prefixed maildir
filename is generated, and the file is materialized atomically, so no
partially written message is ever visible. MessageReader reads a message's
raw content back through a std.Io.Reader:
var buffer: [4096]u8 = undefined;
var mw = try notmuch.helpers.MessageWriter.init(io, db, &buffer, .{ .subdir = .new });
try mw.writer().print("Message-ID: <{s}>\r\n\r\nHello!\r\n", .{message_id});
const message = try mw.finish(null);
defer message.deinit();
var mr = try notmuch.helpers.MessageReader.init(io, message, &buffer);
defer mr.deinit();
const content = try mr.reader().allocRemaining(allocator, .unlimited);
defer allocator.free(content);
What's available
| Type | Covers |
|---|---|
notmuch.Database |
opening, creating, closing, upgrading and compacting databases; configuration-only loading; atomic sections; revisions; indexing and removing files; message lookup; directory lookup; typed and string-keyed configuration access, values and pairs; status strings |
notmuch.Query |
query strings and syntax, sort order, tag exclusions, message and thread searches, result counts |
notmuch.Message |
message and thread IDs, headers, dates, filenames, flags, tags, maildir flag synchronization, properties, freeze/thaw, reindexing |
notmuch.Thread |
thread ID, subject, authors, oldest/newest dates, message and file counts, contained messages, tags |
notmuch.Directory |
directory mtimes, child files and subdirectories, deletion |
notmuch.IndexOpts |
indexing options, including the decryption policy |
notmuch.MessagesIterator, notmuch.ThreadsIterator, notmuch.FilenamesIterator |
iteration over search results |
notmuch.TagsIterator, notmuch.PropertiesIterator, notmuch.PairsIterator, notmuch.ValuesIterator |
iteration over tags, message properties, and configuration data |
notmuch.Error |
the full set of notmuch_status_t codes as Zig errors, plus error.Unexpected for status codes a function is not documented to return |
notmuch.Status |
the raw notmuch_status_t codes as a Zig enum, with libnotmuch's human-readable descriptions via toString |
notmuch.compact, notmuch.builtWith, notmuch.tag_max |
module-level helpers |
notmuch.helpers.MessageWriter, notmuch.helpers.MessageReader |
streaming an email through a std.Io.Writer into the mail store and indexing it, and reading a message's raw content through a std.Io.Reader (conveniences unique to these bindings) |
Full API documentation is generated from the source and published at https://notmuch-zig.ocjtech.us. To read it locally instead:
$ zig build docs
$ xdg-open zig-out/docs/index.html
Development
A Nix flake provides a development shell with Zig, notmuch, and the linting
tools, and sets NOTMUCH_INCLUDE for you:
$ nix develop
Run the test suite with:
$ zig build test
The suite exercises the bindings against real notmuch databases built in
temporary directories from a small embedded email corpus (src/testing.zig).
Usage-example tests sit next to the functions they exercise as doctests, so
they double as examples in the generated API documentation; edge cases and
error paths live in src/tests.zig. A compile-check test
(src/compile_check.zig) gives every public wrapper a real call site, since
Zig never analyzes the body of a function that is never called.
test/build-archive.sh builds a throwaway notmuch database from a git
repository's commit log, which is useful for exercising the bindings against
larger volumes of real mail-shaped data.
The project is REUSE compliant; reuse lint checks
that every file carries copyright and licensing information.
License
GPL-3.0-or-later. See the LICENSES directory for the full texts.