aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Culverhouse <tim@timculverhouse.com>2023-08-29 13:15:45 -0500
committerRobin Jarry <robin@jarry.cc>2023-08-30 22:10:20 +0200
commit3a55b8e6fd51c3dda1ea71c6806f2ee2d71c1065 (patch)
tree93a83c576c8c4cad8164d6b7ef65dbb185aa8390
parentab7d32c1fe5182a7a7631bb4dc35bed49af752c0 (diff)
downloadaerc-3a55b8e6fd51c3dda1ea71c6806f2ee2d71c1065.tar.gz
aerc-3a55b8e6fd51c3dda1ea71c6806f2ee2d71c1065.zip
notmuch: add notmuch bindings
aerc is using an unmaintained fork of a not-well-functioning notmuch binding library. Add custom bindings directly into the aerc repo to make them more maintainable and more customizable to our needs. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
-rwxr-xr-xcontrib/goflags.sh4
-rw-r--r--lib/notmuch/database.go314
-rw-r--r--lib/notmuch/directory.go64
-rw-r--r--lib/notmuch/errors.go55
-rw-r--r--lib/notmuch/message.go260
-rw-r--r--lib/notmuch/messages.go58
-rw-r--r--lib/notmuch/notmuch.go21
-rw-r--r--lib/notmuch/properties.go39
-rw-r--r--lib/notmuch/query.go120
-rw-r--r--lib/notmuch/thread.go99
-rw-r--r--lib/notmuch/threads.go44
11 files changed, 1078 insertions, 0 deletions
diff --git a/contrib/goflags.sh b/contrib/goflags.sh
index 8cc7ae8c..cadf9e18 100755
--- a/contrib/goflags.sh
+++ b/contrib/goflags.sh
@@ -9,6 +9,10 @@ if ${CC:-cc} -x c - -o/dev/null -lnotmuch 2>/dev/null; then
fi <<EOF
#include <notmuch.h>
+#if !LIBNOTMUCH_CHECK_VERSION(5, 6, 0)
+#error "aerc requires libnotmuch.so.5.6 or later"
+#endif
+
void main(void) {
notmuch_status_to_string(NOTMUCH_STATUS_SUCCESS);
}
diff --git a/lib/notmuch/database.go b/lib/notmuch/database.go
new file mode 100644
index 00000000..046d5d18
--- /dev/null
+++ b/lib/notmuch/database.go
@@ -0,0 +1,314 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <stdlib.h>
+#include <notmuch.h>
+
+*/
+import "C"
+
+import (
+ "errors"
+ "fmt"
+ "unsafe"
+)
+
+type Mode int
+
+const (
+ MODE_READ_ONLY Mode = C.NOTMUCH_DATABASE_MODE_READ_ONLY
+ MODE_READ_WRITE Mode = C.NOTMUCH_DATABASE_MODE_READ_WRITE
+)
+
+type Database struct {
+ // The path to the notmuch database. If Path is the empty string, the
+ // location will be found in the following order:
+ //
+ // 1. The value of the environment variable NOTMUCH_DATABASE
+ // 2. From the config file specified by Config
+ // 3. From the Profile specified by profile, given by
+ // $XDG_DATA_HOME/notmuch/$PROFILE
+ Path string
+
+ // The path to the notmuch configuration file to use.
+ Config string
+
+ // If FindConfig is true, libnotmuch will attempt to locate a suitable
+ // configuration file in the following order:
+ //
+ // 1. The value of the environment variable NOTMUCH_CONFIG
+ // 2. $XDG_CONFIG_HOME/notmuch/
+ // 3. $HOME/.notmuch-config
+ //
+ // If not configuration file is found, a STATUS_NO_CONFIG error will be
+ // returned
+ FindConfig bool
+
+ // The profile to use. If Profile is non-empty, the value will be
+ // appended to the paths determined for Config and Path. If Profile is
+ // the empty string, the profile will be determined in the following
+ // order:
+ //
+ // 1. The value of the environment variable NOTMUCH_PROFILE
+ // 2. "default" if Config and/or Path are a directory, "" if they are a
+ // filepath
+ Profile string
+
+ db *C.notmuch_database_t
+ open bool
+}
+
+// Create creates a notmuch database at the Path
+func (db *Database) Create() error {
+ var cdb *C.notmuch_database_t
+ var cPath *C.char
+ defer C.free(unsafe.Pointer(cPath))
+ if db.Path != "" {
+ cPath = C.CString(db.Path)
+ }
+ err := errorWrap(C.notmuch_database_create(cPath, &cdb)) //nolint:gocritic // see note in notmuch.go
+ if err != nil {
+ return err
+ }
+ db.db = cdb
+ return nil
+}
+
+// Open opens the database with the given mode. Caller must call Close when done
+// to commit changes and free resources
+func (db *Database) Open(mode Mode) error {
+ var (
+ cPath *C.char
+ cConfig *C.char
+ cProfile *C.char
+ cErr *C.char
+ )
+ defer C.free(unsafe.Pointer(cPath))
+ defer C.free(unsafe.Pointer(cConfig))
+ defer C.free(unsafe.Pointer(cProfile))
+ defer C.free(unsafe.Pointer(cErr))
+
+ if db.Path != "" {
+ cPath = C.CString(db.Path)
+ }
+
+ if !db.FindConfig {
+ cConfig = C.CString(db.Config)
+ }
+
+ if db.Profile != "" {
+ cProfile = C.CString(db.Profile)
+ }
+ cmode := C.notmuch_database_mode_t(mode)
+
+ var cdb *C.notmuch_database_t
+
+ // gocritic:dupSubExpr throws an issue here no matter how we call this
+ // function
+ err := errorWrap(
+ C.notmuch_database_open_with_config(
+ cPath, cmode, cConfig, cProfile, &cdb, &cErr, //nolint:gocritic // see above
+ ),
+ )
+ if err != nil {
+ return err
+ }
+ db.db = cdb
+ db.open = true
+ return nil
+}
+
+// Reopen an open notmuch database, usually with a different mode
+func (db *Database) Reopen(mode Mode) error {
+ cmode := C.notmuch_database_mode_t(mode)
+ return errorWrap(C.notmuch_database_reopen(db.db, cmode))
+}
+
+// Close commits changes and closes the database, freeing any resources
+// associated with it
+func (db *Database) Close() error {
+ if !db.open {
+ return nil
+ }
+ err := errorWrap(C.notmuch_database_close(db.db))
+ if err != nil {
+ return err
+ }
+ err = errorWrap(C.notmuch_database_destroy(db.db))
+ if err != nil {
+ return err
+ }
+ db.open = false
+ return nil
+}
+
+// LastStatus returns the last status string for the database
+func (db *Database) LastStatus() string {
+ cStatus := C.notmuch_database_status_string(db.db)
+ defer C.free(unsafe.Pointer(cStatus))
+ return C.GoString(cStatus)
+}
+
+func (db *Database) Compact(backupPath string) error {
+ if backupPath == "" {
+ return fmt.Errorf("must have backup path before compacting")
+ }
+ var cBackupPath *C.char
+ defer C.free(unsafe.Pointer(cBackupPath))
+ return errorWrap(C.notmuch_database_compact_db(db.db, cBackupPath, nil, nil))
+}
+
+// Return the resolved path to the notmuch database
+func (db *Database) ResolvedPath() string {
+ cPath := C.notmuch_database_get_path(db.db)
+ return C.GoString(cPath)
+}
+
+// NeedsUpgrade reports if the database must be upgraded before a write
+// operation can be safely performed
+func (db *Database) NeedsUpgrade() bool {
+ return C.notmuch_database_needs_upgrade(db.db) == 1
+}
+
+// Indicate the beginning of an atomic operation
+func (db *Database) BeginAtomic() error {
+ return errorWrap(C.notmuch_database_begin_atomic(db.db))
+}
+
+// Indicate the end of an atomic operation
+func (db *Database) EndAtomic() error {
+ return errorWrap(C.notmuch_database_end_atomic(db.db))
+}
+
+// Returns the UUID and LastMod of the notmuch database
+func (db *Database) Revision() (string, uint64) {
+ var uuid *C.char
+ defer C.free(unsafe.Pointer(uuid))
+ lastmod := uint64(C.notmuch_database_get_revision(db.db, &uuid)) //nolint:gocritic // see note in notmuch.go
+ return C.GoString(uuid), lastmod
+}
+
+// Returns a Directory object relative to the path of the Database
+func (db *Database) Directory(relativePath string) (Directory, error) {
+ var result Directory
+
+ if relativePath == "" {
+ return result, fmt.Errorf("path can't be empty")
+ }
+ var (
+ dir *C.notmuch_directory_t
+ cPath *C.char
+ )
+ cPath = C.CString(relativePath)
+ defer C.free(unsafe.Pointer(cPath))
+ err := errorWrap(C.notmuch_database_get_directory(db.db, cPath, &dir)) //nolint:gocritic // see note in notmuch.go
+ if err != nil {
+ return result, err
+ }
+ result.dir = dir
+
+ return result, nil
+}
+
+// IndexFile indexes a file with path relative to the database path, or an
+// absolute path which share a common ancestor as the database path
+func (db *Database) IndexFile(path string) (Message, error) {
+ var (
+ cPath *C.char
+ msg *C.notmuch_message_t
+ )
+ cPath = C.CString(path)
+ defer C.free(unsafe.Pointer(cPath))
+
+ err := errorWrap(C.notmuch_database_index_file(db.db, cPath, nil, &msg)) //nolint:gocritic // see note in notmuch.go
+ switch {
+ case errors.Is(err, STATUS_DUPLICATE_MESSAGE_ID):
+ break
+ case err != nil:
+ return Message{}, err
+ }
+ message := Message{
+ message: msg,
+ }
+ return message, nil
+}
+
+// Remove a file from the database. If this is the last file associated with a
+// message, the message will be removed from the database.
+func (db *Database) RemoveFile(path string) error {
+ cPath := C.CString(path)
+ defer C.free(unsafe.Pointer(cPath))
+ return errorWrap(C.notmuch_database_remove_message(db.db, cPath))
+}
+
+// FindMessageByID finds a message by the Message-ID header field value
+func (db *Database) FindMessageByID(id string) (Message, error) {
+ var (
+ cID *C.char
+ msg *C.notmuch_message_t
+ )
+ cID = C.CString(id)
+ defer C.free(unsafe.Pointer(cID))
+ err := errorWrap(C.notmuch_database_find_message(db.db, cID, &msg)) //nolint:gocritic // see note in notmuch.go
+ if err != nil {
+ return Message{}, err
+ }
+ message := Message{
+ message: msg,
+ }
+ return message, nil
+}
+
+// FindMessageByFilename finds a message by filename
+func (db *Database) FindMessageByFilename(filename string) (Message, error) {
+ var (
+ cFilename *C.char
+ msg *C.notmuch_message_t
+ )
+ cFilename = C.CString(filename)
+ defer C.free(unsafe.Pointer(cFilename))
+ err := errorWrap(C.notmuch_database_find_message_by_filename(db.db, cFilename, &msg)) //nolint:gocritic // see note in notmuch.go
+ if err != nil {
+ return Message{}, err
+ }
+ if msg == nil {
+ return Message{}, fmt.Errorf("couldn't find message by filename: %s", filename)
+ }
+ message := Message{
+ message: msg,
+ }
+ return message, nil
+}
+
+// Tags returns a slice of all tags in the database
+func (db *Database) Tags() []string {
+ cTags := C.notmuch_database_get_all_tags(db.db)
+ defer C.notmuch_tags_destroy(cTags)
+
+ tags := []string{}
+ for C.notmuch_tags_valid(cTags) > 0 {
+ tag := C.notmuch_tags_get(cTags)
+ tags = append(tags, C.GoString(tag))
+ C.notmuch_tags_move_to_next(cTags)
+ }
+ return tags
+}
+
+// Create a new Query
+func (db *Database) Query(query string) (Query, error) {
+ cQuery := C.CString(query)
+ defer C.free(unsafe.Pointer(cQuery))
+ nmQuery := C.notmuch_query_create(db.db, cQuery)
+ if nmQuery == nil {
+ return Query{}, STATUS_OUT_OF_MEMORY
+ }
+ q := Query{
+ query: nmQuery,
+ }
+ return q, nil
+}
diff --git a/lib/notmuch/directory.go b/lib/notmuch/directory.go
new file mode 100644
index 00000000..796c66ef
--- /dev/null
+++ b/lib/notmuch/directory.go
@@ -0,0 +1,64 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <notmuch.h>
+
+*/
+import "C"
+import "time"
+
+type Directory struct {
+ dir *C.notmuch_directory_t
+}
+
+func (dir *Directory) SetModifiedTime(t time.Time) error {
+ cTime := C.time_t(t.Unix())
+ return errorWrap(C.notmuch_directory_set_mtime(dir.dir, cTime))
+}
+
+func (dir *Directory) ModifiedTime() time.Time {
+ cTime := C.notmuch_directory_get_mtime(dir.dir)
+ return time.Unix(int64(cTime), 0)
+}
+
+func (dir *Directory) Filenames() []string {
+ cFilenames := C.notmuch_directory_get_child_files(dir.dir)
+ defer C.notmuch_filenames_destroy(cFilenames)
+
+ filenames := []string{}
+ for C.notmuch_filenames_valid(cFilenames) > 0 {
+ filename := C.notmuch_filenames_get(cFilenames)
+ filenames = append(filenames, C.GoString(filename))
+ C.notmuch_filenames_move_to_next(cFilenames)
+ }
+ return filenames
+}
+
+func (dir *Directory) Directories() []string {
+ cFilenames := C.notmuch_directory_get_child_directories(dir.dir)
+ defer C.notmuch_filenames_destroy(cFilenames)
+
+ filenames := []string{}
+ for C.notmuch_filenames_valid(cFilenames) > 0 {
+ filename := C.notmuch_filenames_get(cFilenames)
+ filenames = append(filenames, C.GoString(filename))
+ C.notmuch_filenames_move_to_next(cFilenames)
+ }
+ return filenames
+}
+
+// Delete deletes a directory document from the database and destroys
+// the underlying object. Any child directories and files must have been
+// deleted firs the caller
+func (dir *Directory) Delete() error {
+ return errorWrap(C.notmuch_directory_delete(dir.dir))
+}
+
+func (dir *Directory) Close() {
+ C.notmuch_directory_destroy(dir.dir)
+}
diff --git a/lib/notmuch/errors.go b/lib/notmuch/errors.go
new file mode 100644
index 00000000..1b64163d
--- /dev/null
+++ b/lib/notmuch/errors.go
@@ -0,0 +1,55 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <notmuch.h>
+
+*/
+import "C"
+
+// Status codes used for the return values of most functions
+type Status int
+
+const (
+ STATUS_SUCCESS Status = C.NOTMUCH_STATUS_SUCCESS
+ STATUS_OUT_OF_MEMORY Status = C.NOTMUCH_STATUS_OUT_OF_MEMORY
+ STATUS_READ_ONLY_DATABASE Status = C.NOTMUCH_STATUS_READ_ONLY_DATABASE
+ STATUS_XAPIAN_EXCEPTION Status = C.NOTMUCH_STATUS_XAPIAN_EXCEPTION
+ STATUS_FILE_ERROR Status = C.NOTMUCH_STATUS_FILE_ERROR
+ STATUS_FILE_NOT_EMAIL Status = C.NOTMUCH_STATUS_FILE_NOT_EMAIL
+ STATUS_DUPLICATE_MESSAGE_ID Status = C.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID
+ STATUS_NULL_POINTER Status = C.NOTMUCH_STATUS_NULL_POINTER
+ STATUS_TAG_TOO_LONG Status = C.NOTMUCH_STATUS_TAG_TOO_LONG
+ STATUS_UNBALANCED_FREEZE_THAW Status = C.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW
+ STATUS_UNBALANCED_ATOMIC Status = C.NOTMUCH_STATUS_UNBALANCED_ATOMIC
+ STATUS_UNSUPPORTED_OPERATION Status = C.NOTMUCH_STATUS_UNSUPPORTED_OPERATION
+ STATUS_UPGRADE_REQUIRED Status = C.NOTMUCH_STATUS_UPGRADE_REQUIRED
+ STATUS_PATH_ERROR Status = C.NOTMUCH_STATUS_PATH_ERROR
+ STATUS_IGNORED Status = C.NOTMUCH_STATUS_IGNORED
+ STATUS_ILLEGAL_ARGUMENT Status = C.NOTMUCH_STATUS_ILLEGAL_ARGUMENT
+ STATUS_MALFORMED_CRYPTO_PROTOCOL Status = C.NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL
+ STATUS_FAILED_CRYPTO_CONTEXT_CREATION Status = C.NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION
+ STATUS_UNKNOWN_CRYPTO_PROTOCOL Status = C.NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL
+ STATUS_NO_CONFIG Status = C.NOTMUCH_STATUS_NO_CONFIG
+ STATUS_NO_DATABASE Status = C.NOTMUCH_STATUS_NO_DATABASE
+ STATUS_DATABASE_EXISTS Status = C.NOTMUCH_STATUS_DATABASE_EXISTS
+ STATUS_BAD_QUERY_SYNTAX Status = C.NOTMUCH_STATUS_BAD_QUERY_SYNTAX
+ STATUS_NO_MAIL_ROOT Status = C.NOTMUCH_STATUS_NO_MAIL_ROOT
+ STATUS_CLOSED_DATABASE Status = C.NOTMUCH_STATUS_CLOSED_DATABASE
+)
+
+func (s Status) Error() string {
+ status := C.notmuch_status_to_string(C.notmuch_status_t(s))
+ return C.GoString(status)
+}
+
+func errorWrap(st C.notmuch_status_t) error {
+ if Status(st) == STATUS_SUCCESS {
+ return nil
+ }
+ return Status(st)
+}
diff --git a/lib/notmuch/message.go b/lib/notmuch/message.go
new file mode 100644
index 00000000..5b97e39f
--- /dev/null
+++ b/lib/notmuch/message.go
@@ -0,0 +1,260 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <stdlib.h>
+#include <notmuch.h>
+
+*/
+import "C"
+
+import (
+ "time"
+ "unsafe"
+)
+
+type Message struct {
+ message *C.notmuch_message_t
+}
+
+// Close frees resources associated with the message
+func (m *Message) Close() {
+ C.notmuch_message_destroy(m.message)
+}
+
+// ID returns the message ID
+func (m *Message) ID() string {
+ cID := C.notmuch_message_get_message_id(m.message)
+ return C.GoString(cID)
+}
+
+// ThreadID returns the thread ID of the message
+func (m *Message) ThreadID() string {
+ cID := C.notmuch_message_get_thread_id(m.message)
+ return C.GoString(cID)
+}
+
+func (m *Message) Replies() Messages {
+ cMessages := C.notmuch_message_get_replies(m.message)
+ return Messages{
+ messages: cMessages,
+ }
+}
+
+func (m *Message) TotalFiles() int {
+ return int(C.notmuch_message_count_files(m.message))
+}
+
+// Filename returns a single filename associated with the message. If the
+// message has multiple filenames, the return value will be arbitrarily chosen
+func (m *Message) Filename() string {
+ cFilename := C.notmuch_message_get_filename(m.message)
+ return C.GoString(cFilename)
+}
+
+func (m *Message) Filenames() []string {
+ cFilenames := C.notmuch_message_get_filenames(m.message)
+ defer C.notmuch_filenames_destroy(cFilenames)
+
+ filenames := []string{}
+ for C.notmuch_filenames_valid(cFilenames) > 0 {
+ filename := C.notmuch_filenames_get(cFilenames)
+ filenames = append(filenames, C.GoString(filename))
+ C.notmuch_filenames_move_to_next(cFilenames)
+ }
+ return filenames
+}
+
+// TODO is this needed?
+// func (m *Message) Reindex() error {
+//
+// }
+
+type Flag int
+
+const (
+ MESSAGE_FLAG_MATCH Flag = iota
+ MESSAGE_FLAG_EXCLUDED
+ MESSAGE_FLAG_GHOST
+)
+
+func (m *Message) Flag(flag Flag) (bool, error) {
+ var ok C.notmuch_bool_t
+ cFlag := C.notmuch_message_flag_t(flag)
+ err := errorWrap(C.notmuch_message_get_flag_st(m.message, cFlag, &ok))
+ if err != nil {
+ return false, err
+ }
+ if ok == 0 {
+ return false, nil
+ }
+ return true, nil
+}
+
+// TODO why does this exist??
+// func (m *Message) SetFlag(flag Flag) {
+//
+// }
+
+func (m *Message) Date() time.Time {
+ cTime := C.notmuch_message_get_date(m.message)
+ return time.Unix(int64(cTime), 0)
+}
+
+func (m *Message) Header(field string) string {
+ cField := C.CString(field)
+ defer C.free(unsafe.Pointer(cField))
+ cHeader := C.notmuch_message_get_header(m.message, cField)
+ return C.GoString(cHeader)
+}
+
+func (m *Message) Tags() []string {
+ cTags := C.notmuch_message_get_tags(m.message)
+ defer C.notmuch_tags_destroy(cTags)
+
+ tags := []string{}
+ for C.notmuch_tags_valid(cTags) > 0 {
+ tag := C.notmuch_tags_get(cTags)
+ tags = append(tags, C.GoString(tag))
+ C.notmuch_tags_move_to_next(cTags)
+ }
+ return tags
+}
+
+func (m *Message) AddTag(tag string) error {
+ cTag := C.CString(tag)
+ defer C.free(unsafe.Pointer(cTag))
+
+ return errorWrap(C.notmuch_message_add_tag(m.message, cTag))
+}
+
+func (m *Message) RemoveTag(tag string) error {
+ cTag := C.CString(tag)
+ defer C.free(unsafe.Pointer(cTag))
+
+ return errorWrap(C.notmuch_message_remove_tag(m.message, cTag))
+}
+
+func (m *Message) RemoveAllTags() error {
+ return errorWrap(C.notmuch_message_remove_all_tags(m.message))
+}
+
+// SyncTagsToMaildirFlags adds/removes the appropriate tags to the maildir
+// filename
+func (m *Message) SyncTagsToMaildirFlags() error {
+ return errorWrap(C.notmuch_message_tags_to_maildir_flags(m.message))
+}
+
+// SyncMaildirFlagsToTags syncs the current maildir flags to the notmuch tags
+func (m *Message) SyncMaildirFlagsToTags() error {
+ return errorWrap(C.notmuch_message_maildir_flags_to_tags(m.message))
+}
+
+func (m *Message) HasMaildirFlag(flag rune) (bool, error) {
+ var ok C.notmuch_bool_t
+ err := errorWrap(C.notmuch_message_has_maildir_flag_st(m.message, C.char(flag), &ok))
+ if err != nil {
+ return false, err
+ }
+ if ok == 0 {
+ return false, nil
+ }
+ return true, nil
+}
+
+func (m *Message) Freeze() error {
+ return errorWrap(C.notmuch_message_freeze(m.message))
+}
+
+func (m *Message) Thaw() error {
+ return errorWrap(C.notmuch_message_thaw(m.message))
+}
+
+func (m *Message) Property(key string) (string, error) {
+ var (
+ cKey *C.char
+ cValue *C.char
+ )
+ defer C.free(unsafe.Pointer(cKey))
+ defer C.free(unsafe.Pointer(cValue))
+ cKey = C.CString(key)
+ err := errorWrap(C.notmuch_message_get_property(m.message, cKey, &cValue)) //nolint:gocritic // see note in notmuch.go
+ if err != nil {
+ return "", err
+ }
+ return C.GoString(cValue), nil
+}
+
+func (m *Message) AddProperty(key string, value string) error {
+ var (
+ cKey *C.char
+ cValue *C.char
+ )
+ defer C.free(unsafe.Pointer(cKey))
+ defer C.free(unsafe.Pointer(cValue))
+ cKey = C.CString(key)
+ cValue = C.CString(value)
+ return errorWrap(C.notmuch_message_add_property(m.message, cKey, cValue))
+}
+
+func (m *Message) RemoveProperty(key string, value string) error {
+ var (
+ cKey *C.char
+ cValue *C.char
+ )
+ defer C.free(unsafe.Pointer(cKey))
+ defer C.free(unsafe.Pointer(cValue))
+ cKey = C.CString(key)
+ cValue = C.CString(value)
+ return errorWrap(C.notmuch_message_remove_property(m.message, cKey, cValue))
+}
+
+func (m *Message) RemoveAllProperties(key string) error {
+ var cKey *C.char
+ defer C.free(unsafe.Pointer(cKey))
+ cKey = C.CString(key)
+ return errorWrap(C.notmuch_message_remove_all_properties(m.message, cKey))
+}
+
+func (m *Message) RemoveAllPropertiesWithPrefix(prefix string) error {
+ var cPrefix *C.char
+ defer C.free(unsafe.Pointer(cPrefix))
+ cPrefix = C.CString(prefix)
+ return errorWrap(C.notmuch_message_remove_all_properties_with_prefix(m.message, cPrefix))
+}
+
+func (m *Message) Properties(key string, exact bool) *Properties {
+ var (
+ cKey *C.char
+ cExact C.int
+ )
+ defer C.free(unsafe.Pointer(cKey))
+ if exact {
+ cExact = 1
+ }
+
+ cKey = C.CString(key)
+ props := C.notmuch_message_get_properties(m.message, cKey, cExact)
+
+ return &Properties{
+ properties: props,
+ }
+}
+
+func (m *Message) CountProperties(key string) (int, error) {
+ var (
+ cKey *C.char
+ cCount C.uint
+ )
+ defer C.free(unsafe.Pointer(cKey))
+ cKey = C.CString(key)
+ err := errorWrap(C.notmuch_message_count_properties(m.message, cKey, &cCount))
+ if err != nil {
+ return 0, err
+ }
+ return int(cCount), nil
+}
diff --git a/lib/notmuch/messages.go b/lib/notmuch/messages.go
new file mode 100644
index 00000000..22cc0094
--- /dev/null
+++ b/lib/notmuch/messages.go
@@ -0,0 +1,58 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <notmuch.h>
+
+*/
+import "C"
+
+type Messages struct {
+ message *C.notmuch_message_t
+ messages *C.notmuch_messages_t
+}
+
+// Next advances the Messages iterator to the next message. Next returns false if
+// no more messages are available
+func (m *Messages) Next() bool {
+ if C.notmuch_messages_valid(m.messages) == 0 {
+ return false
+ }
+ m.message = C.notmuch_messages_get(m.messages)
+ C.notmuch_messages_move_to_next(m.messages)
+ return true
+}
+
+// Message returns the current message in the iterator
+func (m *Messages) Message() Message {
+ return Message{
+ message: m.message,
+ }
+}
+
+// Close frees memory associated with a Messages iterator. This method is not
+// strictly necessary to call, as the resources will be freed when the Query
+// associated with the Messages object is freed.
+func (m *Messages) Close() {
+ C.notmuch_messages_destroy(m.messages)
+}
+
+// Tags returns a slice of all tags in the message list. WARNING: After calling
+// tags, the message list can no longer be iterated; a new list must be created
+// to iterate after calling Tags
+func (m *Messages) Tags() []string {
+ cTags := C.notmuch_messages_collect_tags(m.messages)
+ defer C.notmuch_tags_destroy(cTags)
+
+ tags := []string{}
+ for C.notmuch_tags_valid(cTags) > 0 {
+ tag := C.notmuch_tags_get(cTags)
+ tags = append(tags, C.GoString(tag))
+ C.notmuch_tags_move_to_next(cTags)
+ }
+ return tags
+}
diff --git a/lib/notmuch/notmuch.go b/lib/notmuch/notmuch.go
new file mode 100644
index 00000000..9b13878b
--- /dev/null
+++ b/lib/notmuch/notmuch.go
@@ -0,0 +1,21 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <stdlib.h>
+#include <notmuch.h>
+
+#if !LIBNOTMUCH_CHECK_VERSION(5, 6, 0)
+#error "aerc requires libnotmuch.so.5.6 or later"
+#endif
+
+*/
+import "C"
+
+// NOTE: Any CGO call which passes a reference to a pointer (**object) will fail
+// gocritic:dupSubExpr. All of these calls are set to be ignored by the linter
+// Reference: https://github.com/go-critic/go-critic/issues/897
diff --git a/lib/notmuch/properties.go b/lib/notmuch/properties.go
new file mode 100644
index 00000000..6c025d05
--- /dev/null
+++ b/lib/notmuch/properties.go
@@ -0,0 +1,39 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <notmuch.h>
+
+*/
+import "C"
+
+type Properties struct {
+ key *C.char
+ value *C.char
+ properties *C.notmuch_message_properties_t
+}
+
+// Next advances the Properties iterator to the next property. Next returns false if
+// no more properties are available
+func (p *Properties) Next() bool {
+ if C.notmuch_message_properties_valid(p.properties) == 0 {
+ return false
+ }
+ p.key = C.notmuch_message_properties_key(p.properties)
+ p.value = C.notmuch_message_properties_value(p.properties)
+ C.notmuch_message_properties_move_to_next(p.properties)
+ return true
+}
+
+// Returns the key of the current iterator location
+func (p *Properties) Key() string {
+ return C.GoString(p.key)
+}
+
+func (p *Properties) Value() string {
+ return C.GoString(p.value)
+}
diff --git a/lib/notmuch/query.go b/lib/notmuch/query.go
new file mode 100644
index 00000000..e621fcf2
--- /dev/null
+++ b/lib/notmuch/query.go
@@ -0,0 +1,120 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <stdlib.h>
+#include <notmuch.h>
+
+*/
+import "C"
+import "unsafe"
+
+type ExcludeMode int
+
+const (
+ EXCLUDE_FLAG ExcludeMode = C.NOTMUCH_EXCLUDE_FLAG
+ EXCLUDE_TRUE ExcludeMode = C.NOTMUCH_EXCLUDE_TRUE
+ EXCLUDE_FALSE ExcludeMode = C.NOTMUCH_EXCLUDE_FALSE
+ EXCLUDE_ALL ExcludeMode = C.NOTMUCH_EXCLUDE_ALL
+)
+
+type SortMode int
+
+const (
+ SORT_OLDEST_FIRST SortMode = C.NOTMUCH_SORT_OLDEST_FIRST
+ SORT_NEWEST_FIRST SortMode = C.NOTMUCH_SORT_NEWEST_FIRST
+ SORT_MESSAGE_ID SortMode = C.NOTMUCH_SORT_MESSAGE_ID
+ SORT_UNSORTED SortMode = C.NOTMUCH_SORT_UNSORTED
+)
+
+type Query struct {
+ query *C.notmuch_query_t
+}
+
+// Close frees resources associated with a query. Closing a query release all
+// resources associated with any underlying search (Threads, Messages, etc)
+func (q *Query) Close() {
+ C.notmuch_query_destroy(q.query)
+}
+
+// Return the string of the query
+func (q *Query) String() string {
+ return C.GoString(C.notmuch_query_get_query_string(q.query))
+}
+
+// Returns the Database associated with the query. The Path, Config, and Profile
+// values will not be set on the returned valued
+func (q *Query) Database() Database {
+ db := C.notmuch_query_get_database(q.query)
+ return Database{
+ db: db,
+ }
+}
+
+// Exclude sets the exclusion mode.
+func (q *Query) Exclude(val ExcludeMode) {
+ cVal := C.notmuch_exclude_t(val)
+ C.notmuch_query_set_omit_excluded(q.query, cVal)
+}
+
+// Sort sets the sort order of the results
+func (q *Query) Sort(sort SortMode) {
+ cVal := C.notmuch_sort_t(sort)
+ C.notmuch_query_set_sort(q.query, cVal)
+}
+
+// SortMode returns the current sort order of the results
+func (q *Query) SortMode() SortMode {
+ return SortMode(C.notmuch_query_get_sort(q.query))
+}
+
+// ExcludeTag adds a tag to exclude from the results
+func (q *Query) ExcludeTag(tag string) error {
+ cTag := C.CString(tag)
+ defer C.free(unsafe.Pointer(cTag))
+ return errorWrap(C.notmuch_query_add_tag_exclude(q.query, cTag))
+}
+
+// Threads returns an iterator over the threads that match the query
+func (q *Query) Threads() (Threads, error) {
+ var cThreads *C.notmuch_threads_t
+ err := errorWrap(C.notmuch_query_search_threads(q.query, &cThreads)) //nolint:gocritic // see note in notmuch.go
+ if err != nil {
+ return Threads{}, err
+ }
+ threads := Threads{
+ threads: cThreads,
+ }
+ return threads, nil
+}
+
+// Messages returns an iterator over the messages that match the query
+func (q *Query) Messages() (Messages, error) {
+ var cMessages *C.notmuch_messages_t
+ err := errorWrap(C.notmuch_query_search_messages(q.query, &cMessages)) //nolint:gocritic // see note in notmuch.go
+ if err != nil {
+ return Messages{}, err
+ }
+ messages := Messages{
+ messages: cMessages,
+ }
+ return messages, nil
+}
+
+// CountMessages returns the number of messages matching the query
+func (q *Query) CountMessages() (int, error) {
+ var count C.uint
+ err := errorWrap(C.notmuch_query_count_messages(q.query, &count))
+ return int(count), err
+}
+
+// CountThreads returns the number of threads matching the query
+func (q *Query) CountThreads() (int, error) {
+ var count C.uint
+ err := errorWrap(C.notmuch_query_count_threads(q.query, &count))
+ return int(count), err
+}
diff --git a/lib/notmuch/thread.go b/lib/notmuch/thread.go
new file mode 100644
index 00000000..1b6eacef
--- /dev/null
+++ b/lib/notmuch/thread.go
@@ -0,0 +1,99 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <stdlib.h>
+#include <notmuch.h>
+
+*/
+import "C"
+import "time"
+
+type Thread struct {
+ thread *C.notmuch_thread_t
+}
+
+// ID returns the thread ID
+func (t *Thread) ID() string {
+ cID := C.notmuch_thread_get_thread_id(t.thread)
+ return C.GoString(cID)
+}
+
+// TotalMessages returns the total number of messages in the thread
+func (t *Thread) TotalMessages() int {
+ return int(C.notmuch_thread_get_total_messages(t.thread))
+}
+
+// TotalMessages returns the total number of files in the thread
+func (t *Thread) TotalFiles() int {
+ return int(C.notmuch_thread_get_total_files(t.thread))
+}
+
+// TopLevelMessages returns an iterator over the top level messages in the
+// thread. Messages are sorted oldest-first
+func (t *Thread) TopLevelMessages() Messages {
+ cMessages := C.notmuch_thread_get_toplevel_messages(t.thread)
+ return Messages{
+ messages: cMessages,
+ }
+}
+
+// Messages returns an iterator over the messages in the thread. Messages are
+// sorted oldest-first
+func (t *Thread) Messages() Messages {
+ cMessages := C.notmuch_thread_get_messages(t.thread)
+ return Messages{
+ messages: cMessages,
+ }
+}
+
+// Matches returns the number of messages in the thread that matched the query
+func (t *Thread) Matches() int {
+ return int(C.notmuch_thread_get_matched_messages(t.thread))
+}
+
+// Returns a string of authors of the thread
+func (t *Thread) Authors() string {
+ cAuthors := C.notmuch_thread_get_authors(t.thread)
+ return C.GoString(cAuthors)
+}
+
+// Returns the subject of the thread
+func (t *Thread) Subject() string {
+ cSubject := C.notmuch_thread_get_subject(t.thread)
+ return C.GoString(cSubject)
+}
+
+// Returns the sent-date of the oldest message in the thread
+func (t *Thread) OldestDate() time.Time {
+ cTime := C.notmuch_thread_get_oldest_date(t.thread)
+ return time.Unix(int64(cTime), 0)
+}
+
+// Returns the sent-date of the newest message in the thread
+func (t *Thread) NewestDate() time.Time {
+ cTime := C.notmuch_thread_get_newest_date(t.thread)
+ return time.Unix(int64(cTime), 0)
+}
+
+// Tags returns a slice of all tags in the thread
+func (t *Thread) Tags() []string {
+ cTags := C.notmuch_thread_get_tags(t.thread)
+ defer C.notmuch_tags_destroy(cTags)
+
+ tags := []string{}
+ for C.notmuch_tags_valid(cTags) > 0 {
+ tag := C.notmuch_tags_get(cTags)
+ tags = append(tags, C.GoString(tag))
+ C.notmuch_tags_move_to_next(cTags)
+ }
+ return tags
+}
+
+func (t *Thread) Close() {
+ C.notmuch_thread_destroy(t.thread)
+}
diff --git a/lib/notmuch/threads.go b/lib/notmuch/threads.go
new file mode 100644
index 00000000..6a2c7b66
--- /dev/null
+++ b/lib/notmuch/threads.go
@@ -0,0 +1,44 @@
+//go:build notmuch
+// +build notmuch
+
+package notmuch
+
+/*
+#cgo LDFLAGS: -lnotmuch
+
+#include <stdlib.h>
+#include <notmuch.h>
+
+*/
+import "C"
+
+// Threads is an iterator over a set of threads.
+type Threads struct {
+ thread *C.notmuch_thread_t
+ threads *C.notmuch_threads_t
+}
+
+// Next advances the Threads iterator to the next thread. Next returns false if
+// no more threads are available
+func (t *Threads) Next() bool {
+ if C.notmuch_threads_valid(t.threads) == 0 {
+ return false
+ }
+ t.thread = C.notmuch_threads_get(t.threads)
+ C.notmuch_threads_move_to_next(t.threads)
+ return true
+}
+
+// Thread returns the current thread in the iterator
+func (t *Threads) Thread() Thread {
+ return Thread{
+ thread: t.thread,
+ }
+}
+
+// Close frees memory associated with a Threads iterator. This method is not
+// strictly necessary to call, as the resources will be freed when the Query
+// associated with the Threads object is freed.
+func (t *Threads) Close() {
+ C.notmuch_threads_destroy(t.threads)
+}