diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/notmuch/database.go | 314 | ||||
-rw-r--r-- | lib/notmuch/directory.go | 64 | ||||
-rw-r--r-- | lib/notmuch/errors.go | 55 | ||||
-rw-r--r-- | lib/notmuch/message.go | 260 | ||||
-rw-r--r-- | lib/notmuch/messages.go | 58 | ||||
-rw-r--r-- | lib/notmuch/notmuch.go | 21 | ||||
-rw-r--r-- | lib/notmuch/properties.go | 39 | ||||
-rw-r--r-- | lib/notmuch/query.go | 120 | ||||
-rw-r--r-- | lib/notmuch/thread.go | 99 | ||||
-rw-r--r-- | lib/notmuch/threads.go | 44 |
10 files changed, 1074 insertions, 0 deletions
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) +} |