aboutsummaryrefslogblamecommitdiffstats
path: root/lib/notmuch/database.go
blob: 046d5d187fd4b767005f602be8e77fdafad84c9f (plain) (tree)

























































































































































































































































































































                                                                                                                                         
//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
}