Return-Path: X-Original-To: notmuch@notmuchmail.org Delivered-To: notmuch@notmuchmail.org Received: from localhost (localhost [127.0.0.1]) by olra.theworths.org (Postfix) with ESMTP id 591BA431FDA for ; Mon, 30 Apr 2012 12:56:41 -0700 (PDT) X-Virus-Scanned: Debian amavisd-new at olra.theworths.org X-Spam-Flag: NO X-Spam-Score: 0 X-Spam-Level: X-Spam-Status: No, score=0 tagged_above=-999 required=5 tests=[none] autolearn=disabled Received: from olra.theworths.org ([127.0.0.1]) by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id Z8lQCSxjR+TL for ; Mon, 30 Apr 2012 12:56:35 -0700 (PDT) Received: from mail.cryptobitch.de (cryptobitch.de [88.198.7.68]) (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) (No client certificate requested) by olra.theworths.org (Postfix) with ESMTPS id BE109431FAE for ; Mon, 30 Apr 2012 12:56:34 -0700 (PDT) Received: from mail.jade-hamburg.de (mail.jade-hamburg.de [85.183.11.228]) (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) (No client certificate requested) by mail.cryptobitch.de (Postfix) with ESMTPSA id 112ED584BA3 for ; Mon, 30 Apr 2012 21:56:33 +0200 (CEST) Received: by mail.jade-hamburg.de (Postfix, from userid 401) id 62DD1DF2A8; Mon, 30 Apr 2012 21:56:32 +0200 (CEST) Received: from thinkbox.jade-hamburg.de (unknown [10.1.1.153]) (using TLSv1 with cipher AES256-SHA (256/256 bits)) (No client certificate requested) (Authenticated sender: teythoon) by mail.jade-hamburg.de (Postfix) with ESMTPSA id 6A147DF2A1; Mon, 30 Apr 2012 21:55:52 +0200 (CEST) Received: from teythoon by thinkbox.jade-hamburg.de with local (Exim 4.77) (envelope-from ) id 1SOwhL-0003yk-Hz; Mon, 30 Apr 2012 21:55:51 +0200 From: Justus Winter <4winter@informatik.uni-hamburg.de> To: notmuch@notmuchmail.org Subject: [PATCH 1/5] go: reorganize the go bindings Date: Mon, 30 Apr 2012 21:55:40 +0200 Message-Id: <1335815744-15258-1-git-send-email-4winter@informatik.uni-hamburg.de> X-Mailer: git-send-email 1.7.10 In-Reply-To: <20120430195420.2528.48612@thinkbox.jade-hamburg.de> References: <20120430195420.2528.48612@thinkbox.jade-hamburg.de> X-BeenThere: notmuch@notmuchmail.org X-Mailman-Version: 2.1.13 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 30 Apr 2012 19:56:41 -0000 Signed-off-by: Justus Winter <4winter@informatik.uni-hamburg.de> --- bindings/go/cmds/notmuch-addrlookup.go | 259 ----- bindings/go/pkg/notmuch.go | 1124 ---------------------- bindings/go/src/notmuch-addrlookup/addrlookup.go | 259 +++++ bindings/go/src/notmuch/notmuch.go | 1124 ++++++++++++++++++++++ 4 files changed, 1383 insertions(+), 1383 deletions(-) delete mode 100644 bindings/go/cmds/notmuch-addrlookup.go delete mode 100644 bindings/go/pkg/notmuch.go create mode 100644 bindings/go/src/notmuch-addrlookup/addrlookup.go create mode 100644 bindings/go/src/notmuch/notmuch.go diff --git a/bindings/go/cmds/notmuch-addrlookup.go b/bindings/go/cmds/notmuch-addrlookup.go deleted file mode 100644 index 16958e5..0000000 --- a/bindings/go/cmds/notmuch-addrlookup.go +++ /dev/null @@ -1,259 +0,0 @@ -package main - -// stdlib imports -import "os" -import "path" -import "log" -import "fmt" -import "regexp" -import "strings" -import "sort" - -// 3rd-party imports -import "notmuch" -import "github.com/kless/goconfig/config" - -type mail_addr_freq struct { - addr string - count [3]uint -} - -type frequencies map[string]uint - -/* Used to sort the email addresses from most to least used */ -func sort_by_freq(m1, m2 *mail_addr_freq) int { - if (m1.count[0] == m2.count[0] && - m1.count[1] == m2.count[1] && - m1.count[2] == m2.count[2]) { - return 0 - } - - if (m1.count[0] > m2.count[0] || - m1.count[0] == m2.count[0] && - m1.count[1] > m2.count[1] || - m1.count[0] == m2.count[0] && - m1.count[1] == m2.count[1] && - m1.count[2] > m2.count[2]) { - return -1 - } - - return 1 -} - -type maddresses []*mail_addr_freq - -func (self *maddresses) Len() int { - return len(*self) -} - -func (self *maddresses) Less(i,j int) bool { - m1 := (*self)[i] - m2 := (*self)[j] - v := sort_by_freq(m1, m2) - if v<=0 { - return true - } - return false -} - -func (self *maddresses) Swap(i,j int) { - (*self)[i], (*self)[j] = (*self)[j], (*self)[i] -} - -// find most frequent real name for each mail address -func frequent_fullname(freqs frequencies) string { - var maxfreq uint = 0 - fullname := "" - freqs_sz := len(freqs) - - for mail,freq := range freqs { - if (freq > maxfreq && mail != "") || freqs_sz == 1 { - // only use the entry if it has a real name - // or if this is the only entry - maxfreq = freq - fullname = mail - } - } - return fullname -} - -func addresses_by_frequency(msgs *notmuch.Messages, name string, pass uint, addr_to_realname *map[string]*frequencies) *frequencies { - - freqs := make(frequencies) - - pattern := `\s*(("(\.|[^"])*"|[^,])*?)` - // pattern := "\\s*((\\\"(\\\\.|[^\\\\\"])*\\\"|[^,])*" + - // "\\b\\w+([-+.]\\w+)*\\@\\w+[-\\.\\w]*\\.([-\\.\\w]+)*\\w\\b)>?)" - pattern = `.*` + strings.ToLower(name) + `.*` - var re *regexp.Regexp = nil - var err os.Error = nil - if re,err = regexp.Compile(pattern); err != nil { - log.Printf("error: %v\n", err) - return &freqs - } - - headers := []string{"from"} - if pass == 1 { - headers = append(headers, "to", "cc", "bcc") - } - - for ;msgs.Valid();msgs.MoveToNext() { - msg := msgs.Get() - //println("==> msg [", msg.GetMessageId(), "]") - for _,header := range headers { - froms := strings.ToLower(msg.GetHeader(header)) - //println(" froms: ["+froms+"]") - for _,from := range strings.Split(froms, ",", -1) { - from = strings.Trim(from, " ") - match := re.FindString(from) - //println(" -> match: ["+match+"]") - occ,ok := freqs[match] - if !ok { - freqs[match] = 0 - occ = 0 - } - freqs[match] = occ+1 - } - } - } - return &freqs -} - -func search_address_passes(queries [3]*notmuch.Query, name string) []string { - var val []string - addr_freq := make(map[string]*mail_addr_freq) - addr_to_realname := make(map[string]*frequencies) - - var pass uint = 0 // 0-based - for _,query := range queries { - if query == nil { - //println("**warning: idx [",idx,"] contains a nil query") - continue - } - msgs := query.SearchMessages() - ht := addresses_by_frequency(msgs, name, pass, &addr_to_realname) - for addr, count := range *ht { - freq,ok := addr_freq[addr] - if !ok { - freq = &mail_addr_freq{addr:addr, count:[3]uint{0,0,0}} - } - freq.count[pass] = count - addr_freq[addr] = freq - } - msgs.Destroy() - pass += 1 - } - - addrs := make(maddresses, len(addr_freq)) - { - iaddr := 0 - for _, freq := range addr_freq { - addrs[iaddr] = freq - iaddr += 1 - } - } - sort.Sort(&addrs) - - for _,addr := range addrs { - freqs,ok := addr_to_realname[addr.addr] - if ok { - val = append(val, frequent_fullname(*freqs)) - } else { - val = append(val, addr.addr) - } - } - //println("val:",val) - return val -} - -type address_matcher struct { - // the notmuch database - db *notmuch.Database - // full path of the notmuch database - user_db_path string - // user primary email - user_primary_email string - // user tag to mark from addresses as in the address book - user_addrbook_tag string -} - -func new_address_matcher() *address_matcher { - var cfg *config.Config - var err os.Error - - // honor NOTMUCH_CONFIG - home := os.Getenv("NOTMUCH_CONFIG") - if home == "" { - home = os.Getenv("HOME") - } - - if cfg,err = config.ReadDefault(path.Join(home, ".notmuch-config")); err != nil { - log.Fatalf("error loading config file:",err) - } - - db_path,_ := cfg.String("database", "path") - primary_email,_ := cfg.String("user", "primary_email") - addrbook_tag,err := cfg.String("user", "addrbook_tag") - if err != nil { - addrbook_tag = "addressbook" - } - - self := &address_matcher{db:nil, - user_db_path:db_path, - user_primary_email:primary_email, - user_addrbook_tag:addrbook_tag} - return self -} - -func (self *address_matcher) run(name string) { - queries := [3]*notmuch.Query{} - - // open the database - self.db = notmuch.OpenDatabase(self.user_db_path, - notmuch.DATABASE_MODE_READ_ONLY) - - // pass 1: look at all from: addresses with the address book tag - query := "tag:" + self.user_addrbook_tag - if name != "" { - query = query + " and from:" + name + "*" - } - queries[0] = self.db.CreateQuery(query) - - // pass 2: look at all to: addresses sent from our primary mail - query = "" - if name != "" { - query = "to:"+name+"*" - } - if self.user_primary_email != "" { - query = query + " from:" + self.user_primary_email - } - queries[1] = self.db.CreateQuery(query) - - // if that leads only to a few hits, we check every from too - if queries[0].CountMessages() + queries[1].CountMessages() < 10 { - query = "" - if name != "" { - query = "from:"+name+"*" - } - queries[2] = self.db.CreateQuery(query) - } - - // actually retrieve and sort addresses - results := search_address_passes(queries, name) - for _,v := range results { - if v != "" && v != "\n" { - fmt.Println(v) - } - } - return -} - -func main() { - //fmt.Println("args:",os.Args) - app := new_address_matcher() - name := "" - if len(os.Args) > 1 { - name = os.Args[1] - } - app.run(name) -} \ No newline at end of file diff --git a/bindings/go/pkg/notmuch.go b/bindings/go/pkg/notmuch.go deleted file mode 100644 index de9de23..0000000 --- a/bindings/go/pkg/notmuch.go +++ /dev/null @@ -1,1124 +0,0 @@ -// Wrapper around the notmuch library - -package notmuch - -/* -#include -#include -#include -#include "notmuch.h" -*/ -import "C" -import "unsafe" - -// Status codes used for the return values of most functions -type Status C.notmuch_status_t -const ( - STATUS_SUCCESS Status = 0 - STATUS_OUT_OF_MEMORY - STATUS_READ_ONLY_DATABASE - STATUS_XAPIAN_EXCEPTION - STATUS_FILE_ERROR - STATUS_FILE_NOT_EMAIL - STATUS_DUPLICATE_MESSAGE_ID - STATUS_NULL_POINTER - STATUS_TAG_TOO_LONG - STATUS_UNBALANCED_FREEZE_THAW - - STATUS_LAST_STATUS -) - -func (self Status) String() string { - var p *C.char - - // p is read-only - p = C.notmuch_status_to_string(C.notmuch_status_t(self)) - if p != nil { - s := C.GoString(p) - return s - } - return "" -} - -/* Various opaque data types. For each notmuch__t see the various - * notmuch_ functions below. */ - -type Database struct { - db *C.notmuch_database_t -} - -type Query struct { - query *C.notmuch_query_t -} - -type Threads struct { - threads *C.notmuch_threads_t -} - -type Thread struct { - thread *C.notmuch_thread_t -} - -type Messages struct { - messages *C.notmuch_messages_t -} - -type Message struct { - message *C.notmuch_message_t -} - -type Tags struct { - tags *C.notmuch_tags_t -} - -type Directory struct { - dir *C.notmuch_directory_t -} - -type Filenames struct { - fnames *C.notmuch_filenames_t -} - -type DatabaseMode C.notmuch_database_mode_t -const ( - DATABASE_MODE_READ_ONLY DatabaseMode = 0 - DATABASE_MODE_READ_WRITE -) - -// Create a new, empty notmuch database located at 'path' -func NewDatabase(path string) *Database { - - var c_path *C.char = C.CString(path) - defer C.free(unsafe.Pointer(c_path)) - - if c_path == nil { - return nil - } - - self := &Database{db:nil} - self.db = C.notmuch_database_create(c_path) - if self.db == nil { - return nil - } - return self -} - -/* Open an existing notmuch database located at 'path'. - * - * The database should have been created at some time in the past, - * (not necessarily by this process), by calling - * notmuch_database_create with 'path'. By default the database should be - * opened for reading only. In order to write to the database you need to - * pass the NOTMUCH_DATABASE_MODE_READ_WRITE mode. - * - * An existing notmuch database can be identified by the presence of a - * directory named ".notmuch" below 'path'. - * - * The caller should call notmuch_database_destroy when finished with - * this database. - * - * In case of any failure, this function returns NULL, (after printing - * an error message on stderr). - */ -func OpenDatabase(path string, mode DatabaseMode) *Database { - - var c_path *C.char = C.CString(path) - defer C.free(unsafe.Pointer(c_path)) - - if c_path == nil { - return nil - } - - self := &Database{db:nil} - self.db = C.notmuch_database_open(c_path, C.notmuch_database_mode_t(mode)) - if self.db == nil { - return nil - } - return self -} - -/* Close the given notmuch database, freeing all associated - * resources. See notmuch_database_open. */ -func (self *Database) Close() { - C.notmuch_database_destroy(self.db) -} - -/* Return the database path of the given database. - */ -func (self *Database) GetPath() string { - - /* The return value is a string owned by notmuch so should not be - * modified nor freed by the caller. */ - var p *C.char = C.notmuch_database_get_path(self.db) - if p != nil { - s := C.GoString(p) - return s - } - return "" -} - -/* Return the database format version of the given database. */ -func (self *Database) GetVersion() uint { - return uint(C.notmuch_database_get_version(self.db)) -} - -/* Does this database need to be upgraded before writing to it? - * - * If this function returns TRUE then no functions that modify the - * database (notmuch_database_add_message, notmuch_message_add_tag, - * notmuch_directory_set_mtime, etc.) will work unless the function - * notmuch_database_upgrade is called successfully first. */ -func (self *Database) NeedsUpgrade() bool { - do_upgrade := C.notmuch_database_needs_upgrade(self.db) - if do_upgrade == 0 { - return false - } - return true -} - -// TODO: notmuch_database_upgrade - - -/* Retrieve a directory object from the database for 'path'. - * - * Here, 'path' should be a path relative to the path of 'database' - * (see notmuch_database_get_path), or else should be an absolute path - * with initial components that match the path of 'database'. - * - * Can return NULL if a Xapian exception occurs. - */ -func (self *Database) GetDirectory(path string) *Directory { - var c_path *C.char = C.CString(path) - defer C.free(unsafe.Pointer(c_path)) - - if c_path == nil { - return nil - } - - c_dir := C.notmuch_database_get_directory(self.db, c_path) - if c_dir == nil { - return nil - } - return &Directory{dir:c_dir} -} - -/* Add a new message to the given notmuch database. - * - * Here,'filename' should be a path relative to the path of - * 'database' (see notmuch_database_get_path), or else should be an - * absolute filename with initial components that match the path of - * 'database'. - * - * The file should be a single mail message (not a multi-message mbox) - * that is expected to remain at its current location, (since the - * notmuch database will reference the filename, and will not copy the - * entire contents of the file. - * - * If 'message' is not NULL, then, on successful return '*message' - * will be initialized to a message object that can be used for things - * such as adding tags to the just-added message. The user should call - * notmuch_message_destroy when done with the message. On any failure - * '*message' will be set to NULL. - * - * Return value: - * - * NOTMUCH_STATUS_SUCCESS: Message successfully added to database. - * - * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred, - * message not added. - * - * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message - * ID as another message already in the database. The new - * filename was successfully added to the message in the database - * (if not already present). - * - * NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the - * file, (such as permission denied, or file not found, - * etc.). Nothing added to the database. - * - * NOTMUCH_STATUS_FILE_NOT_EMAIL: the contents of filename don't look - * like an email message. Nothing added to the database. - * - * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only - * mode so no message can be added. - */ -func -(self *Database) AddMessage(fname string) (*Message, Status) { - var c_fname *C.char = C.CString(fname) - defer C.free(unsafe.Pointer(c_fname)) - - if c_fname == nil { - return nil, STATUS_OUT_OF_MEMORY - } - - var c_msg *C.notmuch_message_t = new(C.notmuch_message_t) - st := Status(C.notmuch_database_add_message(self.db, c_fname, &c_msg)) - - return &Message{message:c_msg}, st -} - -/* Remove a message from the given notmuch database. - * - * Note that only this particular filename association is removed from - * the database. If the same message (as determined by the message ID) - * is still available via other filenames, then the message will - * persist in the database for those filenames. When the last filename - * is removed for a particular message, the database content for that - * message will be entirely removed. - * - * Return value: - * - * NOTMUCH_STATUS_SUCCESS: The last filename was removed and the - * message was removed from the database. - * - * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred, - * message not removed. - * - * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: This filename was removed but - * the message persists in the database with at least one other - * filename. - * - * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only - * mode so no message can be removed. - */ -func (self *Database) RemoveMessage(fname string) Status { - - var c_fname *C.char = C.CString(fname) - defer C.free(unsafe.Pointer(c_fname)) - - if c_fname == nil { - return STATUS_OUT_OF_MEMORY - } - - st := C.notmuch_database_remove_message(self.db, c_fname) - return Status(st) -} - -/* Find a message with the given message_id. - * - * If the database contains a message with the given message_id, then - * a new notmuch_message_t object is returned. The caller should call - * notmuch_message_destroy when done with the message. - * - * This function returns NULL in the following situations: - * - * * No message is found with the given message_id - * * An out-of-memory situation occurs - * * A Xapian exception occurs - */ -func (self *Database) FindMessage(message_id string) (*Message, Status) { - - var c_msg_id *C.char = C.CString(message_id) - defer C.free(unsafe.Pointer(c_msg_id)) - - if c_msg_id == nil { - return nil, STATUS_OUT_OF_MEMORY - } - - msg := &Message{message:nil} - st := Status(C.notmuch_database_find_message(self.db, c_msg_id, &msg.message)) - if st != STATUS_SUCCESS { - return nil, st - } - return msg, st -} - -/* Return a list of all tags found in the database. - * - * This function creates a list of all tags found in the database. The - * resulting list contains all tags from all messages found in the database. - * - * On error this function returns NULL. - */ -func (self *Database) GetAllTags() *Tags { - tags := C.notmuch_database_get_all_tags(self.db) - if tags == nil { - return nil - } - return &Tags{tags:tags} -} - -/* Create a new query for 'database'. - * - * Here, 'database' should be an open database, (see - * notmuch_database_open and notmuch_database_create). - * - * For the query string, we'll document the syntax here more - * completely in the future, but it's likely to be a specialized - * version of the general Xapian query syntax: - * - * http://xapian.org/docs/queryparser.html - * - * As a special case, passing either a length-zero string, (that is ""), - * or a string consisting of a single asterisk (that is "*"), will - * result in a query that returns all messages in the database. - * - * See notmuch_query_set_sort for controlling the order of results. - * See notmuch_query_search_messages and notmuch_query_search_threads - * to actually execute the query. - * - * User should call notmuch_query_destroy when finished with this - * query. - * - * Will return NULL if insufficient memory is available. - */ -func (self *Database) CreateQuery(query string) *Query { - - var c_query *C.char = C.CString(query) - defer C.free(unsafe.Pointer(c_query)) - - if c_query == nil { - return nil - } - - q := C.notmuch_query_create(self.db, c_query) - if q == nil { - return nil - } - return &Query{query:q} -} - -/* Sort values for notmuch_query_set_sort */ -type Sort C.notmuch_sort_t -const ( - SORT_OLDEST_FIRST Sort = 0 - SORT_NEWEST_FIRST - SORT_MESSAGE_ID - SORT_UNSORTED -) - -/* Return the query_string of this query. See notmuch_query_create. */ -func (self *Query) String() string { - // FIXME: do we own 'q' or not ? - q := C.notmuch_query_get_query_string(self.query) - //defer C.free(unsafe.Pointer(q)) - - if q != nil { - s := C.GoString(q) - return s - } - - return "" -} - -/* Specify the sorting desired for this query. */ -func (self *Query) SetSort(sort Sort) { - C.notmuch_query_set_sort(self.query, C.notmuch_sort_t(sort)) -} - -/* Return the sort specified for this query. See notmuch_query_set_sort. */ -func (self *Query) GetSort() Sort { - return Sort(C.notmuch_query_get_sort(self.query)) -} - -/* Execute a query for threads, returning a notmuch_threads_t object - * which can be used to iterate over the results. The returned threads - * object is owned by the query and as such, will only be valid until - * notmuch_query_destroy. - * - * Typical usage might be: - * - * notmuch_query_t *query; - * notmuch_threads_t *threads; - * notmuch_thread_t *thread; - * - * query = notmuch_query_create (database, query_string); - * - * for (threads = notmuch_query_search_threads (query); - * notmuch_threads_valid (threads); - * notmuch_threads_move_to_next (threads)) - * { - * thread = notmuch_threads_get (threads); - * .... - * notmuch_thread_destroy (thread); - * } - * - * notmuch_query_destroy (query); - * - * Note: If you are finished with a thread before its containing - * query, you can call notmuch_thread_destroy to clean up some memory - * sooner (as in the above example). Otherwise, if your thread objects - * are long-lived, then you don't need to call notmuch_thread_destroy - * and all the memory will still be reclaimed when the query is - * destroyed. - * - * Note that there's no explicit destructor needed for the - * notmuch_threads_t object. (For consistency, we do provide a - * notmuch_threads_destroy function, but there's no good reason - * to call it if the query is about to be destroyed). - * - * If a Xapian exception occurs this function will return NULL. - */ -func (self *Query) SearchThreads() *Threads { - threads := C.notmuch_query_search_threads(self.query) - if threads == nil { - return nil - } - return &Threads{threads:threads} -} - -/* Execute a query for messages, returning a notmuch_messages_t object - * which can be used to iterate over the results. The returned - * messages object is owned by the query and as such, will only be - * valid until notmuch_query_destroy. - * - * Typical usage might be: - * - * notmuch_query_t *query; - * notmuch_messages_t *messages; - * notmuch_message_t *message; - * - * query = notmuch_query_create (database, query_string); - * - * for (messages = notmuch_query_search_messages (query); - * notmuch_messages_valid (messages); - * notmuch_messages_move_to_next (messages)) - * { - * message = notmuch_messages_get (messages); - * .... - * notmuch_message_destroy (message); - * } - * - * notmuch_query_destroy (query); - * - * Note: If you are finished with a message before its containing - * query, you can call notmuch_message_destroy to clean up some memory - * sooner (as in the above example). Otherwise, if your message - * objects are long-lived, then you don't need to call - * notmuch_message_destroy and all the memory will still be reclaimed - * when the query is destroyed. - * - * Note that there's no explicit destructor needed for the - * notmuch_messages_t object. (For consistency, we do provide a - * notmuch_messages_destroy function, but there's no good - * reason to call it if the query is about to be destroyed). - * - * If a Xapian exception occurs this function will return NULL. - */ -func (self *Query) SearchMessages() *Messages { - msgs := C.notmuch_query_search_messages(self.query) - if msgs == nil { - return nil - } - return &Messages{messages:msgs} -} - -/* Destroy a notmuch_query_t along with any associated resources. - * - * This will in turn destroy any notmuch_threads_t and - * notmuch_messages_t objects generated by this query, (and in - * turn any notmuch_thread_t and notmuch_message_t objects generated - * from those results, etc.), if such objects haven't already been - * destroyed. - */ -func (self *Query) Destroy() { - if self.query != nil { - C.notmuch_query_destroy(self.query) - } -} - -/* Return an estimate of the number of messages matching a search - * - * This function performs a search and returns Xapian's best - * guess as to number of matching messages. - * - * If a Xapian exception occurs, this function may return 0 (after - * printing a message). - */ -func (self *Query) CountMessages() uint { - return uint(C.notmuch_query_count_messages(self.query)) -} - -// TODO: wrap threads and thread - -/* Is the given 'threads' iterator pointing at a valid thread. - * - * When this function returns TRUE, notmuch_threads_get will return a - * valid object. Whereas when this function returns FALSE, - * notmuch_threads_get will return NULL. - * - * See the documentation of notmuch_query_search_threads for example - * code showing how to iterate over a notmuch_threads_t object. - */ -func (self *Threads) Valid() bool { - if self.threads == nil { - return false - } - valid := C.notmuch_threads_valid(self.threads) - if valid == 0 { - return false - } - return true -} - -/* Destroy a notmuch_threads_t object. - * - * It's not strictly necessary to call this function. All memory from - * the notmuch_threads_t object will be reclaimed when the - * containg query object is destroyed. - */ -func (self *Threads) Destroy() { - if self.threads != nil { - C.notmuch_threads_destroy(self.threads) - } -} - -/* Is the given 'messages' iterator pointing at a valid message. - * - * When this function returns TRUE, notmuch_messages_get will return a - * valid object. Whereas when this function returns FALSE, - * notmuch_messages_get will return NULL. - * - * See the documentation of notmuch_query_search_messages for example - * code showing how to iterate over a notmuch_messages_t object. - */ -func (self *Messages) Valid() bool { - if self.messages == nil { - return false - } - valid := C.notmuch_messages_valid(self.messages) - if valid == 0 { - return false - } - return true -} - -/* Get the current message from 'messages' as a notmuch_message_t. - * - * Note: The returned message belongs to 'messages' and has a lifetime - * identical to it (and the query to which it belongs). - * - * See the documentation of notmuch_query_search_messages for example - * code showing how to iterate over a notmuch_messages_t object. - * - * If an out-of-memory situation occurs, this function will return - * NULL. - */ -func (self *Messages) Get() *Message { - if self.messages == nil { - return nil - } - msg := C.notmuch_messages_get(self.messages) - if msg == nil { - return nil - } - return &Message{message:msg} -} - -/* Move the 'messages' iterator to the next message. - * - * If 'messages' is already pointing at the last message then the - * iterator will be moved to a point just beyond that last message, - * (where notmuch_messages_valid will return FALSE and - * notmuch_messages_get will return NULL). - * - * See the documentation of notmuch_query_search_messages for example - * code showing how to iterate over a notmuch_messages_t object. - */ -func (self *Messages) MoveToNext() { - if self.messages == nil { - return - } - C.notmuch_messages_move_to_next(self.messages) -} - -/* Destroy a notmuch_messages_t object. - * - * It's not strictly necessary to call this function. All memory from - * the notmuch_messages_t object will be reclaimed when the containing - * query object is destroyed. - */ -func (self *Messages) Destroy() { - if self.messages != nil { - C.notmuch_messages_destroy(self.messages) - } -} - -/* Return a list of tags from all messages. - * - * The resulting list is guaranteed not to contain duplicated tags. - * - * WARNING: You can no longer iterate over messages after calling this - * function, because the iterator will point at the end of the list. - * We do not have a function to reset the iterator yet and the only - * way how you can iterate over the list again is to recreate the - * message list. - * - * The function returns NULL on error. - */ -func (self *Messages) CollectTags() *Tags { - if self.messages == nil { - return nil - } - tags := C.notmuch_messages_collect_tags(self.messages) - if tags == nil { - return nil - } - return &Tags{tags:tags} -} - -/* Get the message ID of 'message'. - * - * The returned string belongs to 'message' and as such, should not be - * modified by the caller and will only be valid for as long as the - * message is valid, (which is until the query from which it derived - * is destroyed). - * - * This function will not return NULL since Notmuch ensures that every - * message has a unique message ID, (Notmuch will generate an ID for a - * message if the original file does not contain one). - */ -func (self *Message) GetMessageId() string { - - if self.message == nil { - return "" - } - id := C.notmuch_message_get_message_id(self.message) - // we dont own id - // defer C.free(unsafe.Pointer(id)) - if id == nil { - return "" - } - return C.GoString(id) -} - -/* Get the thread ID of 'message'. - * - * The returned string belongs to 'message' and as such, should not be - * modified by the caller and will only be valid for as long as the - * message is valid, (for example, until the user calls - * notmuch_message_destroy on 'message' or until a query from which it - * derived is destroyed). - * - * This function will not return NULL since Notmuch ensures that every - * message belongs to a single thread. - */ -func (self *Message) GetThreadId() string { - - if self.message == nil { - return "" - } - id := C.notmuch_message_get_thread_id(self.message) - // we dont own id - // defer C.free(unsafe.Pointer(id)) - - if id == nil { - return "" - } - - return C.GoString(id) -} - -/* Get a notmuch_messages_t iterator for all of the replies to - * 'message'. - * - * Note: This call only makes sense if 'message' was ultimately - * obtained from a notmuch_thread_t object, (such as by coming - * directly from the result of calling notmuch_thread_get_ - * toplevel_messages or by any number of subsequent - * calls to notmuch_message_get_replies). - * - * If 'message' was obtained through some non-thread means, (such as - * by a call to notmuch_query_search_messages), then this function - * will return NULL. - * - * If there are no replies to 'message', this function will return - * NULL. (Note that notmuch_messages_valid will accept that NULL - * value as legitimate, and simply return FALSE for it.) - */ -func (self *Message) GetReplies() *Messages { - if self.message == nil { - return nil - } - msgs := C.notmuch_message_get_replies(self.message) - if msgs == nil { - return nil - } - return &Messages{messages:msgs} -} - -/* Get a filename for the email corresponding to 'message'. - * - * The returned filename is an absolute filename, (the initial - * component will match notmuch_database_get_path() ). - * - * The returned string belongs to the message so should not be - * modified or freed by the caller (nor should it be referenced after - * the message is destroyed). - * - * Note: If this message corresponds to multiple files in the mail - * store, (that is, multiple files contain identical message IDs), - * this function will arbitrarily return a single one of those - * filenames. - */ -func (self *Message) GetFileName() string { - if self.message == nil { - return "" - } - fname := C.notmuch_message_get_filename(self.message) - // we dont own fname - // defer C.free(unsafe.Pointer(fname)) - - if fname == nil { - return "" - } - - return C.GoString(fname) -} - -type Flag C.notmuch_message_flag_t -const ( - MESSAGE_FLAG_MATCH Flag = 0 -) - -/* Get a value of a flag for the email corresponding to 'message'. */ -func (self *Message) GetFlag(flag Flag) bool { - if self.message == nil { - return false - } - v := C.notmuch_message_get_flag(self.message, C.notmuch_message_flag_t(flag)) - if v == 0 { - return false - } - return true -} - -/* Set a value of a flag for the email corresponding to 'message'. */ -func (self *Message) SetFlag(flag Flag, value bool) { - if self.message == nil { - return - } - var v C.notmuch_bool_t = 0 - if value { - v = 1 - } - C.notmuch_message_set_flag(self.message, C.notmuch_message_flag_t(flag), v) -} - -// TODO: wrap notmuch_message_get_date - -/* Get the value of the specified header from 'message'. - * - * The value will be read from the actual message file, not from the - * notmuch database. The header name is case insensitive. - * - * The returned string belongs to the message so should not be - * modified or freed by the caller (nor should it be referenced after - * the message is destroyed). - * - * Returns an empty string ("") if the message does not contain a - * header line matching 'header'. Returns NULL if any error occurs. - */ -func (self *Message) GetHeader(header string) string { - if self.message == nil { - return "" - } - - var c_header *C.char = C.CString(header) - defer C.free(unsafe.Pointer(c_header)) - - /* we dont own value */ - value := C.notmuch_message_get_header(self.message, c_header) - if value == nil { - return "" - } - - return C.GoString(value) -} - -/* Get the tags for 'message', returning a notmuch_tags_t object which - * can be used to iterate over all tags. - * - * The tags object is owned by the message and as such, will only be - * valid for as long as the message is valid, (which is until the - * query from which it derived is destroyed). - * - * Typical usage might be: - * - * notmuch_message_t *message; - * notmuch_tags_t *tags; - * const char *tag; - * - * message = notmuch_database_find_message (database, message_id); - * - * for (tags = notmuch_message_get_tags (message); - * notmuch_tags_valid (tags); - * notmuch_result_move_to_next (tags)) - * { - * tag = notmuch_tags_get (tags); - * .... - * } - * - * notmuch_message_destroy (message); - * - * Note that there's no explicit destructor needed for the - * notmuch_tags_t object. (For consistency, we do provide a - * notmuch_tags_destroy function, but there's no good reason to call - * it if the message is about to be destroyed). - */ -func (self *Message) GetTags() *Tags { - if self.message == nil { - return nil - } - tags := C.notmuch_message_get_tags(self.message) - if tags == nil { - return nil - } - return &Tags{tags:tags} -} - -/* The longest possible tag value. */ -const TAG_MAX = 200 - -/* Add a tag to the given message. - * - * Return value: - * - * NOTMUCH_STATUS_SUCCESS: Tag successfully added to message - * - * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL - * - * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long - * (exceeds NOTMUCH_TAG_MAX) - * - * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only - * mode so message cannot be modified. - */ -func (self *Message) AddTag(tag string) Status { - if self.message == nil { - return STATUS_NULL_POINTER - } - c_tag := C.CString(tag) - defer C.free(unsafe.Pointer(c_tag)) - - return Status(C.notmuch_message_add_tag(self.message, c_tag)) -} - -/* Remove a tag from the given message. - * - * Return value: - * - * NOTMUCH_STATUS_SUCCESS: Tag successfully removed from message - * - * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL - * - * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long - * (exceeds NOTMUCH_TAG_MAX) - * - * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only - * mode so message cannot be modified. - */ -func (self *Message) RemoveTag(tag string) Status { - if self.message == nil { - return STATUS_NULL_POINTER - } - c_tag := C.CString(tag) - defer C.free(unsafe.Pointer(c_tag)) - - return Status(C.notmuch_message_remove_tag(self.message, c_tag)) -} - -/* Remove all tags from the given message. - * - * See notmuch_message_freeze for an example showing how to safely - * replace tag values. - * - * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only - * mode so message cannot be modified. - */ -func (self *Message) RemoveAllTags() Status { - if self.message == nil { - return STATUS_NULL_POINTER - } - return Status(C.notmuch_message_remove_all_tags(self.message)) -} - -/* Freeze the current state of 'message' within the database. - * - * This means that changes to the message state, (via - * notmuch_message_add_tag, notmuch_message_remove_tag, and - * notmuch_message_remove_all_tags), will not be committed to the - * database until the message is thawed with notmuch_message_thaw. - * - * Multiple calls to freeze/thaw are valid and these calls will - * "stack". That is there must be as many calls to thaw as to freeze - * before a message is actually thawed. - * - * The ability to do freeze/thaw allows for safe transactions to - * change tag values. For example, explicitly setting a message to - * have a given set of tags might look like this: - * - * notmuch_message_freeze (message); - * - * notmuch_message_remove_all_tags (message); - * - * for (i = 0; i < NUM_TAGS; i++) - * notmuch_message_add_tag (message, tags[i]); - * - * notmuch_message_thaw (message); - * - * With freeze/thaw used like this, the message in the database is - * guaranteed to have either the full set of original tag values, or - * the full set of new tag values, but nothing in between. - * - * Imagine the example above without freeze/thaw and the operation - * somehow getting interrupted. This could result in the message being - * left with no tags if the interruption happened after - * notmuch_message_remove_all_tags but before notmuch_message_add_tag. - * - * Return value: - * - * NOTMUCH_STATUS_SUCCESS: Message successfully frozen. - * - * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only - * mode so message cannot be modified. - */ -func (self *Message) Freeze() Status { - if self.message == nil { - return STATUS_NULL_POINTER - } - return Status(C.notmuch_message_freeze(self.message)) -} - -/* Thaw the current 'message', synchronizing any changes that may have - * occurred while 'message' was frozen into the notmuch database. - * - * See notmuch_message_freeze for an example of how to use this - * function to safely provide tag changes. - * - * Multiple calls to freeze/thaw are valid and these calls with - * "stack". That is there must be as many calls to thaw as to freeze - * before a message is actually thawed. - * - * Return value: - * - * NOTMUCH_STATUS_SUCCESS: Message successfully thawed, (or at least - * its frozen count has successfully been reduced by 1). - * - * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: An attempt was made to thaw - * an unfrozen message. That is, there have been an unbalanced - * number of calls to notmuch_message_freeze and - * notmuch_message_thaw. - */ -func (self *Message) Thaw() Status { - if self.message == nil { - return STATUS_NULL_POINTER - } - - return Status(C.notmuch_message_thaw(self.message)) -} - -/* Destroy a notmuch_message_t object. - * - * It can be useful to call this function in the case of a single - * query object with many messages in the result, (such as iterating - * over the entire database). Otherwise, it's fine to never call this - * function and there will still be no memory leaks. (The memory from - * the messages get reclaimed when the containing query is destroyed.) - */ -func (self *Message) Destroy() { - if self.message == nil { - return - } - C.notmuch_message_destroy(self.message) -} - -/* Is the given 'tags' iterator pointing at a valid tag. - * - * When this function returns TRUE, notmuch_tags_get will return a - * valid string. Whereas when this function returns FALSE, - * notmuch_tags_get will return NULL. - * - * See the documentation of notmuch_message_get_tags for example code - * showing how to iterate over a notmuch_tags_t object. - */ -func (self *Tags) Valid() bool { - if self.tags == nil { - return false - } - v := C.notmuch_tags_valid(self.tags) - if v == 0 { - return false - } - return true -} - -/* Get the current tag from 'tags' as a string. - * - * Note: The returned string belongs to 'tags' and has a lifetime - * identical to it (and the query to which it ultimately belongs). - * - * See the documentation of notmuch_message_get_tags for example code - * showing how to iterate over a notmuch_tags_t object. - */ -func (self *Tags) Get() string { - if self.tags == nil { - return "" - } - s := C.notmuch_tags_get(self.tags) - // we dont own 's' - - return C.GoString(s) -} -func (self *Tags) String() string { - return self.Get() -} - -/* Move the 'tags' iterator to the next tag. - * - * If 'tags' is already pointing at the last tag then the iterator - * will be moved to a point just beyond that last tag, (where - * notmuch_tags_valid will return FALSE and notmuch_tags_get will - * return NULL). - * - * See the documentation of notmuch_message_get_tags for example code - * showing how to iterate over a notmuch_tags_t object. - */ -func (self *Tags) MoveToNext() { - if self.tags == nil { - return - } - C.notmuch_tags_move_to_next(self.tags) -} - -/* Destroy a notmuch_tags_t object. - * - * It's not strictly necessary to call this function. All memory from - * the notmuch_tags_t object will be reclaimed when the containing - * message or query objects are destroyed. - */ -func (self *Tags) Destroy() { - if self.tags == nil { - return - } - C.notmuch_tags_destroy(self.tags) -} - -// TODO: wrap notmuch_directory_ - -/* Destroy a notmuch_directory_t object. */ -func (self *Directory) Destroy() { - if self.dir == nil { - return - } - C.notmuch_directory_destroy(self.dir) -} - -// TODO: wrap notmuch_filenames_ - -/* Destroy a notmuch_filenames_t object. - * - * It's not strictly necessary to call this function. All memory from - * the notmuch_filenames_t object will be reclaimed when the - * containing directory object is destroyed. - * - * It is acceptable to pass NULL for 'filenames', in which case this - * function will do nothing. - */ -func (self *Filenames) Destroy() { - if self.fnames == nil { - return - } - C.notmuch_filenames_destroy(self.fnames) -} -/* EOF */ diff --git a/bindings/go/src/notmuch-addrlookup/addrlookup.go b/bindings/go/src/notmuch-addrlookup/addrlookup.go new file mode 100644 index 0000000..16958e5 --- /dev/null +++ b/bindings/go/src/notmuch-addrlookup/addrlookup.go @@ -0,0 +1,259 @@ +package main + +// stdlib imports +import "os" +import "path" +import "log" +import "fmt" +import "regexp" +import "strings" +import "sort" + +// 3rd-party imports +import "notmuch" +import "github.com/kless/goconfig/config" + +type mail_addr_freq struct { + addr string + count [3]uint +} + +type frequencies map[string]uint + +/* Used to sort the email addresses from most to least used */ +func sort_by_freq(m1, m2 *mail_addr_freq) int { + if (m1.count[0] == m2.count[0] && + m1.count[1] == m2.count[1] && + m1.count[2] == m2.count[2]) { + return 0 + } + + if (m1.count[0] > m2.count[0] || + m1.count[0] == m2.count[0] && + m1.count[1] > m2.count[1] || + m1.count[0] == m2.count[0] && + m1.count[1] == m2.count[1] && + m1.count[2] > m2.count[2]) { + return -1 + } + + return 1 +} + +type maddresses []*mail_addr_freq + +func (self *maddresses) Len() int { + return len(*self) +} + +func (self *maddresses) Less(i,j int) bool { + m1 := (*self)[i] + m2 := (*self)[j] + v := sort_by_freq(m1, m2) + if v<=0 { + return true + } + return false +} + +func (self *maddresses) Swap(i,j int) { + (*self)[i], (*self)[j] = (*self)[j], (*self)[i] +} + +// find most frequent real name for each mail address +func frequent_fullname(freqs frequencies) string { + var maxfreq uint = 0 + fullname := "" + freqs_sz := len(freqs) + + for mail,freq := range freqs { + if (freq > maxfreq && mail != "") || freqs_sz == 1 { + // only use the entry if it has a real name + // or if this is the only entry + maxfreq = freq + fullname = mail + } + } + return fullname +} + +func addresses_by_frequency(msgs *notmuch.Messages, name string, pass uint, addr_to_realname *map[string]*frequencies) *frequencies { + + freqs := make(frequencies) + + pattern := `\s*(("(\.|[^"])*"|[^,])*?)` + // pattern := "\\s*((\\\"(\\\\.|[^\\\\\"])*\\\"|[^,])*" + + // "\\b\\w+([-+.]\\w+)*\\@\\w+[-\\.\\w]*\\.([-\\.\\w]+)*\\w\\b)>?)" + pattern = `.*` + strings.ToLower(name) + `.*` + var re *regexp.Regexp = nil + var err os.Error = nil + if re,err = regexp.Compile(pattern); err != nil { + log.Printf("error: %v\n", err) + return &freqs + } + + headers := []string{"from"} + if pass == 1 { + headers = append(headers, "to", "cc", "bcc") + } + + for ;msgs.Valid();msgs.MoveToNext() { + msg := msgs.Get() + //println("==> msg [", msg.GetMessageId(), "]") + for _,header := range headers { + froms := strings.ToLower(msg.GetHeader(header)) + //println(" froms: ["+froms+"]") + for _,from := range strings.Split(froms, ",", -1) { + from = strings.Trim(from, " ") + match := re.FindString(from) + //println(" -> match: ["+match+"]") + occ,ok := freqs[match] + if !ok { + freqs[match] = 0 + occ = 0 + } + freqs[match] = occ+1 + } + } + } + return &freqs +} + +func search_address_passes(queries [3]*notmuch.Query, name string) []string { + var val []string + addr_freq := make(map[string]*mail_addr_freq) + addr_to_realname := make(map[string]*frequencies) + + var pass uint = 0 // 0-based + for _,query := range queries { + if query == nil { + //println("**warning: idx [",idx,"] contains a nil query") + continue + } + msgs := query.SearchMessages() + ht := addresses_by_frequency(msgs, name, pass, &addr_to_realname) + for addr, count := range *ht { + freq,ok := addr_freq[addr] + if !ok { + freq = &mail_addr_freq{addr:addr, count:[3]uint{0,0,0}} + } + freq.count[pass] = count + addr_freq[addr] = freq + } + msgs.Destroy() + pass += 1 + } + + addrs := make(maddresses, len(addr_freq)) + { + iaddr := 0 + for _, freq := range addr_freq { + addrs[iaddr] = freq + iaddr += 1 + } + } + sort.Sort(&addrs) + + for _,addr := range addrs { + freqs,ok := addr_to_realname[addr.addr] + if ok { + val = append(val, frequent_fullname(*freqs)) + } else { + val = append(val, addr.addr) + } + } + //println("val:",val) + return val +} + +type address_matcher struct { + // the notmuch database + db *notmuch.Database + // full path of the notmuch database + user_db_path string + // user primary email + user_primary_email string + // user tag to mark from addresses as in the address book + user_addrbook_tag string +} + +func new_address_matcher() *address_matcher { + var cfg *config.Config + var err os.Error + + // honor NOTMUCH_CONFIG + home := os.Getenv("NOTMUCH_CONFIG") + if home == "" { + home = os.Getenv("HOME") + } + + if cfg,err = config.ReadDefault(path.Join(home, ".notmuch-config")); err != nil { + log.Fatalf("error loading config file:",err) + } + + db_path,_ := cfg.String("database", "path") + primary_email,_ := cfg.String("user", "primary_email") + addrbook_tag,err := cfg.String("user", "addrbook_tag") + if err != nil { + addrbook_tag = "addressbook" + } + + self := &address_matcher{db:nil, + user_db_path:db_path, + user_primary_email:primary_email, + user_addrbook_tag:addrbook_tag} + return self +} + +func (self *address_matcher) run(name string) { + queries := [3]*notmuch.Query{} + + // open the database + self.db = notmuch.OpenDatabase(self.user_db_path, + notmuch.DATABASE_MODE_READ_ONLY) + + // pass 1: look at all from: addresses with the address book tag + query := "tag:" + self.user_addrbook_tag + if name != "" { + query = query + " and from:" + name + "*" + } + queries[0] = self.db.CreateQuery(query) + + // pass 2: look at all to: addresses sent from our primary mail + query = "" + if name != "" { + query = "to:"+name+"*" + } + if self.user_primary_email != "" { + query = query + " from:" + self.user_primary_email + } + queries[1] = self.db.CreateQuery(query) + + // if that leads only to a few hits, we check every from too + if queries[0].CountMessages() + queries[1].CountMessages() < 10 { + query = "" + if name != "" { + query = "from:"+name+"*" + } + queries[2] = self.db.CreateQuery(query) + } + + // actually retrieve and sort addresses + results := search_address_passes(queries, name) + for _,v := range results { + if v != "" && v != "\n" { + fmt.Println(v) + } + } + return +} + +func main() { + //fmt.Println("args:",os.Args) + app := new_address_matcher() + name := "" + if len(os.Args) > 1 { + name = os.Args[1] + } + app.run(name) +} \ No newline at end of file diff --git a/bindings/go/src/notmuch/notmuch.go b/bindings/go/src/notmuch/notmuch.go new file mode 100644 index 0000000..de9de23 --- /dev/null +++ b/bindings/go/src/notmuch/notmuch.go @@ -0,0 +1,1124 @@ +// Wrapper around the notmuch library + +package notmuch + +/* +#include +#include +#include +#include "notmuch.h" +*/ +import "C" +import "unsafe" + +// Status codes used for the return values of most functions +type Status C.notmuch_status_t +const ( + STATUS_SUCCESS Status = 0 + STATUS_OUT_OF_MEMORY + STATUS_READ_ONLY_DATABASE + STATUS_XAPIAN_EXCEPTION + STATUS_FILE_ERROR + STATUS_FILE_NOT_EMAIL + STATUS_DUPLICATE_MESSAGE_ID + STATUS_NULL_POINTER + STATUS_TAG_TOO_LONG + STATUS_UNBALANCED_FREEZE_THAW + + STATUS_LAST_STATUS +) + +func (self Status) String() string { + var p *C.char + + // p is read-only + p = C.notmuch_status_to_string(C.notmuch_status_t(self)) + if p != nil { + s := C.GoString(p) + return s + } + return "" +} + +/* Various opaque data types. For each notmuch__t see the various + * notmuch_ functions below. */ + +type Database struct { + db *C.notmuch_database_t +} + +type Query struct { + query *C.notmuch_query_t +} + +type Threads struct { + threads *C.notmuch_threads_t +} + +type Thread struct { + thread *C.notmuch_thread_t +} + +type Messages struct { + messages *C.notmuch_messages_t +} + +type Message struct { + message *C.notmuch_message_t +} + +type Tags struct { + tags *C.notmuch_tags_t +} + +type Directory struct { + dir *C.notmuch_directory_t +} + +type Filenames struct { + fnames *C.notmuch_filenames_t +} + +type DatabaseMode C.notmuch_database_mode_t +const ( + DATABASE_MODE_READ_ONLY DatabaseMode = 0 + DATABASE_MODE_READ_WRITE +) + +// Create a new, empty notmuch database located at 'path' +func NewDatabase(path string) *Database { + + var c_path *C.char = C.CString(path) + defer C.free(unsafe.Pointer(c_path)) + + if c_path == nil { + return nil + } + + self := &Database{db:nil} + self.db = C.notmuch_database_create(c_path) + if self.db == nil { + return nil + } + return self +} + +/* Open an existing notmuch database located at 'path'. + * + * The database should have been created at some time in the past, + * (not necessarily by this process), by calling + * notmuch_database_create with 'path'. By default the database should be + * opened for reading only. In order to write to the database you need to + * pass the NOTMUCH_DATABASE_MODE_READ_WRITE mode. + * + * An existing notmuch database can be identified by the presence of a + * directory named ".notmuch" below 'path'. + * + * The caller should call notmuch_database_destroy when finished with + * this database. + * + * In case of any failure, this function returns NULL, (after printing + * an error message on stderr). + */ +func OpenDatabase(path string, mode DatabaseMode) *Database { + + var c_path *C.char = C.CString(path) + defer C.free(unsafe.Pointer(c_path)) + + if c_path == nil { + return nil + } + + self := &Database{db:nil} + self.db = C.notmuch_database_open(c_path, C.notmuch_database_mode_t(mode)) + if self.db == nil { + return nil + } + return self +} + +/* Close the given notmuch database, freeing all associated + * resources. See notmuch_database_open. */ +func (self *Database) Close() { + C.notmuch_database_destroy(self.db) +} + +/* Return the database path of the given database. + */ +func (self *Database) GetPath() string { + + /* The return value is a string owned by notmuch so should not be + * modified nor freed by the caller. */ + var p *C.char = C.notmuch_database_get_path(self.db) + if p != nil { + s := C.GoString(p) + return s + } + return "" +} + +/* Return the database format version of the given database. */ +func (self *Database) GetVersion() uint { + return uint(C.notmuch_database_get_version(self.db)) +} + +/* Does this database need to be upgraded before writing to it? + * + * If this function returns TRUE then no functions that modify the + * database (notmuch_database_add_message, notmuch_message_add_tag, + * notmuch_directory_set_mtime, etc.) will work unless the function + * notmuch_database_upgrade is called successfully first. */ +func (self *Database) NeedsUpgrade() bool { + do_upgrade := C.notmuch_database_needs_upgrade(self.db) + if do_upgrade == 0 { + return false + } + return true +} + +// TODO: notmuch_database_upgrade + + +/* Retrieve a directory object from the database for 'path'. + * + * Here, 'path' should be a path relative to the path of 'database' + * (see notmuch_database_get_path), or else should be an absolute path + * with initial components that match the path of 'database'. + * + * Can return NULL if a Xapian exception occurs. + */ +func (self *Database) GetDirectory(path string) *Directory { + var c_path *C.char = C.CString(path) + defer C.free(unsafe.Pointer(c_path)) + + if c_path == nil { + return nil + } + + c_dir := C.notmuch_database_get_directory(self.db, c_path) + if c_dir == nil { + return nil + } + return &Directory{dir:c_dir} +} + +/* Add a new message to the given notmuch database. + * + * Here,'filename' should be a path relative to the path of + * 'database' (see notmuch_database_get_path), or else should be an + * absolute filename with initial components that match the path of + * 'database'. + * + * The file should be a single mail message (not a multi-message mbox) + * that is expected to remain at its current location, (since the + * notmuch database will reference the filename, and will not copy the + * entire contents of the file. + * + * If 'message' is not NULL, then, on successful return '*message' + * will be initialized to a message object that can be used for things + * such as adding tags to the just-added message. The user should call + * notmuch_message_destroy when done with the message. On any failure + * '*message' will be set to NULL. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Message successfully added to database. + * + * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred, + * message not added. + * + * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message + * ID as another message already in the database. The new + * filename was successfully added to the message in the database + * (if not already present). + * + * NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the + * file, (such as permission denied, or file not found, + * etc.). Nothing added to the database. + * + * NOTMUCH_STATUS_FILE_NOT_EMAIL: the contents of filename don't look + * like an email message. Nothing added to the database. + * + * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only + * mode so no message can be added. + */ +func +(self *Database) AddMessage(fname string) (*Message, Status) { + var c_fname *C.char = C.CString(fname) + defer C.free(unsafe.Pointer(c_fname)) + + if c_fname == nil { + return nil, STATUS_OUT_OF_MEMORY + } + + var c_msg *C.notmuch_message_t = new(C.notmuch_message_t) + st := Status(C.notmuch_database_add_message(self.db, c_fname, &c_msg)) + + return &Message{message:c_msg}, st +} + +/* Remove a message from the given notmuch database. + * + * Note that only this particular filename association is removed from + * the database. If the same message (as determined by the message ID) + * is still available via other filenames, then the message will + * persist in the database for those filenames. When the last filename + * is removed for a particular message, the database content for that + * message will be entirely removed. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: The last filename was removed and the + * message was removed from the database. + * + * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred, + * message not removed. + * + * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: This filename was removed but + * the message persists in the database with at least one other + * filename. + * + * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only + * mode so no message can be removed. + */ +func (self *Database) RemoveMessage(fname string) Status { + + var c_fname *C.char = C.CString(fname) + defer C.free(unsafe.Pointer(c_fname)) + + if c_fname == nil { + return STATUS_OUT_OF_MEMORY + } + + st := C.notmuch_database_remove_message(self.db, c_fname) + return Status(st) +} + +/* Find a message with the given message_id. + * + * If the database contains a message with the given message_id, then + * a new notmuch_message_t object is returned. The caller should call + * notmuch_message_destroy when done with the message. + * + * This function returns NULL in the following situations: + * + * * No message is found with the given message_id + * * An out-of-memory situation occurs + * * A Xapian exception occurs + */ +func (self *Database) FindMessage(message_id string) (*Message, Status) { + + var c_msg_id *C.char = C.CString(message_id) + defer C.free(unsafe.Pointer(c_msg_id)) + + if c_msg_id == nil { + return nil, STATUS_OUT_OF_MEMORY + } + + msg := &Message{message:nil} + st := Status(C.notmuch_database_find_message(self.db, c_msg_id, &msg.message)) + if st != STATUS_SUCCESS { + return nil, st + } + return msg, st +} + +/* Return a list of all tags found in the database. + * + * This function creates a list of all tags found in the database. The + * resulting list contains all tags from all messages found in the database. + * + * On error this function returns NULL. + */ +func (self *Database) GetAllTags() *Tags { + tags := C.notmuch_database_get_all_tags(self.db) + if tags == nil { + return nil + } + return &Tags{tags:tags} +} + +/* Create a new query for 'database'. + * + * Here, 'database' should be an open database, (see + * notmuch_database_open and notmuch_database_create). + * + * For the query string, we'll document the syntax here more + * completely in the future, but it's likely to be a specialized + * version of the general Xapian query syntax: + * + * http://xapian.org/docs/queryparser.html + * + * As a special case, passing either a length-zero string, (that is ""), + * or a string consisting of a single asterisk (that is "*"), will + * result in a query that returns all messages in the database. + * + * See notmuch_query_set_sort for controlling the order of results. + * See notmuch_query_search_messages and notmuch_query_search_threads + * to actually execute the query. + * + * User should call notmuch_query_destroy when finished with this + * query. + * + * Will return NULL if insufficient memory is available. + */ +func (self *Database) CreateQuery(query string) *Query { + + var c_query *C.char = C.CString(query) + defer C.free(unsafe.Pointer(c_query)) + + if c_query == nil { + return nil + } + + q := C.notmuch_query_create(self.db, c_query) + if q == nil { + return nil + } + return &Query{query:q} +} + +/* Sort values for notmuch_query_set_sort */ +type Sort C.notmuch_sort_t +const ( + SORT_OLDEST_FIRST Sort = 0 + SORT_NEWEST_FIRST + SORT_MESSAGE_ID + SORT_UNSORTED +) + +/* Return the query_string of this query. See notmuch_query_create. */ +func (self *Query) String() string { + // FIXME: do we own 'q' or not ? + q := C.notmuch_query_get_query_string(self.query) + //defer C.free(unsafe.Pointer(q)) + + if q != nil { + s := C.GoString(q) + return s + } + + return "" +} + +/* Specify the sorting desired for this query. */ +func (self *Query) SetSort(sort Sort) { + C.notmuch_query_set_sort(self.query, C.notmuch_sort_t(sort)) +} + +/* Return the sort specified for this query. See notmuch_query_set_sort. */ +func (self *Query) GetSort() Sort { + return Sort(C.notmuch_query_get_sort(self.query)) +} + +/* Execute a query for threads, returning a notmuch_threads_t object + * which can be used to iterate over the results. The returned threads + * object is owned by the query and as such, will only be valid until + * notmuch_query_destroy. + * + * Typical usage might be: + * + * notmuch_query_t *query; + * notmuch_threads_t *threads; + * notmuch_thread_t *thread; + * + * query = notmuch_query_create (database, query_string); + * + * for (threads = notmuch_query_search_threads (query); + * notmuch_threads_valid (threads); + * notmuch_threads_move_to_next (threads)) + * { + * thread = notmuch_threads_get (threads); + * .... + * notmuch_thread_destroy (thread); + * } + * + * notmuch_query_destroy (query); + * + * Note: If you are finished with a thread before its containing + * query, you can call notmuch_thread_destroy to clean up some memory + * sooner (as in the above example). Otherwise, if your thread objects + * are long-lived, then you don't need to call notmuch_thread_destroy + * and all the memory will still be reclaimed when the query is + * destroyed. + * + * Note that there's no explicit destructor needed for the + * notmuch_threads_t object. (For consistency, we do provide a + * notmuch_threads_destroy function, but there's no good reason + * to call it if the query is about to be destroyed). + * + * If a Xapian exception occurs this function will return NULL. + */ +func (self *Query) SearchThreads() *Threads { + threads := C.notmuch_query_search_threads(self.query) + if threads == nil { + return nil + } + return &Threads{threads:threads} +} + +/* Execute a query for messages, returning a notmuch_messages_t object + * which can be used to iterate over the results. The returned + * messages object is owned by the query and as such, will only be + * valid until notmuch_query_destroy. + * + * Typical usage might be: + * + * notmuch_query_t *query; + * notmuch_messages_t *messages; + * notmuch_message_t *message; + * + * query = notmuch_query_create (database, query_string); + * + * for (messages = notmuch_query_search_messages (query); + * notmuch_messages_valid (messages); + * notmuch_messages_move_to_next (messages)) + * { + * message = notmuch_messages_get (messages); + * .... + * notmuch_message_destroy (message); + * } + * + * notmuch_query_destroy (query); + * + * Note: If you are finished with a message before its containing + * query, you can call notmuch_message_destroy to clean up some memory + * sooner (as in the above example). Otherwise, if your message + * objects are long-lived, then you don't need to call + * notmuch_message_destroy and all the memory will still be reclaimed + * when the query is destroyed. + * + * Note that there's no explicit destructor needed for the + * notmuch_messages_t object. (For consistency, we do provide a + * notmuch_messages_destroy function, but there's no good + * reason to call it if the query is about to be destroyed). + * + * If a Xapian exception occurs this function will return NULL. + */ +func (self *Query) SearchMessages() *Messages { + msgs := C.notmuch_query_search_messages(self.query) + if msgs == nil { + return nil + } + return &Messages{messages:msgs} +} + +/* Destroy a notmuch_query_t along with any associated resources. + * + * This will in turn destroy any notmuch_threads_t and + * notmuch_messages_t objects generated by this query, (and in + * turn any notmuch_thread_t and notmuch_message_t objects generated + * from those results, etc.), if such objects haven't already been + * destroyed. + */ +func (self *Query) Destroy() { + if self.query != nil { + C.notmuch_query_destroy(self.query) + } +} + +/* Return an estimate of the number of messages matching a search + * + * This function performs a search and returns Xapian's best + * guess as to number of matching messages. + * + * If a Xapian exception occurs, this function may return 0 (after + * printing a message). + */ +func (self *Query) CountMessages() uint { + return uint(C.notmuch_query_count_messages(self.query)) +} + +// TODO: wrap threads and thread + +/* Is the given 'threads' iterator pointing at a valid thread. + * + * When this function returns TRUE, notmuch_threads_get will return a + * valid object. Whereas when this function returns FALSE, + * notmuch_threads_get will return NULL. + * + * See the documentation of notmuch_query_search_threads for example + * code showing how to iterate over a notmuch_threads_t object. + */ +func (self *Threads) Valid() bool { + if self.threads == nil { + return false + } + valid := C.notmuch_threads_valid(self.threads) + if valid == 0 { + return false + } + return true +} + +/* Destroy a notmuch_threads_t object. + * + * It's not strictly necessary to call this function. All memory from + * the notmuch_threads_t object will be reclaimed when the + * containg query object is destroyed. + */ +func (self *Threads) Destroy() { + if self.threads != nil { + C.notmuch_threads_destroy(self.threads) + } +} + +/* Is the given 'messages' iterator pointing at a valid message. + * + * When this function returns TRUE, notmuch_messages_get will return a + * valid object. Whereas when this function returns FALSE, + * notmuch_messages_get will return NULL. + * + * See the documentation of notmuch_query_search_messages for example + * code showing how to iterate over a notmuch_messages_t object. + */ +func (self *Messages) Valid() bool { + if self.messages == nil { + return false + } + valid := C.notmuch_messages_valid(self.messages) + if valid == 0 { + return false + } + return true +} + +/* Get the current message from 'messages' as a notmuch_message_t. + * + * Note: The returned message belongs to 'messages' and has a lifetime + * identical to it (and the query to which it belongs). + * + * See the documentation of notmuch_query_search_messages for example + * code showing how to iterate over a notmuch_messages_t object. + * + * If an out-of-memory situation occurs, this function will return + * NULL. + */ +func (self *Messages) Get() *Message { + if self.messages == nil { + return nil + } + msg := C.notmuch_messages_get(self.messages) + if msg == nil { + return nil + } + return &Message{message:msg} +} + +/* Move the 'messages' iterator to the next message. + * + * If 'messages' is already pointing at the last message then the + * iterator will be moved to a point just beyond that last message, + * (where notmuch_messages_valid will return FALSE and + * notmuch_messages_get will return NULL). + * + * See the documentation of notmuch_query_search_messages for example + * code showing how to iterate over a notmuch_messages_t object. + */ +func (self *Messages) MoveToNext() { + if self.messages == nil { + return + } + C.notmuch_messages_move_to_next(self.messages) +} + +/* Destroy a notmuch_messages_t object. + * + * It's not strictly necessary to call this function. All memory from + * the notmuch_messages_t object will be reclaimed when the containing + * query object is destroyed. + */ +func (self *Messages) Destroy() { + if self.messages != nil { + C.notmuch_messages_destroy(self.messages) + } +} + +/* Return a list of tags from all messages. + * + * The resulting list is guaranteed not to contain duplicated tags. + * + * WARNING: You can no longer iterate over messages after calling this + * function, because the iterator will point at the end of the list. + * We do not have a function to reset the iterator yet and the only + * way how you can iterate over the list again is to recreate the + * message list. + * + * The function returns NULL on error. + */ +func (self *Messages) CollectTags() *Tags { + if self.messages == nil { + return nil + } + tags := C.notmuch_messages_collect_tags(self.messages) + if tags == nil { + return nil + } + return &Tags{tags:tags} +} + +/* Get the message ID of 'message'. + * + * The returned string belongs to 'message' and as such, should not be + * modified by the caller and will only be valid for as long as the + * message is valid, (which is until the query from which it derived + * is destroyed). + * + * This function will not return NULL since Notmuch ensures that every + * message has a unique message ID, (Notmuch will generate an ID for a + * message if the original file does not contain one). + */ +func (self *Message) GetMessageId() string { + + if self.message == nil { + return "" + } + id := C.notmuch_message_get_message_id(self.message) + // we dont own id + // defer C.free(unsafe.Pointer(id)) + if id == nil { + return "" + } + return C.GoString(id) +} + +/* Get the thread ID of 'message'. + * + * The returned string belongs to 'message' and as such, should not be + * modified by the caller and will only be valid for as long as the + * message is valid, (for example, until the user calls + * notmuch_message_destroy on 'message' or until a query from which it + * derived is destroyed). + * + * This function will not return NULL since Notmuch ensures that every + * message belongs to a single thread. + */ +func (self *Message) GetThreadId() string { + + if self.message == nil { + return "" + } + id := C.notmuch_message_get_thread_id(self.message) + // we dont own id + // defer C.free(unsafe.Pointer(id)) + + if id == nil { + return "" + } + + return C.GoString(id) +} + +/* Get a notmuch_messages_t iterator for all of the replies to + * 'message'. + * + * Note: This call only makes sense if 'message' was ultimately + * obtained from a notmuch_thread_t object, (such as by coming + * directly from the result of calling notmuch_thread_get_ + * toplevel_messages or by any number of subsequent + * calls to notmuch_message_get_replies). + * + * If 'message' was obtained through some non-thread means, (such as + * by a call to notmuch_query_search_messages), then this function + * will return NULL. + * + * If there are no replies to 'message', this function will return + * NULL. (Note that notmuch_messages_valid will accept that NULL + * value as legitimate, and simply return FALSE for it.) + */ +func (self *Message) GetReplies() *Messages { + if self.message == nil { + return nil + } + msgs := C.notmuch_message_get_replies(self.message) + if msgs == nil { + return nil + } + return &Messages{messages:msgs} +} + +/* Get a filename for the email corresponding to 'message'. + * + * The returned filename is an absolute filename, (the initial + * component will match notmuch_database_get_path() ). + * + * The returned string belongs to the message so should not be + * modified or freed by the caller (nor should it be referenced after + * the message is destroyed). + * + * Note: If this message corresponds to multiple files in the mail + * store, (that is, multiple files contain identical message IDs), + * this function will arbitrarily return a single one of those + * filenames. + */ +func (self *Message) GetFileName() string { + if self.message == nil { + return "" + } + fname := C.notmuch_message_get_filename(self.message) + // we dont own fname + // defer C.free(unsafe.Pointer(fname)) + + if fname == nil { + return "" + } + + return C.GoString(fname) +} + +type Flag C.notmuch_message_flag_t +const ( + MESSAGE_FLAG_MATCH Flag = 0 +) + +/* Get a value of a flag for the email corresponding to 'message'. */ +func (self *Message) GetFlag(flag Flag) bool { + if self.message == nil { + return false + } + v := C.notmuch_message_get_flag(self.message, C.notmuch_message_flag_t(flag)) + if v == 0 { + return false + } + return true +} + +/* Set a value of a flag for the email corresponding to 'message'. */ +func (self *Message) SetFlag(flag Flag, value bool) { + if self.message == nil { + return + } + var v C.notmuch_bool_t = 0 + if value { + v = 1 + } + C.notmuch_message_set_flag(self.message, C.notmuch_message_flag_t(flag), v) +} + +// TODO: wrap notmuch_message_get_date + +/* Get the value of the specified header from 'message'. + * + * The value will be read from the actual message file, not from the + * notmuch database. The header name is case insensitive. + * + * The returned string belongs to the message so should not be + * modified or freed by the caller (nor should it be referenced after + * the message is destroyed). + * + * Returns an empty string ("") if the message does not contain a + * header line matching 'header'. Returns NULL if any error occurs. + */ +func (self *Message) GetHeader(header string) string { + if self.message == nil { + return "" + } + + var c_header *C.char = C.CString(header) + defer C.free(unsafe.Pointer(c_header)) + + /* we dont own value */ + value := C.notmuch_message_get_header(self.message, c_header) + if value == nil { + return "" + } + + return C.GoString(value) +} + +/* Get the tags for 'message', returning a notmuch_tags_t object which + * can be used to iterate over all tags. + * + * The tags object is owned by the message and as such, will only be + * valid for as long as the message is valid, (which is until the + * query from which it derived is destroyed). + * + * Typical usage might be: + * + * notmuch_message_t *message; + * notmuch_tags_t *tags; + * const char *tag; + * + * message = notmuch_database_find_message (database, message_id); + * + * for (tags = notmuch_message_get_tags (message); + * notmuch_tags_valid (tags); + * notmuch_result_move_to_next (tags)) + * { + * tag = notmuch_tags_get (tags); + * .... + * } + * + * notmuch_message_destroy (message); + * + * Note that there's no explicit destructor needed for the + * notmuch_tags_t object. (For consistency, we do provide a + * notmuch_tags_destroy function, but there's no good reason to call + * it if the message is about to be destroyed). + */ +func (self *Message) GetTags() *Tags { + if self.message == nil { + return nil + } + tags := C.notmuch_message_get_tags(self.message) + if tags == nil { + return nil + } + return &Tags{tags:tags} +} + +/* The longest possible tag value. */ +const TAG_MAX = 200 + +/* Add a tag to the given message. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Tag successfully added to message + * + * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL + * + * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long + * (exceeds NOTMUCH_TAG_MAX) + * + * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only + * mode so message cannot be modified. + */ +func (self *Message) AddTag(tag string) Status { + if self.message == nil { + return STATUS_NULL_POINTER + } + c_tag := C.CString(tag) + defer C.free(unsafe.Pointer(c_tag)) + + return Status(C.notmuch_message_add_tag(self.message, c_tag)) +} + +/* Remove a tag from the given message. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Tag successfully removed from message + * + * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL + * + * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long + * (exceeds NOTMUCH_TAG_MAX) + * + * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only + * mode so message cannot be modified. + */ +func (self *Message) RemoveTag(tag string) Status { + if self.message == nil { + return STATUS_NULL_POINTER + } + c_tag := C.CString(tag) + defer C.free(unsafe.Pointer(c_tag)) + + return Status(C.notmuch_message_remove_tag(self.message, c_tag)) +} + +/* Remove all tags from the given message. + * + * See notmuch_message_freeze for an example showing how to safely + * replace tag values. + * + * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only + * mode so message cannot be modified. + */ +func (self *Message) RemoveAllTags() Status { + if self.message == nil { + return STATUS_NULL_POINTER + } + return Status(C.notmuch_message_remove_all_tags(self.message)) +} + +/* Freeze the current state of 'message' within the database. + * + * This means that changes to the message state, (via + * notmuch_message_add_tag, notmuch_message_remove_tag, and + * notmuch_message_remove_all_tags), will not be committed to the + * database until the message is thawed with notmuch_message_thaw. + * + * Multiple calls to freeze/thaw are valid and these calls will + * "stack". That is there must be as many calls to thaw as to freeze + * before a message is actually thawed. + * + * The ability to do freeze/thaw allows for safe transactions to + * change tag values. For example, explicitly setting a message to + * have a given set of tags might look like this: + * + * notmuch_message_freeze (message); + * + * notmuch_message_remove_all_tags (message); + * + * for (i = 0; i < NUM_TAGS; i++) + * notmuch_message_add_tag (message, tags[i]); + * + * notmuch_message_thaw (message); + * + * With freeze/thaw used like this, the message in the database is + * guaranteed to have either the full set of original tag values, or + * the full set of new tag values, but nothing in between. + * + * Imagine the example above without freeze/thaw and the operation + * somehow getting interrupted. This could result in the message being + * left with no tags if the interruption happened after + * notmuch_message_remove_all_tags but before notmuch_message_add_tag. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Message successfully frozen. + * + * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only + * mode so message cannot be modified. + */ +func (self *Message) Freeze() Status { + if self.message == nil { + return STATUS_NULL_POINTER + } + return Status(C.notmuch_message_freeze(self.message)) +} + +/* Thaw the current 'message', synchronizing any changes that may have + * occurred while 'message' was frozen into the notmuch database. + * + * See notmuch_message_freeze for an example of how to use this + * function to safely provide tag changes. + * + * Multiple calls to freeze/thaw are valid and these calls with + * "stack". That is there must be as many calls to thaw as to freeze + * before a message is actually thawed. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Message successfully thawed, (or at least + * its frozen count has successfully been reduced by 1). + * + * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: An attempt was made to thaw + * an unfrozen message. That is, there have been an unbalanced + * number of calls to notmuch_message_freeze and + * notmuch_message_thaw. + */ +func (self *Message) Thaw() Status { + if self.message == nil { + return STATUS_NULL_POINTER + } + + return Status(C.notmuch_message_thaw(self.message)) +} + +/* Destroy a notmuch_message_t object. + * + * It can be useful to call this function in the case of a single + * query object with many messages in the result, (such as iterating + * over the entire database). Otherwise, it's fine to never call this + * function and there will still be no memory leaks. (The memory from + * the messages get reclaimed when the containing query is destroyed.) + */ +func (self *Message) Destroy() { + if self.message == nil { + return + } + C.notmuch_message_destroy(self.message) +} + +/* Is the given 'tags' iterator pointing at a valid tag. + * + * When this function returns TRUE, notmuch_tags_get will return a + * valid string. Whereas when this function returns FALSE, + * notmuch_tags_get will return NULL. + * + * See the documentation of notmuch_message_get_tags for example code + * showing how to iterate over a notmuch_tags_t object. + */ +func (self *Tags) Valid() bool { + if self.tags == nil { + return false + } + v := C.notmuch_tags_valid(self.tags) + if v == 0 { + return false + } + return true +} + +/* Get the current tag from 'tags' as a string. + * + * Note: The returned string belongs to 'tags' and has a lifetime + * identical to it (and the query to which it ultimately belongs). + * + * See the documentation of notmuch_message_get_tags for example code + * showing how to iterate over a notmuch_tags_t object. + */ +func (self *Tags) Get() string { + if self.tags == nil { + return "" + } + s := C.notmuch_tags_get(self.tags) + // we dont own 's' + + return C.GoString(s) +} +func (self *Tags) String() string { + return self.Get() +} + +/* Move the 'tags' iterator to the next tag. + * + * If 'tags' is already pointing at the last tag then the iterator + * will be moved to a point just beyond that last tag, (where + * notmuch_tags_valid will return FALSE and notmuch_tags_get will + * return NULL). + * + * See the documentation of notmuch_message_get_tags for example code + * showing how to iterate over a notmuch_tags_t object. + */ +func (self *Tags) MoveToNext() { + if self.tags == nil { + return + } + C.notmuch_tags_move_to_next(self.tags) +} + +/* Destroy a notmuch_tags_t object. + * + * It's not strictly necessary to call this function. All memory from + * the notmuch_tags_t object will be reclaimed when the containing + * message or query objects are destroyed. + */ +func (self *Tags) Destroy() { + if self.tags == nil { + return + } + C.notmuch_tags_destroy(self.tags) +} + +// TODO: wrap notmuch_directory_ + +/* Destroy a notmuch_directory_t object. */ +func (self *Directory) Destroy() { + if self.dir == nil { + return + } + C.notmuch_directory_destroy(self.dir) +} + +// TODO: wrap notmuch_filenames_ + +/* Destroy a notmuch_filenames_t object. + * + * It's not strictly necessary to call this function. All memory from + * the notmuch_filenames_t object will be reclaimed when the + * containing directory object is destroyed. + * + * It is acceptable to pass NULL for 'filenames', in which case this + * function will do nothing. + */ +func (self *Filenames) Destroy() { + if self.fnames == nil { + return + } + C.notmuch_filenames_destroy(self.fnames) +} +/* EOF */ -- 1.7.10