From: Carl Worth Date: Tue, 10 Nov 2009 00:12:28 +0000 (-0800) Subject: libify: Move library sources down into lib directory. X-Git-Tag: 0.1~552 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=146549321044615d9aef2b30cedccda9c49f3f38 libify: Move library sources down into lib directory. A "make" invocation still works from the top-level, but not from down inside the lib directory yet. --- diff --git a/Makefile b/Makefile index 280b5566..1e7f5f2c 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PROGS=notmuch WARN_FLAGS=-Wall -Wextra -Wmissing-declarations -Wwrite-strings -Wswitch-enum -NOTMUCH_DEPENDS_FLAGS=`pkg-config --cflags glib-2.0 gmime-2.4 talloc` +NOTMUCH_DEPENDS_FLAGS=-I./lib `pkg-config --cflags glib-2.0 gmime-2.4 talloc` NOTMUCH_CXX_DEPENDS_FLAGS=$(NOTMUCH_DEPENDS_FLAGS) `xapian-config --cxxflags` NOTMUCH_CFLAGS=$(WARN_FLAGS) -O0 -g $(NOTMUCH_DEPENDS_FLAGS) @@ -11,17 +11,17 @@ NOTMUCH_CXXFLAGS=$(WARN_FLAGS) -O0 -g $(NOTMUCH_CXX_DEPENDS_FLAGS) NOTMUCH_LDFLAGS=`pkg-config --libs glib-2.0 gmime-2.4 talloc` \ `xapian-config --libs` -LIBRARY= \ - database.o \ - index.o \ - libsha1.o \ - message.o \ - message-file.o \ - query.o \ - sha1.o \ - tags.o \ - thread.o \ - xutil.o +LIBRARY= \ + lib/database.o \ + lib/index.o \ + lib/libsha1.o \ + lib/message.o \ + lib/message-file.o \ + lib/query.o \ + lib/sha1.o \ + lib/tags.o \ + lib/thread.o \ + lib/xutil.o MAIN= \ notmuch.o @@ -37,7 +37,7 @@ all: $(PROGS) notmuch: $(MAIN) $(LIBRARY) $(CC) $(NOTMUCH_LDFLAGS) $^ -o $@ -Makefile.dep: *.c *.cc +Makefile.dep: *.c lib/*.c lib/*.cc $(CXX) -M $(CPPFLAGS) $(NOTMUCH_DEPENDS_FLAGS) \ $(NOTMUCH_CXX_DEPENDS_FLAGS) $^ > $@ -include Makefile.dep @@ -52,4 +52,4 @@ install: all notmuch.1.gz $(DESTDIR)/etc/bash_completion.d/notmuch clean: - rm -f $(PROGS) *.o Makefile.dep + rm -f $(PROGS) lib/*.o *.o Makefile.dep diff --git a/database-private.h b/database-private.h deleted file mode 100644 index 76e26ce0..00000000 --- a/database-private.h +++ /dev/null @@ -1,35 +0,0 @@ -/* database-private.h - For peeking into the internals of notmuch_database_t - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#ifndef NOTMUCH_DATABASE_PRIVATE_H -#define NOTMUCH_DATABASE_PRIVATE_H - -#include "notmuch-private.h" - -#include - -struct _notmuch_database { - char *path; - Xapian::WritableDatabase *xapian_db; - Xapian::QueryParser *query_parser; - Xapian::TermGenerator *term_gen; -}; - -#endif diff --git a/database.cc b/database.cc deleted file mode 100644 index 4524016b..00000000 --- a/database.cc +++ /dev/null @@ -1,987 +0,0 @@ -/* database.cc - The database interfaces of the notmuch mail library - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#include "database-private.h" - -#include - -#include - -#include /* g_free, GPtrArray, GHashTable */ - -using namespace std; - -#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0])) - -typedef struct { - const char *name; - const char *prefix; -} prefix_t; - -/* Here's the current schema for our database: - * - * We currently have two different types of documents: mail and timestamps. - * - * Mail document - * ------------- - * A mail document is associated with a particular email message file - * on disk. It is indexed with the following prefixed terms: - * - * Single terms of given prefix: - * - * type: mail - * - * id: Unique ID of mail, (from Message-ID header or generated - * as "notmuch-sha1-. - * - * thread: The ID of the thread to which the mail belongs - * - * Multiple terms of given prefix: - * - * ref: All unresolved message IDs from In-Reply-To and - * References headers in the message. (Once a referenced - * message is added to the database and the thread IDs - * are linked the corresponding "ref" term is dropped - * from the message document.) - * - * tag: Any tags associated with this message by the user. - * - * A mail document also has two values: - * - * TIMESTAMP: The time_t value corresponding to the message's - * Date header. - * - * MESSAGE_ID: The unique ID of the mail mess (see "id" above) - * - * Timestamp document - * ------------------ - * A timestamp document is used by a client of the notmuch library to - * maintain data necessary to allow for efficient polling of mail - * directories. The notmuch library does no interpretation of - * timestamps, but merely allows the user to store and retrieve - * timestamps as name/value pairs. - * - * The timestamp document is indexed with a single prefixed term: - * - * timestamp: The user's key value (likely a directory name) - * - * and has a single value: - * - * TIMESTAMP: The time_t value from the user. - */ - -/* With these prefix values we follow the conventions published here: - * - * http://xapian.org/docs/omega/termprefixes.html - * - * as much as makes sense. Note that I took some liberty in matching - * the reserved prefix values to notmuch concepts, (for example, 'G' - * is documented as "newsGroup (or similar entity - e.g. a web forum - * name)", for which I think the thread is the closest analogue in - * notmuch. This in spite of the fact that we will eventually be - * storing mailing-list messages where 'G' for "mailing list name" - * might be even a closer analogue. I'm treating the single-character - * prefixes preferentially for core notmuch concepts (which will be - * nearly universal to all mail messages). - */ - -prefix_t BOOLEAN_PREFIX_INTERNAL[] = { - { "type", "T" }, - { "ref", "XREFERENCE" }, - { "replyto", "XREPLYTO" }, - { "timestamp", "XTIMESTAMP" }, - { "contact", "XCONTACT" } -}; - -prefix_t BOOLEAN_PREFIX_EXTERNAL[] = { - { "thread", "G" }, - { "tag", "K" }, - { "id", "Q" } -}; - -prefix_t PROBABILISTIC_PREFIX[]= { - { "from", "XFROM" }, - { "to", "XTO" }, - { "attachment", "XATTACHMENT" }, - { "subject", "XSUBJECT"} -}; - -int -_internal_error (const char *format, ...) -{ - va_list va_args; - - va_start (va_args, format); - - fprintf (stderr, "Internal error: "); - vfprintf (stderr, format, va_args); - - exit (1); - - return 1; -} - -const char * -_find_prefix (const char *name) -{ - unsigned int i; - - for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++) - if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0) - return BOOLEAN_PREFIX_INTERNAL[i].prefix; - - for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) - if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0) - return BOOLEAN_PREFIX_EXTERNAL[i].prefix; - - for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) - if (strcmp (name, PROBABILISTIC_PREFIX[i].name) == 0) - return PROBABILISTIC_PREFIX[i].prefix; - - INTERNAL_ERROR ("No prefix exists for '%s'\n", name); - - return ""; -} - -const char * -notmuch_status_to_string (notmuch_status_t status) -{ - switch (status) { - case NOTMUCH_STATUS_SUCCESS: - return "No error occurred"; - case NOTMUCH_STATUS_OUT_OF_MEMORY: - return "Out of memory"; - case NOTMUCH_STATUS_XAPIAN_EXCEPTION: - return "A Xapian exception occurred"; - case NOTMUCH_STATUS_FILE_ERROR: - return "Something went wrong trying to read or write a file"; - case NOTMUCH_STATUS_FILE_NOT_EMAIL: - return "File is not an email"; - case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: - return "Message ID is identical to a message in database"; - case NOTMUCH_STATUS_NULL_POINTER: - return "Erroneous NULL pointer"; - case NOTMUCH_STATUS_TAG_TOO_LONG: - return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)"; - case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: - return "Unblanced number of calls to notmuch_message_freeze/thaw"; - default: - case NOTMUCH_STATUS_LAST_STATUS: - return "Unknown error status value"; - } -} - -static void -find_doc_ids (notmuch_database_t *notmuch, - const char *prefix_name, - const char *value, - Xapian::PostingIterator *begin, - Xapian::PostingIterator *end) -{ - Xapian::PostingIterator i; - char *term; - - term = talloc_asprintf (notmuch, "%s%s", - _find_prefix (prefix_name), value); - - *begin = notmuch->xapian_db->postlist_begin (term); - - *end = notmuch->xapian_db->postlist_end (term); - - talloc_free (term); -} - -static notmuch_private_status_t -find_unique_doc_id (notmuch_database_t *notmuch, - const char *prefix_name, - const char *value, - unsigned int *doc_id) -{ - Xapian::PostingIterator i, end; - - find_doc_ids (notmuch, prefix_name, value, &i, &end); - - if (i == end) { - *doc_id = 0; - return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND; - } else { - *doc_id = *i; - return NOTMUCH_PRIVATE_STATUS_SUCCESS; - } -} - -static Xapian::Document -find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id) -{ - return notmuch->xapian_db->get_document (doc_id); -} - -static notmuch_private_status_t -find_unique_document (notmuch_database_t *notmuch, - const char *prefix_name, - const char *value, - Xapian::Document *document, - unsigned int *doc_id) -{ - notmuch_private_status_t status; - - status = find_unique_doc_id (notmuch, prefix_name, value, doc_id); - - if (status) { - *document = Xapian::Document (); - return status; - } - - *document = find_document_for_doc_id (notmuch, *doc_id); - return NOTMUCH_PRIVATE_STATUS_SUCCESS; -} - -notmuch_message_t * -notmuch_database_find_message (notmuch_database_t *notmuch, - const char *message_id) -{ - notmuch_private_status_t status; - unsigned int doc_id; - - status = find_unique_doc_id (notmuch, "id", message_id, &doc_id); - - if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) - return NULL; - - return _notmuch_message_create (notmuch, notmuch, doc_id, NULL); -} - -/* Advance 'str' past any whitespace or RFC 822 comments. A comment is - * a (potentially nested) parenthesized sequence with '\' used to - * escape any character (including parentheses). - * - * If the sequence to be skipped continues to the end of the string, - * then 'str' will be left pointing at the final terminating '\0' - * character. - */ -static void -skip_space_and_comments (const char **str) -{ - const char *s; - - s = *str; - while (*s && (isspace (*s) || *s == '(')) { - while (*s && isspace (*s)) - s++; - if (*s == '(') { - int nesting = 1; - s++; - while (*s && nesting) { - if (*s == '(') - nesting++; - else if (*s == ')') - nesting--; - else if (*s == '\\') - if (*(s+1)) - s++; - s++; - } - } - } - - *str = s; -} - -/* Parse an RFC 822 message-id, discarding whitespace, any RFC 822 - * comments, and the '<' and '>' delimeters. - * - * If not NULL, then *next will be made to point to the first character - * not parsed, (possibly pointing to the final '\0' terminator. - * - * Returns a newly talloc'ed string belonging to 'ctx'. - * - * Returns NULL if there is any error parsing the message-id. */ -static char * -parse_message_id (void *ctx, const char *message_id, const char **next) -{ - const char *s, *end; - char *result; - - if (message_id == NULL) - return NULL; - - s = message_id; - - skip_space_and_comments (&s); - - /* Skip any unstructured text as well. */ - while (*s && *s != '<') - s++; - - if (*s == '<') { - s++; - } else { - if (next) - *next = s; - return NULL; - } - - skip_space_and_comments (&s); - - end = s; - while (*end && *end != '>') - end++; - if (next) { - if (*end) - *next = end + 1; - else - *next = end; - } - - if (end > s && *end == '>') - end--; - if (end <= s) - return NULL; - - result = talloc_strndup (ctx, s, end - s + 1); - - /* Finally, collapse any whitespace that is within the message-id - * itself. */ - { - char *r; - int len; - - for (r = result, len = strlen (r); *r; r++, len--) - if (*r == ' ' || *r == '\t') - memmove (r, r+1, len); - } - - return result; -} - -/* Parse a References header value, putting a (talloc'ed under 'ctx') - * copy of each referenced message-id into 'hash'. */ -static void -parse_references (void *ctx, - GHashTable *hash, - const char *refs) -{ - char *ref; - - if (refs == NULL) - return; - - while (*refs) { - ref = parse_message_id (ctx, refs, &refs); - - if (ref) - g_hash_table_insert (hash, ref, NULL); - } -} - -char * -notmuch_database_default_path (void) -{ - char *path; - - if (getenv ("NOTMUCH_BASE")) - return strdup (getenv ("NOTMUCH_BASE")); - - if (asprintf (&path, "%s/mail", getenv ("HOME")) == -1) { - fprintf (stderr, "Out of memory.\n"); - return xstrdup(""); - } - - return path; -} - -notmuch_database_t * -notmuch_database_create (const char *path) -{ - notmuch_database_t *notmuch = NULL; - char *notmuch_path = NULL; - struct stat st; - int err; - char *local_path = NULL; - - if (path == NULL) - path = local_path = notmuch_database_default_path (); - - err = stat (path, &st); - if (err) { - fprintf (stderr, "Error: Cannot create database at %s: %s.\n", - path, strerror (errno)); - goto DONE; - } - - if (! S_ISDIR (st.st_mode)) { - fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n", - path); - goto DONE; - } - - notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch"); - - err = mkdir (notmuch_path, 0755); - - if (err) { - fprintf (stderr, "Error: Cannot create directory %s: %s.\n", - notmuch_path, strerror (errno)); - goto DONE; - } - - notmuch = notmuch_database_open (path); - - DONE: - if (notmuch_path) - talloc_free (notmuch_path); - if (local_path) - free (local_path); - - return notmuch; -} - -notmuch_database_t * -notmuch_database_open (const char *path) -{ - notmuch_database_t *notmuch = NULL; - char *notmuch_path = NULL, *xapian_path = NULL; - struct stat st; - int err; - char *local_path = NULL; - unsigned int i; - - if (path == NULL) - path = local_path = notmuch_database_default_path (); - - if (asprintf (¬much_path, "%s/%s", path, ".notmuch") == -1) { - notmuch_path = NULL; - fprintf (stderr, "Out of memory\n"); - goto DONE; - } - - err = stat (notmuch_path, &st); - if (err) { - fprintf (stderr, "Error opening database at %s: %s\n", - notmuch_path, strerror (errno)); - goto DONE; - } - - if (asprintf (&xapian_path, "%s/%s", notmuch_path, "xapian") == -1) { - xapian_path = NULL; - fprintf (stderr, "Out of memory\n"); - goto DONE; - } - - notmuch = talloc (NULL, notmuch_database_t); - notmuch->path = talloc_strdup (notmuch, path); - - if (notmuch->path[strlen (notmuch->path) - 1] == '/') - notmuch->path[strlen (notmuch->path) - 1] = '\0'; - - try { - notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path, - Xapian::DB_CREATE_OR_OPEN); - notmuch->query_parser = new Xapian::QueryParser; - notmuch->term_gen = new Xapian::TermGenerator; - notmuch->term_gen->set_stemmer (Xapian::Stem ("english")); - - notmuch->query_parser->set_default_op (Xapian::Query::OP_AND); - notmuch->query_parser->set_database (*notmuch->xapian_db); - notmuch->query_parser->set_stemmer (Xapian::Stem ("english")); - notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME); - - for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) { - prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i]; - notmuch->query_parser->add_boolean_prefix (prefix->name, - prefix->prefix); - } - - for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) { - prefix_t *prefix = &PROBABILISTIC_PREFIX[i]; - notmuch->query_parser->add_prefix (prefix->name, prefix->prefix); - } - } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred: %s\n", - error.get_msg().c_str()); - notmuch = NULL; - } - - DONE: - if (local_path) - free (local_path); - if (notmuch_path) - free (notmuch_path); - if (xapian_path) - free (xapian_path); - - return notmuch; -} - -void -notmuch_database_close (notmuch_database_t *notmuch) -{ - notmuch->xapian_db->flush (); - - delete notmuch->term_gen; - delete notmuch->query_parser; - delete notmuch->xapian_db; - talloc_free (notmuch); -} - -const char * -notmuch_database_get_path (notmuch_database_t *notmuch) -{ - return notmuch->path; -} - -static notmuch_private_status_t -find_timestamp_document (notmuch_database_t *notmuch, const char *db_key, - Xapian::Document *doc, unsigned int *doc_id) -{ - return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id); -} - -/* We allow the user to use arbitrarily long keys for timestamps, - * (they're for filesystem paths after all, which have no limit we - * know about). But we have a term-length limit. So if we exceed that, - * we'll use the SHA-1 of the user's key as the actual key for - * constructing a database term. - * - * Caution: This function returns a newly allocated string which the - * caller should free() when finished. - */ -static char * -timestamp_db_key (const char *key) -{ - int term_len = strlen (_find_prefix ("timestamp")) + strlen (key); - - if (term_len > NOTMUCH_TERM_MAX) - return notmuch_sha1_of_string (key); - else - return strdup (key); -} - -notmuch_status_t -notmuch_database_set_timestamp (notmuch_database_t *notmuch, - const char *key, time_t timestamp) -{ - Xapian::Document doc; - unsigned int doc_id; - notmuch_private_status_t status; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - char *db_key = NULL; - - db_key = timestamp_db_key (key); - - try { - status = find_timestamp_document (notmuch, db_key, &doc, &doc_id); - - doc.add_value (NOTMUCH_VALUE_TIMESTAMP, - Xapian::sortable_serialise (timestamp)); - - if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) { - char *term = talloc_asprintf (NULL, "%s%s", - _find_prefix ("timestamp"), db_key); - doc.add_term (term); - talloc_free (term); - - notmuch->xapian_db->add_document (doc); - } else { - notmuch->xapian_db->replace_document (doc_id, doc); - } - - } catch (Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred: %s.\n", - error.get_msg().c_str()); - ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION; - } - - if (db_key) - free (db_key); - - return ret; -} - -time_t -notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key) -{ - Xapian::Document doc; - unsigned int doc_id; - notmuch_private_status_t status; - char *db_key = NULL; - time_t ret = 0; - - db_key = timestamp_db_key (key); - - try { - status = find_timestamp_document (notmuch, db_key, &doc, &doc_id); - - if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) - goto DONE; - - ret = Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP)); - } catch (Xapian::Error &error) { - goto DONE; - } - - DONE: - if (db_key) - free (db_key); - - return ret; -} - -/* Find the thread ID to which the message with 'message_id' belongs. - * - * Returns NULL if no message with message ID 'message_id' is in the - * database. - * - * Otherwise, returns a newly talloced string belonging to 'ctx'. - */ -static const char * -_resolve_message_id_to_thread_id (notmuch_database_t *notmuch, - void *ctx, - const char *message_id) -{ - notmuch_message_t *message; - const char *ret = NULL; - - message = notmuch_database_find_message (notmuch, message_id); - if (message == NULL) - goto DONE; - - ret = talloc_steal (ctx, notmuch_message_get_thread_id (message)); - - DONE: - if (message) - notmuch_message_destroy (message); - - return ret; -} - -static notmuch_status_t -_merge_threads (notmuch_database_t *notmuch, - const char *winner_thread_id, - const char *loser_thread_id) -{ - Xapian::PostingIterator loser, loser_end; - notmuch_message_t *message = NULL; - notmuch_private_status_t private_status; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - - find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end); - - for ( ; loser != loser_end; loser++) { - message = _notmuch_message_create (notmuch, notmuch, - *loser, &private_status); - if (message == NULL) { - ret = COERCE_STATUS (private_status, - "Cannot find document for doc_id from query"); - goto DONE; - } - - _notmuch_message_remove_term (message, "thread", loser_thread_id); - _notmuch_message_add_term (message, "thread", winner_thread_id); - _notmuch_message_sync (message); - - notmuch_message_destroy (message); - message = NULL; - } - - DONE: - if (message) - notmuch_message_destroy (message); - - return ret; -} - -static void -_my_talloc_free_for_g_hash (void *ptr) -{ - talloc_free (ptr); -} - -static notmuch_status_t -_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch, - notmuch_message_t *message, - notmuch_message_file_t *message_file, - const char **thread_id) -{ - GHashTable *parents = NULL; - const char *refs, *in_reply_to; - GList *l, *keys = NULL; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - - parents = g_hash_table_new_full (g_str_hash, g_str_equal, - _my_talloc_free_for_g_hash, NULL); - - refs = notmuch_message_file_get_header (message_file, "references"); - parse_references (message, parents, refs); - - in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to"); - parse_references (message, parents, in_reply_to); - _notmuch_message_add_term (message, "replyto", - parse_message_id (message, in_reply_to, NULL)); - - keys = g_hash_table_get_keys (parents); - for (l = keys; l; l = l->next) { - char *parent_message_id; - const char *parent_thread_id; - - parent_message_id = (char *) l->data; - parent_thread_id = _resolve_message_id_to_thread_id (notmuch, - message, - parent_message_id); - - if (parent_thread_id == NULL) { - _notmuch_message_add_term (message, "ref", parent_message_id); - } else { - if (*thread_id == NULL) { - *thread_id = talloc_strdup (message, parent_thread_id); - _notmuch_message_add_term (message, "thread", *thread_id); - } else if (strcmp (*thread_id, parent_thread_id)) { - ret = _merge_threads (notmuch, *thread_id, parent_thread_id); - if (ret) - goto DONE; - } - } - } - - DONE: - if (keys) - g_list_free (keys); - if (parents) - g_hash_table_unref (parents); - - return ret; -} - -static notmuch_status_t -_notmuch_database_link_message_to_children (notmuch_database_t *notmuch, - notmuch_message_t *message, - const char **thread_id) -{ - const char *message_id = notmuch_message_get_message_id (message); - Xapian::PostingIterator child, children_end; - notmuch_message_t *child_message = NULL; - const char *child_thread_id; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - notmuch_private_status_t private_status; - - find_doc_ids (notmuch, "ref", message_id, &child, &children_end); - - for ( ; child != children_end; child++) { - - child_message = _notmuch_message_create (message, notmuch, - *child, &private_status); - if (child_message == NULL) { - ret = COERCE_STATUS (private_status, - "Cannot find document for doc_id from query"); - goto DONE; - } - - child_thread_id = notmuch_message_get_thread_id (child_message); - if (*thread_id == NULL) { - *thread_id = talloc_strdup (message, child_thread_id); - _notmuch_message_add_term (message, "thread", *thread_id); - } else if (strcmp (*thread_id, child_thread_id)) { - _notmuch_message_remove_term (child_message, "ref", - message_id); - _notmuch_message_sync (child_message); - ret = _merge_threads (notmuch, *thread_id, child_thread_id); - if (ret) - goto DONE; - } - - notmuch_message_destroy (child_message); - child_message = NULL; - } - - DONE: - if (child_message) - notmuch_message_destroy (child_message); - - return ret; -} - -/* Given a (mostly empty) 'message' and its corresponding - * 'message_file' link it to existing threads in the database. - * - * We first looke at 'message_file' and its link-relevant headers - * (References and In-Reply-To) for message IDs. We also look in the - * database for existing message that reference 'message'.p - * - * The end result is to call _notmuch_message_add_thread_id with one - * or more thread IDs to which this message belongs, (including - * generating a new thread ID if necessary if the message doesn't - * connect to any existing threads). - */ -static notmuch_status_t -_notmuch_database_link_message (notmuch_database_t *notmuch, - notmuch_message_t *message, - notmuch_message_file_t *message_file) -{ - notmuch_status_t status; - const char *thread_id = NULL; - - status = _notmuch_database_link_message_to_parents (notmuch, message, - message_file, - &thread_id); - if (status) - return status; - - status = _notmuch_database_link_message_to_children (notmuch, message, - &thread_id); - if (status) - return status; - - if (thread_id == NULL) - _notmuch_message_ensure_thread_id (message); - - return NOTMUCH_STATUS_SUCCESS; -} - -notmuch_status_t -notmuch_database_add_message (notmuch_database_t *notmuch, - const char *filename, - notmuch_message_t **message_ret) -{ - notmuch_message_file_t *message_file; - notmuch_message_t *message = NULL; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - notmuch_private_status_t private_status; - - const char *date, *header; - const char *from, *to, *subject; - char *message_id; - - if (message_ret) - *message_ret = NULL; - - message_file = notmuch_message_file_open (filename); - if (message_file == NULL) { - ret = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - notmuch_message_file_restrict_headers (message_file, - "date", - "from", - "in-reply-to", - "message-id", - "references", - "subject", - "to", - (char *) NULL); - - try { - /* Before we do any real work, (especially before doing a - * potential SHA-1 computation on the entire file's contents), - * let's make sure that what we're looking at looks like an - * actual email message. - */ - from = notmuch_message_file_get_header (message_file, "from"); - subject = notmuch_message_file_get_header (message_file, "subject"); - to = notmuch_message_file_get_header (message_file, "to"); - - if (from == NULL && - subject == NULL && - to == NULL) - { - ret = NOTMUCH_STATUS_FILE_NOT_EMAIL; - goto DONE; - } - - /* Now that we're sure it's mail, the first order of business - * is to find a message ID (or else create one ourselves). */ - - header = notmuch_message_file_get_header (message_file, "message-id"); - if (header) { - message_id = parse_message_id (message_file, header, NULL); - /* So the header value isn't RFC-compliant, but it's - * better than no message-id at all. */ - if (message_id == NULL) - message_id = talloc_strdup (message_file, header); - } else { - /* No message-id at all, let's generate one by taking a - * hash over the file's contents. */ - char *sha1 = notmuch_sha1_of_file (filename); - - /* If that failed too, something is really wrong. Give up. */ - if (sha1 == NULL) { - ret = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - message_id = talloc_asprintf (message_file, - "notmuch-sha1-%s", sha1); - free (sha1); - } - - /* Now that we have a message ID, we get a message object, - * (which may or may not reference an existing document in the - * database). */ - - /* Use NULL for owner since we want to free this locally. */ - message = _notmuch_message_create_for_message_id (NULL, - notmuch, - message_id, - &private_status); - - talloc_free (message_id); - - if (message == NULL) - goto DONE; - - /* Is this a newly created message object? */ - if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) { - _notmuch_message_set_filename (message, filename); - _notmuch_message_add_term (message, "type", "mail"); - } else { - ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID; - goto DONE; - } - - ret = _notmuch_database_link_message (notmuch, message, message_file); - if (ret) - goto DONE; - - date = notmuch_message_file_get_header (message_file, "date"); - _notmuch_message_set_date (message, date); - - _notmuch_message_index_file (message, filename); - - _notmuch_message_sync (message); - } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred: %s.\n", - error.get_msg().c_str()); - ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION; - goto DONE; - } - - DONE: - if (message) { - if (ret == NOTMUCH_STATUS_SUCCESS && message_ret) - *message_ret = message; - else - notmuch_message_destroy (message); - } - - if (message_file) - notmuch_message_file_close (message_file); - - return ret; -} diff --git a/index.cc b/index.cc deleted file mode 100644 index 747a4e63..00000000 --- a/index.cc +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#include "notmuch-private.h" - -#include - -#include - -/* We're finally down to a single (NAME + address) email "mailbox". */ -static void -_index_address_mailbox (notmuch_message_t *message, - const char *prefix_name, - InternetAddress *address) -{ - InternetAddressMailbox *mailbox = INTERNET_ADDRESS_MAILBOX (address); - const char *name, *addr; - char *contact; - int own_name = 0; - - name = internet_address_get_name (address); - addr = internet_address_mailbox_get_addr (mailbox); - - if (addr) { - if (name) { - contact = talloc_asprintf (message, "\"%s\" <%s>", - name, addr); - _notmuch_message_add_term (message, "contact", contact); - talloc_free (contact); - } else { - _notmuch_message_add_term (message, "contact", addr); - } - } - - /* In the absence of a name, we'll strip the part before the @ - * from the address. */ - if (! name) { - const char *at; - - at = strchr (addr, '@'); - if (at) { - name = strndup (addr, at - addr); - own_name = 1; - } - } - - if (name) - _notmuch_message_gen_terms (message, prefix_name, name); - if (addr) - _notmuch_message_gen_terms (message, prefix_name, addr); -} - -static void -_index_address_list (notmuch_message_t *message, - const char *prefix_name, - InternetAddressList *addresses); - -/* The outer loop over the InternetAddressList wasn't quite enough. - * There can actually be a tree here where a single member of the list - * is a "group" containing another list. Recurse please. - */ -static void -_index_address_group (notmuch_message_t *message, - const char *prefix_name, - InternetAddress *address) -{ - InternetAddressGroup *group; - InternetAddressList *list; - - group = INTERNET_ADDRESS_GROUP (address); - list = internet_address_group_get_members (group); - - if (! list) - return; - - _index_address_list (message, prefix_name, list); -} - -static void -_index_address_list (notmuch_message_t *message, - const char *prefix_name, - InternetAddressList *addresses) -{ - int i; - InternetAddress *address; - - if (addresses == NULL) - return; - - for (i = 0; i < internet_address_list_length (addresses); i++) { - address = internet_address_list_get_address (addresses, i); - if (INTERNET_ADDRESS_IS_MAILBOX (address)) { - _index_address_mailbox (message, prefix_name, address); - } else if (INTERNET_ADDRESS_IS_GROUP (address)) { - _index_address_group (message, prefix_name, address); - } else { - INTERNAL_ERROR ("GMime InternetAddress is neither a mailbox nor a group.\n"); - } - } -} - -static const char * -skip_re_in_subject (const char *subject) -{ - const char *s = subject; - - if (subject == NULL) - return NULL; - - while (*s) { - while (*s && isspace (*s)) - s++; - if (strncasecmp (s, "re:", 3) == 0) - s += 3; - else - break; - } - - return s; -} - -/* Given a string representing the body of a message, generate terms - * for it, (skipping quoted portions and signatures). - * - * This function is evil in that it modifies the string passed to it, - * (changing some newlines into '\0'). - */ -static void -_index_body_text (notmuch_message_t *message, char *body) -{ - char *line, *line_end, *next_line; - - if (body == NULL) - return; - - next_line = body; - - while (1) { - line = next_line; - if (*line == '\0') - break; - - next_line = strchr (line, '\n'); - if (next_line == NULL) { - next_line = line + strlen (line); - } - line_end = next_line - 1; - - /* Get to the next non-blank line. */ - while (*next_line == '\n') - next_line++; - - /* Skip blank lines. */ - if (line_end < line) - continue; - - /* Skip lines that are quotes. */ - if (*line == '>') - continue; - - /* Also skip lines introducing a quote on the next line. */ - if (*line_end == ':' && *next_line == '>') - continue; - - /* Finally, bail as soon as we see a signature. */ - /* XXX: Should only do this if "near" the end of the message. */ - if (strncmp (line, "-- ", 3) == 0) - break; - - *(line_end + 1) = '\0'; - - _notmuch_message_gen_terms (message, NULL, line); - } -} - -/* Callback to generate terms for each mime part of a message. */ -static void -_index_mime_part (notmuch_message_t *message, - GMimeObject *part) -{ - GMimeStream *stream; - GMimeDataWrapper *wrapper; - GByteArray *byte_array; - GMimeContentDisposition *disposition; - char *body; - - if (GMIME_IS_MULTIPART (part)) { - GMimeMultipart *multipart = GMIME_MULTIPART (part); - int i; - - for (i = 0; i < g_mime_multipart_get_count (multipart); i++) { - if (GMIME_IS_MULTIPART_SIGNED (multipart)) { - /* Don't index the signature. */ - if (i == 1) - continue; - if (i > 1) - fprintf (stderr, "Warning: Unexpected extra parts of mutlipart/signed. Indexing anyway.\n"); - } - _index_mime_part (message, - g_mime_multipart_get_part (multipart, i)); - } - return; - } - - if (GMIME_IS_MESSAGE_PART (part)) { - GMimeMessage *mime_message; - - mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part)); - - _index_mime_part (message, g_mime_message_get_mime_part (mime_message)); - - return; - } - - if (! (GMIME_IS_PART (part))) { - fprintf (stderr, "Warning: Not indexing unknown mime part: %s.\n", - g_type_name (G_OBJECT_TYPE (part))); - return; - } - - disposition = g_mime_object_get_content_disposition (part); - if (disposition && - strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0) - { - const char *filename = g_mime_part_get_filename (GMIME_PART (part)); - - _notmuch_message_add_term (message, "tag", "attachment"); - _notmuch_message_gen_terms (message, "attachment", filename); - - /* XXX: Would be nice to call out to something here to parse - * the attachment into text and then index that. */ - return; - } - - byte_array = g_byte_array_new (); - - stream = g_mime_stream_mem_new_with_byte_array (byte_array); - g_mime_stream_mem_set_owner (GMIME_STREAM_MEM (stream), FALSE); - wrapper = g_mime_part_get_content_object (GMIME_PART (part)); - if (wrapper) - g_mime_data_wrapper_write_to_stream (wrapper, stream); - - g_object_unref (stream); - - g_byte_array_append (byte_array, (guint8 *) "\0", 1); - body = (char *) g_byte_array_free (byte_array, FALSE); - - _index_body_text (message, body); - - free (body); -} - -notmuch_status_t -_notmuch_message_index_file (notmuch_message_t *message, - const char *filename) -{ - GMimeStream *stream = NULL; - GMimeParser *parser = NULL; - GMimeMessage *mime_message = NULL; - InternetAddressList *addresses; - FILE *file = NULL; - const char *from, *subject; - notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - static int initialized = 0; - - if (! initialized) { - g_mime_init (0); - initialized = 1; - } - - file = fopen (filename, "r"); - if (! file) { - fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno)); - ret = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - /* Evil GMime steals my FILE* here so I won't fclose it. */ - stream = g_mime_stream_file_new (file); - - parser = g_mime_parser_new_with_stream (stream); - - mime_message = g_mime_parser_construct_message (parser); - - from = g_mime_message_get_sender (mime_message); - addresses = internet_address_list_parse_string (from); - - _index_address_list (message, "from", addresses); - - addresses = g_mime_message_get_all_recipients (mime_message); - _index_address_list (message, "to", addresses); - - subject = g_mime_message_get_subject (mime_message); - subject = skip_re_in_subject (subject); - _notmuch_message_gen_terms (message, "subject", subject); - - _index_mime_part (message, g_mime_message_get_mime_part (mime_message)); - - DONE: - if (mime_message) - g_object_unref (mime_message); - - if (parser) - g_object_unref (parser); - - if (stream) - g_object_unref (stream); - - return ret; -} diff --git a/lib/database-private.h b/lib/database-private.h new file mode 100644 index 00000000..76e26ce0 --- /dev/null +++ b/lib/database-private.h @@ -0,0 +1,35 @@ +/* database-private.h - For peeking into the internals of notmuch_database_t + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#ifndef NOTMUCH_DATABASE_PRIVATE_H +#define NOTMUCH_DATABASE_PRIVATE_H + +#include "notmuch-private.h" + +#include + +struct _notmuch_database { + char *path; + Xapian::WritableDatabase *xapian_db; + Xapian::QueryParser *query_parser; + Xapian::TermGenerator *term_gen; +}; + +#endif diff --git a/lib/database.cc b/lib/database.cc new file mode 100644 index 00000000..4524016b --- /dev/null +++ b/lib/database.cc @@ -0,0 +1,987 @@ +/* database.cc - The database interfaces of the notmuch mail library + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#include "database-private.h" + +#include + +#include + +#include /* g_free, GPtrArray, GHashTable */ + +using namespace std; + +#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0])) + +typedef struct { + const char *name; + const char *prefix; +} prefix_t; + +/* Here's the current schema for our database: + * + * We currently have two different types of documents: mail and timestamps. + * + * Mail document + * ------------- + * A mail document is associated with a particular email message file + * on disk. It is indexed with the following prefixed terms: + * + * Single terms of given prefix: + * + * type: mail + * + * id: Unique ID of mail, (from Message-ID header or generated + * as "notmuch-sha1-. + * + * thread: The ID of the thread to which the mail belongs + * + * Multiple terms of given prefix: + * + * ref: All unresolved message IDs from In-Reply-To and + * References headers in the message. (Once a referenced + * message is added to the database and the thread IDs + * are linked the corresponding "ref" term is dropped + * from the message document.) + * + * tag: Any tags associated with this message by the user. + * + * A mail document also has two values: + * + * TIMESTAMP: The time_t value corresponding to the message's + * Date header. + * + * MESSAGE_ID: The unique ID of the mail mess (see "id" above) + * + * Timestamp document + * ------------------ + * A timestamp document is used by a client of the notmuch library to + * maintain data necessary to allow for efficient polling of mail + * directories. The notmuch library does no interpretation of + * timestamps, but merely allows the user to store and retrieve + * timestamps as name/value pairs. + * + * The timestamp document is indexed with a single prefixed term: + * + * timestamp: The user's key value (likely a directory name) + * + * and has a single value: + * + * TIMESTAMP: The time_t value from the user. + */ + +/* With these prefix values we follow the conventions published here: + * + * http://xapian.org/docs/omega/termprefixes.html + * + * as much as makes sense. Note that I took some liberty in matching + * the reserved prefix values to notmuch concepts, (for example, 'G' + * is documented as "newsGroup (or similar entity - e.g. a web forum + * name)", for which I think the thread is the closest analogue in + * notmuch. This in spite of the fact that we will eventually be + * storing mailing-list messages where 'G' for "mailing list name" + * might be even a closer analogue. I'm treating the single-character + * prefixes preferentially for core notmuch concepts (which will be + * nearly universal to all mail messages). + */ + +prefix_t BOOLEAN_PREFIX_INTERNAL[] = { + { "type", "T" }, + { "ref", "XREFERENCE" }, + { "replyto", "XREPLYTO" }, + { "timestamp", "XTIMESTAMP" }, + { "contact", "XCONTACT" } +}; + +prefix_t BOOLEAN_PREFIX_EXTERNAL[] = { + { "thread", "G" }, + { "tag", "K" }, + { "id", "Q" } +}; + +prefix_t PROBABILISTIC_PREFIX[]= { + { "from", "XFROM" }, + { "to", "XTO" }, + { "attachment", "XATTACHMENT" }, + { "subject", "XSUBJECT"} +}; + +int +_internal_error (const char *format, ...) +{ + va_list va_args; + + va_start (va_args, format); + + fprintf (stderr, "Internal error: "); + vfprintf (stderr, format, va_args); + + exit (1); + + return 1; +} + +const char * +_find_prefix (const char *name) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++) + if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0) + return BOOLEAN_PREFIX_INTERNAL[i].prefix; + + for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) + if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0) + return BOOLEAN_PREFIX_EXTERNAL[i].prefix; + + for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) + if (strcmp (name, PROBABILISTIC_PREFIX[i].name) == 0) + return PROBABILISTIC_PREFIX[i].prefix; + + INTERNAL_ERROR ("No prefix exists for '%s'\n", name); + + return ""; +} + +const char * +notmuch_status_to_string (notmuch_status_t status) +{ + switch (status) { + case NOTMUCH_STATUS_SUCCESS: + return "No error occurred"; + case NOTMUCH_STATUS_OUT_OF_MEMORY: + return "Out of memory"; + case NOTMUCH_STATUS_XAPIAN_EXCEPTION: + return "A Xapian exception occurred"; + case NOTMUCH_STATUS_FILE_ERROR: + return "Something went wrong trying to read or write a file"; + case NOTMUCH_STATUS_FILE_NOT_EMAIL: + return "File is not an email"; + case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: + return "Message ID is identical to a message in database"; + case NOTMUCH_STATUS_NULL_POINTER: + return "Erroneous NULL pointer"; + case NOTMUCH_STATUS_TAG_TOO_LONG: + return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)"; + case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: + return "Unblanced number of calls to notmuch_message_freeze/thaw"; + default: + case NOTMUCH_STATUS_LAST_STATUS: + return "Unknown error status value"; + } +} + +static void +find_doc_ids (notmuch_database_t *notmuch, + const char *prefix_name, + const char *value, + Xapian::PostingIterator *begin, + Xapian::PostingIterator *end) +{ + Xapian::PostingIterator i; + char *term; + + term = talloc_asprintf (notmuch, "%s%s", + _find_prefix (prefix_name), value); + + *begin = notmuch->xapian_db->postlist_begin (term); + + *end = notmuch->xapian_db->postlist_end (term); + + talloc_free (term); +} + +static notmuch_private_status_t +find_unique_doc_id (notmuch_database_t *notmuch, + const char *prefix_name, + const char *value, + unsigned int *doc_id) +{ + Xapian::PostingIterator i, end; + + find_doc_ids (notmuch, prefix_name, value, &i, &end); + + if (i == end) { + *doc_id = 0; + return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND; + } else { + *doc_id = *i; + return NOTMUCH_PRIVATE_STATUS_SUCCESS; + } +} + +static Xapian::Document +find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id) +{ + return notmuch->xapian_db->get_document (doc_id); +} + +static notmuch_private_status_t +find_unique_document (notmuch_database_t *notmuch, + const char *prefix_name, + const char *value, + Xapian::Document *document, + unsigned int *doc_id) +{ + notmuch_private_status_t status; + + status = find_unique_doc_id (notmuch, prefix_name, value, doc_id); + + if (status) { + *document = Xapian::Document (); + return status; + } + + *document = find_document_for_doc_id (notmuch, *doc_id); + return NOTMUCH_PRIVATE_STATUS_SUCCESS; +} + +notmuch_message_t * +notmuch_database_find_message (notmuch_database_t *notmuch, + const char *message_id) +{ + notmuch_private_status_t status; + unsigned int doc_id; + + status = find_unique_doc_id (notmuch, "id", message_id, &doc_id); + + if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) + return NULL; + + return _notmuch_message_create (notmuch, notmuch, doc_id, NULL); +} + +/* Advance 'str' past any whitespace or RFC 822 comments. A comment is + * a (potentially nested) parenthesized sequence with '\' used to + * escape any character (including parentheses). + * + * If the sequence to be skipped continues to the end of the string, + * then 'str' will be left pointing at the final terminating '\0' + * character. + */ +static void +skip_space_and_comments (const char **str) +{ + const char *s; + + s = *str; + while (*s && (isspace (*s) || *s == '(')) { + while (*s && isspace (*s)) + s++; + if (*s == '(') { + int nesting = 1; + s++; + while (*s && nesting) { + if (*s == '(') + nesting++; + else if (*s == ')') + nesting--; + else if (*s == '\\') + if (*(s+1)) + s++; + s++; + } + } + } + + *str = s; +} + +/* Parse an RFC 822 message-id, discarding whitespace, any RFC 822 + * comments, and the '<' and '>' delimeters. + * + * If not NULL, then *next will be made to point to the first character + * not parsed, (possibly pointing to the final '\0' terminator. + * + * Returns a newly talloc'ed string belonging to 'ctx'. + * + * Returns NULL if there is any error parsing the message-id. */ +static char * +parse_message_id (void *ctx, const char *message_id, const char **next) +{ + const char *s, *end; + char *result; + + if (message_id == NULL) + return NULL; + + s = message_id; + + skip_space_and_comments (&s); + + /* Skip any unstructured text as well. */ + while (*s && *s != '<') + s++; + + if (*s == '<') { + s++; + } else { + if (next) + *next = s; + return NULL; + } + + skip_space_and_comments (&s); + + end = s; + while (*end && *end != '>') + end++; + if (next) { + if (*end) + *next = end + 1; + else + *next = end; + } + + if (end > s && *end == '>') + end--; + if (end <= s) + return NULL; + + result = talloc_strndup (ctx, s, end - s + 1); + + /* Finally, collapse any whitespace that is within the message-id + * itself. */ + { + char *r; + int len; + + for (r = result, len = strlen (r); *r; r++, len--) + if (*r == ' ' || *r == '\t') + memmove (r, r+1, len); + } + + return result; +} + +/* Parse a References header value, putting a (talloc'ed under 'ctx') + * copy of each referenced message-id into 'hash'. */ +static void +parse_references (void *ctx, + GHashTable *hash, + const char *refs) +{ + char *ref; + + if (refs == NULL) + return; + + while (*refs) { + ref = parse_message_id (ctx, refs, &refs); + + if (ref) + g_hash_table_insert (hash, ref, NULL); + } +} + +char * +notmuch_database_default_path (void) +{ + char *path; + + if (getenv ("NOTMUCH_BASE")) + return strdup (getenv ("NOTMUCH_BASE")); + + if (asprintf (&path, "%s/mail", getenv ("HOME")) == -1) { + fprintf (stderr, "Out of memory.\n"); + return xstrdup(""); + } + + return path; +} + +notmuch_database_t * +notmuch_database_create (const char *path) +{ + notmuch_database_t *notmuch = NULL; + char *notmuch_path = NULL; + struct stat st; + int err; + char *local_path = NULL; + + if (path == NULL) + path = local_path = notmuch_database_default_path (); + + err = stat (path, &st); + if (err) { + fprintf (stderr, "Error: Cannot create database at %s: %s.\n", + path, strerror (errno)); + goto DONE; + } + + if (! S_ISDIR (st.st_mode)) { + fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n", + path); + goto DONE; + } + + notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch"); + + err = mkdir (notmuch_path, 0755); + + if (err) { + fprintf (stderr, "Error: Cannot create directory %s: %s.\n", + notmuch_path, strerror (errno)); + goto DONE; + } + + notmuch = notmuch_database_open (path); + + DONE: + if (notmuch_path) + talloc_free (notmuch_path); + if (local_path) + free (local_path); + + return notmuch; +} + +notmuch_database_t * +notmuch_database_open (const char *path) +{ + notmuch_database_t *notmuch = NULL; + char *notmuch_path = NULL, *xapian_path = NULL; + struct stat st; + int err; + char *local_path = NULL; + unsigned int i; + + if (path == NULL) + path = local_path = notmuch_database_default_path (); + + if (asprintf (¬much_path, "%s/%s", path, ".notmuch") == -1) { + notmuch_path = NULL; + fprintf (stderr, "Out of memory\n"); + goto DONE; + } + + err = stat (notmuch_path, &st); + if (err) { + fprintf (stderr, "Error opening database at %s: %s\n", + notmuch_path, strerror (errno)); + goto DONE; + } + + if (asprintf (&xapian_path, "%s/%s", notmuch_path, "xapian") == -1) { + xapian_path = NULL; + fprintf (stderr, "Out of memory\n"); + goto DONE; + } + + notmuch = talloc (NULL, notmuch_database_t); + notmuch->path = talloc_strdup (notmuch, path); + + if (notmuch->path[strlen (notmuch->path) - 1] == '/') + notmuch->path[strlen (notmuch->path) - 1] = '\0'; + + try { + notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path, + Xapian::DB_CREATE_OR_OPEN); + notmuch->query_parser = new Xapian::QueryParser; + notmuch->term_gen = new Xapian::TermGenerator; + notmuch->term_gen->set_stemmer (Xapian::Stem ("english")); + + notmuch->query_parser->set_default_op (Xapian::Query::OP_AND); + notmuch->query_parser->set_database (*notmuch->xapian_db); + notmuch->query_parser->set_stemmer (Xapian::Stem ("english")); + notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME); + + for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) { + prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i]; + notmuch->query_parser->add_boolean_prefix (prefix->name, + prefix->prefix); + } + + for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) { + prefix_t *prefix = &PROBABILISTIC_PREFIX[i]; + notmuch->query_parser->add_prefix (prefix->name, prefix->prefix); + } + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred: %s\n", + error.get_msg().c_str()); + notmuch = NULL; + } + + DONE: + if (local_path) + free (local_path); + if (notmuch_path) + free (notmuch_path); + if (xapian_path) + free (xapian_path); + + return notmuch; +} + +void +notmuch_database_close (notmuch_database_t *notmuch) +{ + notmuch->xapian_db->flush (); + + delete notmuch->term_gen; + delete notmuch->query_parser; + delete notmuch->xapian_db; + talloc_free (notmuch); +} + +const char * +notmuch_database_get_path (notmuch_database_t *notmuch) +{ + return notmuch->path; +} + +static notmuch_private_status_t +find_timestamp_document (notmuch_database_t *notmuch, const char *db_key, + Xapian::Document *doc, unsigned int *doc_id) +{ + return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id); +} + +/* We allow the user to use arbitrarily long keys for timestamps, + * (they're for filesystem paths after all, which have no limit we + * know about). But we have a term-length limit. So if we exceed that, + * we'll use the SHA-1 of the user's key as the actual key for + * constructing a database term. + * + * Caution: This function returns a newly allocated string which the + * caller should free() when finished. + */ +static char * +timestamp_db_key (const char *key) +{ + int term_len = strlen (_find_prefix ("timestamp")) + strlen (key); + + if (term_len > NOTMUCH_TERM_MAX) + return notmuch_sha1_of_string (key); + else + return strdup (key); +} + +notmuch_status_t +notmuch_database_set_timestamp (notmuch_database_t *notmuch, + const char *key, time_t timestamp) +{ + Xapian::Document doc; + unsigned int doc_id; + notmuch_private_status_t status; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + char *db_key = NULL; + + db_key = timestamp_db_key (key); + + try { + status = find_timestamp_document (notmuch, db_key, &doc, &doc_id); + + doc.add_value (NOTMUCH_VALUE_TIMESTAMP, + Xapian::sortable_serialise (timestamp)); + + if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) { + char *term = talloc_asprintf (NULL, "%s%s", + _find_prefix ("timestamp"), db_key); + doc.add_term (term); + talloc_free (term); + + notmuch->xapian_db->add_document (doc); + } else { + notmuch->xapian_db->replace_document (doc_id, doc); + } + + } catch (Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred: %s.\n", + error.get_msg().c_str()); + ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + + if (db_key) + free (db_key); + + return ret; +} + +time_t +notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key) +{ + Xapian::Document doc; + unsigned int doc_id; + notmuch_private_status_t status; + char *db_key = NULL; + time_t ret = 0; + + db_key = timestamp_db_key (key); + + try { + status = find_timestamp_document (notmuch, db_key, &doc, &doc_id); + + if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) + goto DONE; + + ret = Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP)); + } catch (Xapian::Error &error) { + goto DONE; + } + + DONE: + if (db_key) + free (db_key); + + return ret; +} + +/* Find the thread ID to which the message with 'message_id' belongs. + * + * Returns NULL if no message with message ID 'message_id' is in the + * database. + * + * Otherwise, returns a newly talloced string belonging to 'ctx'. + */ +static const char * +_resolve_message_id_to_thread_id (notmuch_database_t *notmuch, + void *ctx, + const char *message_id) +{ + notmuch_message_t *message; + const char *ret = NULL; + + message = notmuch_database_find_message (notmuch, message_id); + if (message == NULL) + goto DONE; + + ret = talloc_steal (ctx, notmuch_message_get_thread_id (message)); + + DONE: + if (message) + notmuch_message_destroy (message); + + return ret; +} + +static notmuch_status_t +_merge_threads (notmuch_database_t *notmuch, + const char *winner_thread_id, + const char *loser_thread_id) +{ + Xapian::PostingIterator loser, loser_end; + notmuch_message_t *message = NULL; + notmuch_private_status_t private_status; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + + find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end); + + for ( ; loser != loser_end; loser++) { + message = _notmuch_message_create (notmuch, notmuch, + *loser, &private_status); + if (message == NULL) { + ret = COERCE_STATUS (private_status, + "Cannot find document for doc_id from query"); + goto DONE; + } + + _notmuch_message_remove_term (message, "thread", loser_thread_id); + _notmuch_message_add_term (message, "thread", winner_thread_id); + _notmuch_message_sync (message); + + notmuch_message_destroy (message); + message = NULL; + } + + DONE: + if (message) + notmuch_message_destroy (message); + + return ret; +} + +static void +_my_talloc_free_for_g_hash (void *ptr) +{ + talloc_free (ptr); +} + +static notmuch_status_t +_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch, + notmuch_message_t *message, + notmuch_message_file_t *message_file, + const char **thread_id) +{ + GHashTable *parents = NULL; + const char *refs, *in_reply_to; + GList *l, *keys = NULL; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + + parents = g_hash_table_new_full (g_str_hash, g_str_equal, + _my_talloc_free_for_g_hash, NULL); + + refs = notmuch_message_file_get_header (message_file, "references"); + parse_references (message, parents, refs); + + in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to"); + parse_references (message, parents, in_reply_to); + _notmuch_message_add_term (message, "replyto", + parse_message_id (message, in_reply_to, NULL)); + + keys = g_hash_table_get_keys (parents); + for (l = keys; l; l = l->next) { + char *parent_message_id; + const char *parent_thread_id; + + parent_message_id = (char *) l->data; + parent_thread_id = _resolve_message_id_to_thread_id (notmuch, + message, + parent_message_id); + + if (parent_thread_id == NULL) { + _notmuch_message_add_term (message, "ref", parent_message_id); + } else { + if (*thread_id == NULL) { + *thread_id = talloc_strdup (message, parent_thread_id); + _notmuch_message_add_term (message, "thread", *thread_id); + } else if (strcmp (*thread_id, parent_thread_id)) { + ret = _merge_threads (notmuch, *thread_id, parent_thread_id); + if (ret) + goto DONE; + } + } + } + + DONE: + if (keys) + g_list_free (keys); + if (parents) + g_hash_table_unref (parents); + + return ret; +} + +static notmuch_status_t +_notmuch_database_link_message_to_children (notmuch_database_t *notmuch, + notmuch_message_t *message, + const char **thread_id) +{ + const char *message_id = notmuch_message_get_message_id (message); + Xapian::PostingIterator child, children_end; + notmuch_message_t *child_message = NULL; + const char *child_thread_id; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + notmuch_private_status_t private_status; + + find_doc_ids (notmuch, "ref", message_id, &child, &children_end); + + for ( ; child != children_end; child++) { + + child_message = _notmuch_message_create (message, notmuch, + *child, &private_status); + if (child_message == NULL) { + ret = COERCE_STATUS (private_status, + "Cannot find document for doc_id from query"); + goto DONE; + } + + child_thread_id = notmuch_message_get_thread_id (child_message); + if (*thread_id == NULL) { + *thread_id = talloc_strdup (message, child_thread_id); + _notmuch_message_add_term (message, "thread", *thread_id); + } else if (strcmp (*thread_id, child_thread_id)) { + _notmuch_message_remove_term (child_message, "ref", + message_id); + _notmuch_message_sync (child_message); + ret = _merge_threads (notmuch, *thread_id, child_thread_id); + if (ret) + goto DONE; + } + + notmuch_message_destroy (child_message); + child_message = NULL; + } + + DONE: + if (child_message) + notmuch_message_destroy (child_message); + + return ret; +} + +/* Given a (mostly empty) 'message' and its corresponding + * 'message_file' link it to existing threads in the database. + * + * We first looke at 'message_file' and its link-relevant headers + * (References and In-Reply-To) for message IDs. We also look in the + * database for existing message that reference 'message'.p + * + * The end result is to call _notmuch_message_add_thread_id with one + * or more thread IDs to which this message belongs, (including + * generating a new thread ID if necessary if the message doesn't + * connect to any existing threads). + */ +static notmuch_status_t +_notmuch_database_link_message (notmuch_database_t *notmuch, + notmuch_message_t *message, + notmuch_message_file_t *message_file) +{ + notmuch_status_t status; + const char *thread_id = NULL; + + status = _notmuch_database_link_message_to_parents (notmuch, message, + message_file, + &thread_id); + if (status) + return status; + + status = _notmuch_database_link_message_to_children (notmuch, message, + &thread_id); + if (status) + return status; + + if (thread_id == NULL) + _notmuch_message_ensure_thread_id (message); + + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +notmuch_database_add_message (notmuch_database_t *notmuch, + const char *filename, + notmuch_message_t **message_ret) +{ + notmuch_message_file_t *message_file; + notmuch_message_t *message = NULL; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + notmuch_private_status_t private_status; + + const char *date, *header; + const char *from, *to, *subject; + char *message_id; + + if (message_ret) + *message_ret = NULL; + + message_file = notmuch_message_file_open (filename); + if (message_file == NULL) { + ret = NOTMUCH_STATUS_FILE_ERROR; + goto DONE; + } + + notmuch_message_file_restrict_headers (message_file, + "date", + "from", + "in-reply-to", + "message-id", + "references", + "subject", + "to", + (char *) NULL); + + try { + /* Before we do any real work, (especially before doing a + * potential SHA-1 computation on the entire file's contents), + * let's make sure that what we're looking at looks like an + * actual email message. + */ + from = notmuch_message_file_get_header (message_file, "from"); + subject = notmuch_message_file_get_header (message_file, "subject"); + to = notmuch_message_file_get_header (message_file, "to"); + + if (from == NULL && + subject == NULL && + to == NULL) + { + ret = NOTMUCH_STATUS_FILE_NOT_EMAIL; + goto DONE; + } + + /* Now that we're sure it's mail, the first order of business + * is to find a message ID (or else create one ourselves). */ + + header = notmuch_message_file_get_header (message_file, "message-id"); + if (header) { + message_id = parse_message_id (message_file, header, NULL); + /* So the header value isn't RFC-compliant, but it's + * better than no message-id at all. */ + if (message_id == NULL) + message_id = talloc_strdup (message_file, header); + } else { + /* No message-id at all, let's generate one by taking a + * hash over the file's contents. */ + char *sha1 = notmuch_sha1_of_file (filename); + + /* If that failed too, something is really wrong. Give up. */ + if (sha1 == NULL) { + ret = NOTMUCH_STATUS_FILE_ERROR; + goto DONE; + } + + message_id = talloc_asprintf (message_file, + "notmuch-sha1-%s", sha1); + free (sha1); + } + + /* Now that we have a message ID, we get a message object, + * (which may or may not reference an existing document in the + * database). */ + + /* Use NULL for owner since we want to free this locally. */ + message = _notmuch_message_create_for_message_id (NULL, + notmuch, + message_id, + &private_status); + + talloc_free (message_id); + + if (message == NULL) + goto DONE; + + /* Is this a newly created message object? */ + if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) { + _notmuch_message_set_filename (message, filename); + _notmuch_message_add_term (message, "type", "mail"); + } else { + ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID; + goto DONE; + } + + ret = _notmuch_database_link_message (notmuch, message, message_file); + if (ret) + goto DONE; + + date = notmuch_message_file_get_header (message_file, "date"); + _notmuch_message_set_date (message, date); + + _notmuch_message_index_file (message, filename); + + _notmuch_message_sync (message); + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred: %s.\n", + error.get_msg().c_str()); + ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION; + goto DONE; + } + + DONE: + if (message) { + if (ret == NOTMUCH_STATUS_SUCCESS && message_ret) + *message_ret = message; + else + notmuch_message_destroy (message); + } + + if (message_file) + notmuch_message_file_close (message_file); + + return ret; +} diff --git a/lib/index.cc b/lib/index.cc new file mode 100644 index 00000000..747a4e63 --- /dev/null +++ b/lib/index.cc @@ -0,0 +1,326 @@ +/* + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#include "notmuch-private.h" + +#include + +#include + +/* We're finally down to a single (NAME + address) email "mailbox". */ +static void +_index_address_mailbox (notmuch_message_t *message, + const char *prefix_name, + InternetAddress *address) +{ + InternetAddressMailbox *mailbox = INTERNET_ADDRESS_MAILBOX (address); + const char *name, *addr; + char *contact; + int own_name = 0; + + name = internet_address_get_name (address); + addr = internet_address_mailbox_get_addr (mailbox); + + if (addr) { + if (name) { + contact = talloc_asprintf (message, "\"%s\" <%s>", + name, addr); + _notmuch_message_add_term (message, "contact", contact); + talloc_free (contact); + } else { + _notmuch_message_add_term (message, "contact", addr); + } + } + + /* In the absence of a name, we'll strip the part before the @ + * from the address. */ + if (! name) { + const char *at; + + at = strchr (addr, '@'); + if (at) { + name = strndup (addr, at - addr); + own_name = 1; + } + } + + if (name) + _notmuch_message_gen_terms (message, prefix_name, name); + if (addr) + _notmuch_message_gen_terms (message, prefix_name, addr); +} + +static void +_index_address_list (notmuch_message_t *message, + const char *prefix_name, + InternetAddressList *addresses); + +/* The outer loop over the InternetAddressList wasn't quite enough. + * There can actually be a tree here where a single member of the list + * is a "group" containing another list. Recurse please. + */ +static void +_index_address_group (notmuch_message_t *message, + const char *prefix_name, + InternetAddress *address) +{ + InternetAddressGroup *group; + InternetAddressList *list; + + group = INTERNET_ADDRESS_GROUP (address); + list = internet_address_group_get_members (group); + + if (! list) + return; + + _index_address_list (message, prefix_name, list); +} + +static void +_index_address_list (notmuch_message_t *message, + const char *prefix_name, + InternetAddressList *addresses) +{ + int i; + InternetAddress *address; + + if (addresses == NULL) + return; + + for (i = 0; i < internet_address_list_length (addresses); i++) { + address = internet_address_list_get_address (addresses, i); + if (INTERNET_ADDRESS_IS_MAILBOX (address)) { + _index_address_mailbox (message, prefix_name, address); + } else if (INTERNET_ADDRESS_IS_GROUP (address)) { + _index_address_group (message, prefix_name, address); + } else { + INTERNAL_ERROR ("GMime InternetAddress is neither a mailbox nor a group.\n"); + } + } +} + +static const char * +skip_re_in_subject (const char *subject) +{ + const char *s = subject; + + if (subject == NULL) + return NULL; + + while (*s) { + while (*s && isspace (*s)) + s++; + if (strncasecmp (s, "re:", 3) == 0) + s += 3; + else + break; + } + + return s; +} + +/* Given a string representing the body of a message, generate terms + * for it, (skipping quoted portions and signatures). + * + * This function is evil in that it modifies the string passed to it, + * (changing some newlines into '\0'). + */ +static void +_index_body_text (notmuch_message_t *message, char *body) +{ + char *line, *line_end, *next_line; + + if (body == NULL) + return; + + next_line = body; + + while (1) { + line = next_line; + if (*line == '\0') + break; + + next_line = strchr (line, '\n'); + if (next_line == NULL) { + next_line = line + strlen (line); + } + line_end = next_line - 1; + + /* Get to the next non-blank line. */ + while (*next_line == '\n') + next_line++; + + /* Skip blank lines. */ + if (line_end < line) + continue; + + /* Skip lines that are quotes. */ + if (*line == '>') + continue; + + /* Also skip lines introducing a quote on the next line. */ + if (*line_end == ':' && *next_line == '>') + continue; + + /* Finally, bail as soon as we see a signature. */ + /* XXX: Should only do this if "near" the end of the message. */ + if (strncmp (line, "-- ", 3) == 0) + break; + + *(line_end + 1) = '\0'; + + _notmuch_message_gen_terms (message, NULL, line); + } +} + +/* Callback to generate terms for each mime part of a message. */ +static void +_index_mime_part (notmuch_message_t *message, + GMimeObject *part) +{ + GMimeStream *stream; + GMimeDataWrapper *wrapper; + GByteArray *byte_array; + GMimeContentDisposition *disposition; + char *body; + + if (GMIME_IS_MULTIPART (part)) { + GMimeMultipart *multipart = GMIME_MULTIPART (part); + int i; + + for (i = 0; i < g_mime_multipart_get_count (multipart); i++) { + if (GMIME_IS_MULTIPART_SIGNED (multipart)) { + /* Don't index the signature. */ + if (i == 1) + continue; + if (i > 1) + fprintf (stderr, "Warning: Unexpected extra parts of mutlipart/signed. Indexing anyway.\n"); + } + _index_mime_part (message, + g_mime_multipart_get_part (multipart, i)); + } + return; + } + + if (GMIME_IS_MESSAGE_PART (part)) { + GMimeMessage *mime_message; + + mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part)); + + _index_mime_part (message, g_mime_message_get_mime_part (mime_message)); + + return; + } + + if (! (GMIME_IS_PART (part))) { + fprintf (stderr, "Warning: Not indexing unknown mime part: %s.\n", + g_type_name (G_OBJECT_TYPE (part))); + return; + } + + disposition = g_mime_object_get_content_disposition (part); + if (disposition && + strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0) + { + const char *filename = g_mime_part_get_filename (GMIME_PART (part)); + + _notmuch_message_add_term (message, "tag", "attachment"); + _notmuch_message_gen_terms (message, "attachment", filename); + + /* XXX: Would be nice to call out to something here to parse + * the attachment into text and then index that. */ + return; + } + + byte_array = g_byte_array_new (); + + stream = g_mime_stream_mem_new_with_byte_array (byte_array); + g_mime_stream_mem_set_owner (GMIME_STREAM_MEM (stream), FALSE); + wrapper = g_mime_part_get_content_object (GMIME_PART (part)); + if (wrapper) + g_mime_data_wrapper_write_to_stream (wrapper, stream); + + g_object_unref (stream); + + g_byte_array_append (byte_array, (guint8 *) "\0", 1); + body = (char *) g_byte_array_free (byte_array, FALSE); + + _index_body_text (message, body); + + free (body); +} + +notmuch_status_t +_notmuch_message_index_file (notmuch_message_t *message, + const char *filename) +{ + GMimeStream *stream = NULL; + GMimeParser *parser = NULL; + GMimeMessage *mime_message = NULL; + InternetAddressList *addresses; + FILE *file = NULL; + const char *from, *subject; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + static int initialized = 0; + + if (! initialized) { + g_mime_init (0); + initialized = 1; + } + + file = fopen (filename, "r"); + if (! file) { + fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno)); + ret = NOTMUCH_STATUS_FILE_ERROR; + goto DONE; + } + + /* Evil GMime steals my FILE* here so I won't fclose it. */ + stream = g_mime_stream_file_new (file); + + parser = g_mime_parser_new_with_stream (stream); + + mime_message = g_mime_parser_construct_message (parser); + + from = g_mime_message_get_sender (mime_message); + addresses = internet_address_list_parse_string (from); + + _index_address_list (message, "from", addresses); + + addresses = g_mime_message_get_all_recipients (mime_message); + _index_address_list (message, "to", addresses); + + subject = g_mime_message_get_subject (mime_message); + subject = skip_re_in_subject (subject); + _notmuch_message_gen_terms (message, "subject", subject); + + _index_mime_part (message, g_mime_message_get_mime_part (mime_message)); + + DONE: + if (mime_message) + g_object_unref (mime_message); + + if (parser) + g_object_unref (parser); + + if (stream) + g_object_unref (stream); + + return ret; +} diff --git a/lib/libsha1.c b/lib/libsha1.c new file mode 100644 index 00000000..c39a5a17 --- /dev/null +++ b/lib/libsha1.c @@ -0,0 +1,242 @@ +/* + --------------------------------------------------------------------------- + Copyright (c) 2002, Dr Brian Gladman, Worcester, UK. All rights reserved. + + LICENSE TERMS + + The free distribution and use of this software in both source and binary + form is allowed (with or without changes) provided that: + + 1. distributions of this source code include the above copyright + notice, this list of conditions and the following disclaimer; + + 2. distributions in binary form include the above copyright + notice, this list of conditions and the following disclaimer + in the documentation and/or other associated materials; + + 3. the copyright holder's name is not used to endorse products + built using this software without specific written permission. + + ALTERNATIVELY, provided that this notice is retained in full, this product + may be distributed under the terms of the GNU General Public License (GPL), + in which case the provisions of the GPL apply INSTEAD OF those given above. + + DISCLAIMER + + This software is provided 'as is' with no explicit or implied warranties + in respect of its properties, including, but not limited to, correctness + and/or fitness for purpose. + --------------------------------------------------------------------------- + Issue Date: 01/08/2005 + + This is a byte oriented version of SHA1 that operates on arrays of bytes + stored in memory. +*/ + +#include /* for memcpy() etc. */ + +#include "libsha1.h" + +#if defined(__cplusplus) +extern "C" +{ +#endif + +#define SHA1_BLOCK_SIZE 64 + +#define rotl32(x,n) (((x) << n) | ((x) >> (32 - n))) +#define rotr32(x,n) (((x) >> n) | ((x) << (32 - n))) + +#define bswap_32(x) ((rotr32((x), 24) & 0x00ff00ff) | (rotr32((x), 8) & 0xff00ff00)) + +#if (PLATFORM_BYTE_ORDER == IS_LITTLE_ENDIAN) +#define bsw_32(p,n) \ + { int _i = (n); while(_i--) ((uint32_t*)p)[_i] = bswap_32(((uint32_t*)p)[_i]); } +#else +#define bsw_32(p,n) +#endif + +#define SHA1_MASK (SHA1_BLOCK_SIZE - 1) + +#if 0 + +#define ch(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define parity(x,y,z) ((x) ^ (y) ^ (z)) +#define maj(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) + +#else /* Discovered by Rich Schroeppel and Colin Plumb */ + +#define ch(x,y,z) ((z) ^ ((x) & ((y) ^ (z)))) +#define parity(x,y,z) ((x) ^ (y) ^ (z)) +#define maj(x,y,z) (((x) & (y)) | ((z) & ((x) ^ (y)))) + +#endif + +/* Compile 64 bytes of hash data into SHA1 context. Note */ +/* that this routine assumes that the byte order in the */ +/* ctx->wbuf[] at this point is in such an order that low */ +/* address bytes in the ORIGINAL byte stream will go in */ +/* this buffer to the high end of 32-bit words on BOTH big */ +/* and little endian systems */ + +#ifdef ARRAY +#define q(v,n) v[n] +#else +#define q(v,n) v##n +#endif + +#define one_cycle(v,a,b,c,d,e,f,k,h) \ + q(v,e) += rotr32(q(v,a),27) + \ + f(q(v,b),q(v,c),q(v,d)) + k + h; \ + q(v,b) = rotr32(q(v,b), 2) + +#define five_cycle(v,f,k,i) \ + one_cycle(v, 0,1,2,3,4, f,k,hf(i )); \ + one_cycle(v, 4,0,1,2,3, f,k,hf(i+1)); \ + one_cycle(v, 3,4,0,1,2, f,k,hf(i+2)); \ + one_cycle(v, 2,3,4,0,1, f,k,hf(i+3)); \ + one_cycle(v, 1,2,3,4,0, f,k,hf(i+4)) + +static void sha1_compile(sha1_ctx ctx[1]) +{ uint32_t *w = ctx->wbuf; + +#ifdef ARRAY + uint32_t v[5]; + memcpy(v, ctx->hash, 5 * sizeof(uint32_t)); +#else + uint32_t v0, v1, v2, v3, v4; + v0 = ctx->hash[0]; v1 = ctx->hash[1]; + v2 = ctx->hash[2]; v3 = ctx->hash[3]; + v4 = ctx->hash[4]; +#endif + +#define hf(i) w[i] + + five_cycle(v, ch, 0x5a827999, 0); + five_cycle(v, ch, 0x5a827999, 5); + five_cycle(v, ch, 0x5a827999, 10); + one_cycle(v,0,1,2,3,4, ch, 0x5a827999, hf(15)); \ + +#undef hf +#define hf(i) (w[(i) & 15] = rotl32( \ + w[((i) + 13) & 15] ^ w[((i) + 8) & 15] \ + ^ w[((i) + 2) & 15] ^ w[(i) & 15], 1)) + + one_cycle(v,4,0,1,2,3, ch, 0x5a827999, hf(16)); + one_cycle(v,3,4,0,1,2, ch, 0x5a827999, hf(17)); + one_cycle(v,2,3,4,0,1, ch, 0x5a827999, hf(18)); + one_cycle(v,1,2,3,4,0, ch, 0x5a827999, hf(19)); + + five_cycle(v, parity, 0x6ed9eba1, 20); + five_cycle(v, parity, 0x6ed9eba1, 25); + five_cycle(v, parity, 0x6ed9eba1, 30); + five_cycle(v, parity, 0x6ed9eba1, 35); + + five_cycle(v, maj, 0x8f1bbcdc, 40); + five_cycle(v, maj, 0x8f1bbcdc, 45); + five_cycle(v, maj, 0x8f1bbcdc, 50); + five_cycle(v, maj, 0x8f1bbcdc, 55); + + five_cycle(v, parity, 0xca62c1d6, 60); + five_cycle(v, parity, 0xca62c1d6, 65); + five_cycle(v, parity, 0xca62c1d6, 70); + five_cycle(v, parity, 0xca62c1d6, 75); + +#ifdef ARRAY + ctx->hash[0] += v[0]; ctx->hash[1] += v[1]; + ctx->hash[2] += v[2]; ctx->hash[3] += v[3]; + ctx->hash[4] += v[4]; +#else + ctx->hash[0] += v0; ctx->hash[1] += v1; + ctx->hash[2] += v2; ctx->hash[3] += v3; + ctx->hash[4] += v4; +#endif +} + +void sha1_begin(sha1_ctx ctx[1]) +{ + ctx->count[0] = ctx->count[1] = 0; + ctx->hash[0] = 0x67452301; + ctx->hash[1] = 0xefcdab89; + ctx->hash[2] = 0x98badcfe; + ctx->hash[3] = 0x10325476; + ctx->hash[4] = 0xc3d2e1f0; +} + +/* SHA1 hash data in an array of bytes into hash buffer and */ +/* call the hash_compile function as required. */ + +void sha1_hash(const unsigned char data[], unsigned long len, sha1_ctx ctx[1]) +{ uint32_t pos = (uint32_t)(ctx->count[0] & SHA1_MASK), + space = SHA1_BLOCK_SIZE - pos; + const unsigned char *sp = data; + + if((ctx->count[0] += len) < len) + ++(ctx->count[1]); + + while(len >= space) /* tranfer whole blocks if possible */ + { + memcpy(((unsigned char*)ctx->wbuf) + pos, sp, space); + sp += space; len -= space; space = SHA1_BLOCK_SIZE; pos = 0; + bsw_32(ctx->wbuf, SHA1_BLOCK_SIZE >> 2); + sha1_compile(ctx); + } + + memcpy(((unsigned char*)ctx->wbuf) + pos, sp, len); +} + +/* SHA1 final padding and digest calculation */ + +void sha1_end(unsigned char hval[], sha1_ctx ctx[1]) +{ uint32_t i = (uint32_t)(ctx->count[0] & SHA1_MASK); + + /* put bytes in the buffer in an order in which references to */ + /* 32-bit words will put bytes with lower addresses into the */ + /* top of 32 bit words on BOTH big and little endian machines */ + bsw_32(ctx->wbuf, (i + 3) >> 2); + + /* we now need to mask valid bytes and add the padding which is */ + /* a single 1 bit and as many zero bits as necessary. Note that */ + /* we can always add the first padding byte here because the */ + /* buffer always has at least one empty slot */ + ctx->wbuf[i >> 2] &= 0xffffff80 << 8 * (~i & 3); + ctx->wbuf[i >> 2] |= 0x00000080 << 8 * (~i & 3); + + /* we need 9 or more empty positions, one for the padding byte */ + /* (above) and eight for the length count. If there is not */ + /* enough space, pad and empty the buffer */ + if(i > SHA1_BLOCK_SIZE - 9) + { + if(i < 60) ctx->wbuf[15] = 0; + sha1_compile(ctx); + i = 0; + } + else /* compute a word index for the empty buffer positions */ + i = (i >> 2) + 1; + + while(i < 14) /* and zero pad all but last two positions */ + ctx->wbuf[i++] = 0; + + /* the following 32-bit length fields are assembled in the */ + /* wrong byte order on little endian machines but this is */ + /* corrected later since they are only ever used as 32-bit */ + /* word values. */ + ctx->wbuf[14] = (ctx->count[1] << 3) | (ctx->count[0] >> 29); + ctx->wbuf[15] = ctx->count[0] << 3; + sha1_compile(ctx); + + /* extract the hash value as bytes in case the hash buffer is */ + /* misaligned for 32-bit words */ + for(i = 0; i < SHA1_DIGEST_SIZE; ++i) + hval[i] = (unsigned char)(ctx->hash[i >> 2] >> (8 * (~i & 3))); +} + +void sha1(unsigned char hval[], const unsigned char data[], unsigned long len) +{ sha1_ctx cx[1]; + + sha1_begin(cx); sha1_hash(data, len, cx); sha1_end(hval, cx); +} + +#if defined(__cplusplus) +} +#endif diff --git a/lib/libsha1.h b/lib/libsha1.h new file mode 100644 index 00000000..b4dca93b --- /dev/null +++ b/lib/libsha1.h @@ -0,0 +1,67 @@ +/* + --------------------------------------------------------------------------- + Copyright (c) 2002, Dr Brian Gladman, Worcester, UK. All rights reserved. + + LICENSE TERMS + + The free distribution and use of this software in both source and binary + form is allowed (with or without changes) provided that: + + 1. distributions of this source code include the above copyright + notice, this list of conditions and the following disclaimer; + + 2. distributions in binary form include the above copyright + notice, this list of conditions and the following disclaimer + in the documentation and/or other associated materials; + + 3. the copyright holder's name is not used to endorse products + built using this software without specific written permission. + + ALTERNATIVELY, provided that this notice is retained in full, this product + may be distributed under the terms of the GNU General Public License (GPL), + in which case the provisions of the GPL apply INSTEAD OF those given above. + + DISCLAIMER + + This software is provided 'as is' with no explicit or implied warranties + in respect of its properties, including, but not limited to, correctness + and/or fitness for purpose. + --------------------------------------------------------------------------- + Issue Date: 01/08/2005 +*/ + +#ifndef _SHA1_H +#define _SHA1_H + +#if defined(__cplusplus) +extern "C" +{ +#endif +#if 0 +} /* Appleasing Emacs */ +#endif + +#include + +/* Size of SHA1 digest */ + +#define SHA1_DIGEST_SIZE 20 + +/* type to hold the SHA1 context */ + +typedef struct +{ uint32_t count[2]; + uint32_t hash[5]; + uint32_t wbuf[16]; +} sha1_ctx; + +void sha1_begin(sha1_ctx ctx[1]); +void sha1_hash(const unsigned char data[], unsigned long len, sha1_ctx ctx[1]); +void sha1_end(unsigned char hval[], sha1_ctx ctx[1]); +void sha1(unsigned char hval[], const unsigned char data[], unsigned long len); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/lib/message-file.c b/lib/message-file.c new file mode 100644 index 00000000..75caba6d --- /dev/null +++ b/lib/message-file.c @@ -0,0 +1,352 @@ +/* message.c - Utility functions for parsing an email message for notmuch. + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#include + +#include "notmuch-private.h" + +#include + +#include /* GHashTable */ + +typedef struct { + char *str; + size_t size; + size_t len; +} header_value_closure_t; + +struct _notmuch_message_file { + /* File object */ + FILE *file; + + /* Header storage */ + int restrict_headers; + GHashTable *headers; + int broken_headers; + int good_headers; + size_t header_size; /* Length of full message header in bytes. */ + + /* Parsing state */ + char *line; + size_t line_size; + header_value_closure_t value; + + int parsing_started; + int parsing_finished; +}; + +static int +strcase_equal (const void *a, const void *b) +{ + return strcasecmp (a, b) == 0; +} + +static unsigned int +strcase_hash (const void *ptr) +{ + const char *s = ptr; + + /* This is the djb2 hash. */ + unsigned int hash = 5381; + while (s && *s) { + hash = ((hash << 5) + hash) + tolower (*s); + s++; + } + + return hash; +} + +static int +_notmuch_message_file_destructor (notmuch_message_file_t *message) +{ + if (message->line) + free (message->line); + + if (message->value.size) + free (message->value.str); + + if (message->headers) + g_hash_table_destroy (message->headers); + + if (message->file) + fclose (message->file); + + return 0; +} + +/* Create a new notmuch_message_file_t for 'filename' with 'ctx' as + * the talloc owner. */ +notmuch_message_file_t * +_notmuch_message_file_open_ctx (void *ctx, const char *filename) +{ + notmuch_message_file_t *message; + + message = talloc_zero (ctx, notmuch_message_file_t); + if (unlikely (message == NULL)) + return NULL; + + talloc_set_destructor (message, _notmuch_message_file_destructor); + + message->file = fopen (filename, "r"); + if (message->file == NULL) + goto FAIL; + + message->headers = g_hash_table_new_full (strcase_hash, + strcase_equal, + free, + free); + + message->parsing_started = 0; + message->parsing_finished = 0; + + return message; + + FAIL: + fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno)); + notmuch_message_file_close (message); + + return NULL; +} + +notmuch_message_file_t * +notmuch_message_file_open (const char *filename) +{ + return _notmuch_message_file_open_ctx (NULL, filename); +} + +void +notmuch_message_file_close (notmuch_message_file_t *message) +{ + talloc_free (message); +} + +void +notmuch_message_file_restrict_headersv (notmuch_message_file_t *message, + va_list va_headers) +{ + char *header; + + if (message->parsing_started) + INTERNAL_ERROR ("notmuch_message_file_restrict_headers called after parsing has started"); + + while (1) { + header = va_arg (va_headers, char*); + if (header == NULL) + break; + g_hash_table_insert (message->headers, + xstrdup (header), NULL); + } + + message->restrict_headers = 1; +} + +void +notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...) +{ + va_list va_headers; + + va_start (va_headers, message); + + notmuch_message_file_restrict_headersv (message, va_headers); +} + +static void +copy_header_unfolding (header_value_closure_t *value, + const char *chunk) +{ + char *last; + + if (chunk == NULL) + return; + + while (*chunk == ' ' || *chunk == '\t') + chunk++; + + if (value->len + 1 + strlen (chunk) + 1 > value->size) { + unsigned int new_size = value->size; + if (value->size == 0) + new_size = strlen (chunk) + 1; + else + while (value->len + 1 + strlen (chunk) + 1 > new_size) + new_size *= 2; + value->str = xrealloc (value->str, new_size); + value->size = new_size; + } + + last = value->str + value->len; + if (value->len) { + *last = ' '; + last++; + value->len++; + } + + strcpy (last, chunk); + value->len += strlen (chunk); + + last = value->str + value->len - 1; + if (*last == '\n') { + *last = '\0'; + value->len--; + } +} + +/* As a special-case, a value of NULL for header_desired will force + * the entire header to be parsed if it is not parsed already. This is + * used by the _notmuch_message_file_get_headers_end function. */ +const char * +notmuch_message_file_get_header (notmuch_message_file_t *message, + const char *header_desired) +{ + int contains; + char *header, *decoded_value; + const char *s, *colon; + int match; + static int initialized = 0; + + if (! initialized) { + g_mime_init (0); + initialized = 1; + } + + message->parsing_started = 1; + + if (header_desired == NULL) + contains = 0; + else + contains = g_hash_table_lookup_extended (message->headers, + header_desired, NULL, + (gpointer *) &decoded_value); + + if (contains && decoded_value) + return decoded_value; + + if (message->parsing_finished) + return NULL; + +#define NEXT_HEADER_LINE(closure) \ + while (1) { \ + ssize_t bytes_read = getline (&message->line, \ + &message->line_size, \ + message->file); \ + if (bytes_read == -1) { \ + message->parsing_finished = 1; \ + break; \ + } \ + if (*message->line == '\n') { \ + message->parsing_finished = 1; \ + break; \ + } \ + if (closure && \ + (*message->line == ' ' || *message->line == '\t')) \ + { \ + copy_header_unfolding ((closure), message->line); \ + } \ + if (*message->line == ' ' || *message->line == '\t') \ + message->header_size += strlen (message->line); \ + else \ + break; \ + } + + if (message->line == NULL) + NEXT_HEADER_LINE (NULL); + + while (1) { + + if (message->parsing_finished) + break; + + colon = strchr (message->line, ':'); + + if (colon == NULL) { + message->broken_headers++; + /* A simple heuristic for giving up on things that just + * don't look like mail messages. */ + if (message->broken_headers >= 10 && + message->good_headers < 5) + { + message->parsing_finished = 1; + continue; + } + NEXT_HEADER_LINE (NULL); + continue; + } + + message->header_size += strlen (message->line); + + message->good_headers++; + + header = xstrndup (message->line, colon - message->line); + + if (message->restrict_headers && + ! g_hash_table_lookup_extended (message->headers, + header, NULL, NULL)) + { + free (header); + NEXT_HEADER_LINE (NULL); + continue; + } + + s = colon + 1; + while (*s == ' ' || *s == '\t') + s++; + + message->value.len = 0; + copy_header_unfolding (&message->value, s); + + NEXT_HEADER_LINE (&message->value); + + if (header_desired == 0) + match = 0; + else + match = (strcasecmp (header, header_desired) == 0); + + decoded_value = g_mime_utils_header_decode_text (message->value.str); + + g_hash_table_insert (message->headers, header, decoded_value); + + if (match) + return decoded_value; + } + + if (message->line) + free (message->line); + message->line = NULL; + + if (message->value.size) { + free (message->value.str); + message->value.str = NULL; + message->value.size = 0; + message->value.len = 0; + } + + /* We've parsed all headers and never found the one we're looking + * for. It's probably just not there, but let's check that we + * didn't make a mistake preventing us from seeing it. */ + if (message->restrict_headers && header_desired && + ! g_hash_table_lookup_extended (message->headers, + header_desired, NULL, NULL)) + { + INTERNAL_ERROR ("Attempt to get header \"%s\" which was not\n" + "included in call to notmuch_message_file_restrict_headers\n", + header_desired); + } + + return NULL; +} diff --git a/lib/message.cc b/lib/message.cc new file mode 100644 index 00000000..28f19a88 --- /dev/null +++ b/lib/message.cc @@ -0,0 +1,658 @@ +/* message.cc - Results of message-based searches from a notmuch database + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#include "notmuch-private.h" +#include "database-private.h" + +#include + +#include + +struct _notmuch_message { + notmuch_database_t *notmuch; + Xapian::docid doc_id; + int frozen; + char *message_id; + char *thread_id; + char *filename; + notmuch_message_file_t *message_file; + Xapian::Document doc; +}; + +/* "128 bits of thread-id ought to be enough for anybody" */ +#define NOTMUCH_THREAD_ID_BITS 128 +#define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4) +typedef struct _thread_id { + char str[NOTMUCH_THREAD_ID_DIGITS + 1]; +} thread_id_t; + +/* We end up having to call the destructor explicitly because we had + * to use "placement new" in order to initialize C++ objects within a + * block that we allocated with talloc. So C++ is making talloc + * slightly less simple to use, (we wouldn't need + * talloc_set_destructor at all otherwise). + */ +static int +_notmuch_message_destructor (notmuch_message_t *message) +{ + message->doc.~Document (); + + return 0; +} + +/* Create a new notmuch_message_t object for an existing document in + * the database. + * + * Here, 'talloc owner' is an optional talloc context to which the new + * message will belong. This allows for the caller to not bother + * calling notmuch_message_destroy on the message, and no that all + * memory will be reclaimed with 'talloc_owner' is free. The caller + * still can call notmuch_message_destroy when finished with the + * message if desired. + * + * The 'talloc_owner' argument can also be NULL, in which case the + * caller *is* responsible for calling notmuch_message_destroy. + * + * If no document exists in the database with document ID of 'doc_id' + * then this function returns NULL and optionally sets *status to + * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND. + * + * This function can also fail to due lack of available memory, + * returning NULL and optionally setting *status to + * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY. + * + * The caller can pass NULL for status if uninterested in + * distinguishing these two cases. + */ +notmuch_message_t * +_notmuch_message_create (const void *talloc_owner, + notmuch_database_t *notmuch, + unsigned int doc_id, + notmuch_private_status_t *status) +{ + notmuch_message_t *message; + + if (status) + *status = NOTMUCH_PRIVATE_STATUS_SUCCESS; + + message = talloc (talloc_owner, notmuch_message_t); + if (unlikely (message == NULL)) { + if (status) + *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY; + return NULL; + } + + message->notmuch = notmuch; + message->doc_id = doc_id; + + message->frozen = 0; + + /* Each of these will be lazily created as needed. */ + message->message_id = NULL; + message->thread_id = NULL; + message->filename = NULL; + message->message_file = NULL; + + /* This is C++'s creepy "placement new", which is really just an + * ugly way to call a constructor for a pre-allocated object. So + * it's really not an error to not be checking for OUT_OF_MEMORY + * here, since this "new" isn't actually allocating memory. This + * is language-design comedy of the wrong kind. */ + + new (&message->doc) Xapian::Document; + + talloc_set_destructor (message, _notmuch_message_destructor); + + try { + message->doc = notmuch->xapian_db->get_document (doc_id); + } catch (const Xapian::DocNotFoundError &error) { + talloc_free (message); + if (status) + *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND; + return NULL; + } + + return message; +} + +/* Create a new notmuch_message_t object for a specific message ID, + * (which may or may not already exist in the databas). + * + * Here, 'talloc owner' is an optional talloc context to which the new + * message will belong. This allows for the caller to not bother + * calling notmuch_message_destroy on the message, and no that all + * memory will be reclaimed with 'talloc_owner' is free. The caller + * still can call notmuch_message_destroy when finished with the + * message if desired. + * + * The 'talloc_owner' argument can also be NULL, in which case the + * caller *is* responsible for calling notmuch_message_destroy. + * + * If there is already a document with message ID 'message_id' in the + * database, then the returned message can be used to query/modify the + * document. Otherwise, a new document will be inserted into the + * database before this function returns, (and *status will be set + * to NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND). + * + * If an error occurs, this function will return NULL and *status + * will be set as appropriate. (The status pointer argument must + * not be NULL.) + */ +notmuch_message_t * +_notmuch_message_create_for_message_id (const void *talloc_owner, + notmuch_database_t *notmuch, + const char *message_id, + notmuch_private_status_t *status_ret) +{ + notmuch_message_t *message; + Xapian::Document doc; + unsigned int doc_id; + char *term; + + *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS; + + message = notmuch_database_find_message (notmuch, message_id); + if (message) + return talloc_steal (talloc_owner, message); + + term = talloc_asprintf (NULL, "%s%s", + _find_prefix ("id"), message_id); + if (term == NULL) { + *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY; + return NULL; + } + + try { + doc.add_term (term); + talloc_free (term); + + doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id); + + doc_id = notmuch->xapian_db->add_document (doc); + } catch (const Xapian::Error &error) { + *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION; + return NULL; + } + + message = _notmuch_message_create (talloc_owner, notmuch, + doc_id, status_ret); + + /* We want to inform the caller that we had to create a new + * document. */ + if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS) + *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND; + + return message; +} + +const char * +notmuch_message_get_message_id (notmuch_message_t *message) +{ + Xapian::TermIterator i; + + if (message->message_id) + return message->message_id; + + i = message->doc.termlist_begin (); + i.skip_to (_find_prefix ("id")); + + if (i == message->doc.termlist_end ()) + INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n", + message->doc_id); + + message->message_id = talloc_strdup (message, (*i).c_str () + 1); + +#if DEBUG_DATABASE_SANITY + i++; + + if (i != message->doc.termlist_end () && + strncmp ((*i).c_str (), _find_prefix ("id"), + strlen (_find_prefix ("id"))) == 0) + { + INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs", + message->doc_id); + } +#endif + + return message->message_id; +} + +static void +_notmuch_message_ensure_message_file (notmuch_message_t *message) +{ + const char *filename; + + if (message->message_file) + return; + + filename = notmuch_message_get_filename (message); + if (unlikely (filename == NULL)) + return; + + message->message_file = _notmuch_message_file_open_ctx (message, filename); +} + +const char * +notmuch_message_get_header (notmuch_message_t *message, const char *header) +{ + _notmuch_message_ensure_message_file (message); + if (message->message_file == NULL) + return NULL; + + return notmuch_message_file_get_header (message->message_file, header); +} + +const char * +notmuch_message_get_thread_id (notmuch_message_t *message) +{ + Xapian::TermIterator i; + + if (message->thread_id) + return message->thread_id; + + i = message->doc.termlist_begin (); + i.skip_to (_find_prefix ("thread")); + + if (i == message->doc.termlist_end ()) + INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n", + message->doc_id); + + message->thread_id = talloc_strdup (message, (*i).c_str () + 1); + +#if DEBUG_DATABASE_SANITY + i++; + + if (i != message->doc.termlist_end () && + strncmp ((*i).c_str (), _find_prefix ("thread"), + strlen (_find_prefix ("thread"))) == 0) + { + INTERNAL_ERROR ("Message %s has duplicate thread IDs: %s and %s\n", + notmuch_message_get_message_id (message), + message->thread_id, + (*i).c_str () + 1); + } +#endif + + return message->thread_id; +} + +/* Set the filename for 'message' to 'filename'. + * + * XXX: We should still figure out if we think it's important to store + * multiple filenames for email messages with identical message IDs. + * + * This change will not be reflected in the database until the next + * call to _notmuch_message_set_sync. */ +void +_notmuch_message_set_filename (notmuch_message_t *message, + const char *filename) +{ + const char *s; + const char *db_path; + unsigned int db_path_len; + + if (message->filename) { + talloc_free (message->filename); + message->filename = NULL; + } + + if (filename == NULL) + INTERNAL_ERROR ("Message filename cannot be NULL."); + + s = filename; + + db_path = notmuch_database_get_path (message->notmuch); + db_path_len = strlen (db_path); + + if (*s == '/' && strncmp (s, db_path, db_path_len) == 0 + && strlen (s) > db_path_len) + { + s += db_path_len + 1; + } + + message->doc.set_data (s); +} + +const char * +notmuch_message_get_filename (notmuch_message_t *message) +{ + std::string filename_str; + const char *db_path; + + if (message->filename) + return message->filename; + + filename_str = message->doc.get_data (); + db_path = notmuch_database_get_path (message->notmuch); + + if (filename_str[0] != '/') + message->filename = talloc_asprintf (message, "%s/%s", db_path, + filename_str.c_str ()); + else + message->filename = talloc_strdup (message, filename_str.c_str ()); + + return message->filename; +} + +time_t +notmuch_message_get_date (notmuch_message_t *message) +{ + std::string value; + + try { + value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP); + } catch (Xapian::Error &error) { + INTERNAL_ERROR ("Failed to read timestamp value from document."); + return 0; + } + + return Xapian::sortable_unserialise (value); +} + +notmuch_tags_t * +notmuch_message_get_tags (notmuch_message_t *message) +{ + const char *prefix = _find_prefix ("tag"); + Xapian::TermIterator i, end; + notmuch_tags_t *tags; + std::string tag; + + /* Currently this iteration is written with the assumption that + * "tag" has a single-character prefix. */ + assert (strlen (prefix) == 1); + + tags = _notmuch_tags_create (message); + if (unlikely (tags == NULL)) + return NULL; + + i = message->doc.termlist_begin (); + end = message->doc.termlist_end (); + + i.skip_to (prefix); + + while (1) { + tag = *i; + + if (tag.empty () || tag[0] != *prefix) + break; + + _notmuch_tags_add_tag (tags, tag.c_str () + 1); + + i++; + } + + _notmuch_tags_prepare_iterator (tags); + + return tags; +} + +void +_notmuch_message_set_date (notmuch_message_t *message, + const char *date) +{ + time_t time_value; + + /* GMime really doesn't want to see a NULL date, so protect its + * sensibilities. */ + if (date == NULL) + time_value = 0; + else + time_value = g_mime_utils_header_decode_date (date, NULL); + + message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP, + Xapian::sortable_serialise (time_value)); +} + +static void +thread_id_generate (thread_id_t *thread_id) +{ + static int seeded = 0; + FILE *dev_random; + uint32_t value; + char *s; + int i; + + if (! seeded) { + dev_random = fopen ("/dev/random", "r"); + if (dev_random == NULL) { + srand (time (NULL)); + } else { + fread ((void *) &value, sizeof (value), 1, dev_random); + srand (value); + fclose (dev_random); + } + seeded = 1; + } + + s = thread_id->str; + for (i = 0; i < NOTMUCH_THREAD_ID_DIGITS; i += 8) { + value = rand (); + sprintf (s, "%08x", value); + s += 8; + } +} + +void +_notmuch_message_ensure_thread_id (notmuch_message_t *message) +{ + /* If not part of any existing thread, generate a new thread_id. */ + thread_id_t thread_id; + + thread_id_generate (&thread_id); + _notmuch_message_add_term (message, "thread", thread_id.str); +} + +/* Synchronize changes made to message->doc out into the database. */ +void +_notmuch_message_sync (notmuch_message_t *message) +{ + Xapian::WritableDatabase *db = message->notmuch->xapian_db; + + db->replace_document (message->doc_id, message->doc); +} + +/* Add a name:value term to 'message', (the actual term will be + * encoded by prefixing the value with a short prefix). See + * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term + * names to prefix values. + * + * This change will not be reflected in the database until the next + * call to _notmuch_message_set_sync. */ +notmuch_private_status_t +_notmuch_message_add_term (notmuch_message_t *message, + const char *prefix_name, + const char *value) +{ + + char *term; + + if (value == NULL) + return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; + + term = talloc_asprintf (message, "%s%s", + _find_prefix (prefix_name), value); + + if (strlen (term) > NOTMUCH_TERM_MAX) + return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; + + message->doc.add_term (term); + + talloc_free (term); + + return NOTMUCH_PRIVATE_STATUS_SUCCESS; +} + +/* Parse 'text' and add a term to 'message' for each parsed word. Each + * term will be added both prefixed (if prefix_name is not NULL) and + * also unprefixed). */ +notmuch_private_status_t +_notmuch_message_gen_terms (notmuch_message_t *message, + const char *prefix_name, + const char *text) +{ + Xapian::TermGenerator *term_gen = message->notmuch->term_gen; + + if (text == NULL) + return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; + + term_gen->set_document (message->doc); + + if (prefix_name) { + const char *prefix = _find_prefix (prefix_name); + + term_gen->index_text (text, 1, prefix); + } + + term_gen->index_text (text); + + return NOTMUCH_PRIVATE_STATUS_SUCCESS; +} + +/* Remove a name:value term from 'message', (the actual term will be + * encoded by prefixing the value with a short prefix). See + * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term + * names to prefix values. + * + * This change will not be reflected in the database until the next + * call to _notmuch_message_set_sync. */ +notmuch_private_status_t +_notmuch_message_remove_term (notmuch_message_t *message, + const char *prefix_name, + const char *value) +{ + char *term; + + if (value == NULL) + return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; + + term = talloc_asprintf (message, "%s%s", + _find_prefix (prefix_name), value); + + if (strlen (term) > NOTMUCH_TERM_MAX) + return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; + + try { + message->doc.remove_term (term); + } catch (const Xapian::InvalidArgumentError) { + /* We'll let the philosopher's try to wrestle with the + * question of whether failing to remove that which was not + * there in the first place is failure. For us, we'll silently + * consider it all good. */ + } + + talloc_free (term); + + return NOTMUCH_PRIVATE_STATUS_SUCCESS; +} + +notmuch_status_t +notmuch_message_add_tag (notmuch_message_t *message, const char *tag) +{ + notmuch_private_status_t status; + + if (tag == NULL) + return NOTMUCH_STATUS_NULL_POINTER; + + if (strlen (tag) > NOTMUCH_TAG_MAX) + return NOTMUCH_STATUS_TAG_TOO_LONG; + + status = _notmuch_message_add_term (message, "tag", tag); + if (status) { + INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n", + status); + } + + if (! message->frozen) + _notmuch_message_sync (message); + + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +notmuch_message_remove_tag (notmuch_message_t *message, const char *tag) +{ + notmuch_private_status_t status; + + if (tag == NULL) + return NOTMUCH_STATUS_NULL_POINTER; + + if (strlen (tag) > NOTMUCH_TAG_MAX) + return NOTMUCH_STATUS_TAG_TOO_LONG; + + status = _notmuch_message_remove_term (message, "tag", tag); + if (status) { + INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n", + status); + } + + if (! message->frozen) + _notmuch_message_sync (message); + + return NOTMUCH_STATUS_SUCCESS; +} + +void +notmuch_message_remove_all_tags (notmuch_message_t *message) +{ + notmuch_private_status_t status; + notmuch_tags_t *tags; + const char *tag; + + for (tags = notmuch_message_get_tags (message); + notmuch_tags_has_more (tags); + notmuch_tags_advance (tags)) + { + tag = notmuch_tags_get (tags); + + status = _notmuch_message_remove_term (message, "tag", tag); + if (status) { + INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n", + status); + } + } + + if (! message->frozen) + _notmuch_message_sync (message); +} + +void +notmuch_message_freeze (notmuch_message_t *message) +{ + message->frozen++; +} + +notmuch_status_t +notmuch_message_thaw (notmuch_message_t *message) +{ + if (message->frozen > 0) { + message->frozen--; + if (message->frozen == 0) + _notmuch_message_sync (message); + return NOTMUCH_STATUS_SUCCESS; + } else { + return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW; + } +} + +void +notmuch_message_destroy (notmuch_message_t *message) +{ + talloc_free (message); +} diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h new file mode 100644 index 00000000..92b46343 --- /dev/null +++ b/lib/notmuch-private.h @@ -0,0 +1,314 @@ +/* notmuch-private.h - Internal interfaces for notmuch. + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#ifndef NOTMUCH_PRIVATE_H +#define NOTMUCH_PRIVATE_H + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE /* For getline and asprintf */ +#endif +#include + +#include "notmuch.h" + +NOTMUCH_BEGIN_DECLS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "xutil.h" + +#ifdef DEBUG +# define DEBUG_DATABASE_SANITY 1 +# define DEBUG_QUERY 1 +#endif + +#define COMPILE_TIME_ASSERT(pred) ((void)sizeof(char[1 - 2*!(pred)])) + +/* There's no point in continuing when we've detected that we've done + * something wrong internally (as opposed to the user passing in a + * bogus value). + * + * Note that PRINTF_ATTRIBUTE comes from talloc.h + */ +int +_internal_error (const char *format, ...) PRINTF_ATTRIBUTE (1, 2); + +/* There's no point in continuing when we've detected that we've done + * something wrong internally (as opposed to the user passing in a + * bogus value). + * + * Note that __location__ comes from talloc.h. + */ +#define INTERNAL_ERROR(format, ...) \ + _internal_error (format " (%s).\n", \ + ##__VA_ARGS__, __location__) + +#define unused(x) x __attribute__ ((unused)) + +/* Thanks to Andrew Tridgell's (SAMBA's) talloc for this definition of + * unlikely. The talloc source code comes to us via the GNU LGPL v. 3. + */ +/* these macros gain us a few percent of speed on gcc */ +#if (__GNUC__ >= 3) +/* the strange !! is to ensure that __builtin_expect() takes either 0 or 1 + as its first argument */ +#ifndef likely +#define likely(x) __builtin_expect(!!(x), 1) +#endif +#ifndef unlikely +#define unlikely(x) __builtin_expect(!!(x), 0) +#endif +#else +#ifndef likely +#define likely(x) (x) +#endif +#ifndef unlikely +#define unlikely(x) (x) +#endif +#endif + +typedef enum { + NOTMUCH_VALUE_TIMESTAMP = 0, + NOTMUCH_VALUE_MESSAGE_ID +} notmuch_value_t; + +/* Xapian (with flint backend) complains if we provide a term longer + * than this, but I haven't yet found a way to query the limit + * programmatically. */ +#define NOTMUCH_TERM_MAX 245 + +typedef enum _notmuch_private_status { + /* First, copy all the public status values. */ + NOTMUCH_PRIVATE_STATUS_SUCCESS = NOTMUCH_STATUS_SUCCESS, + NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY = NOTMUCH_STATUS_OUT_OF_MEMORY, + NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION = NOTMUCH_STATUS_XAPIAN_EXCEPTION, + NOTMUCH_PRIVATE_STATUS_FILE_NOT_EMAIL = NOTMUCH_STATUS_FILE_NOT_EMAIL, + NOTMUCH_PRIVATE_STATUS_NULL_POINTER = NOTMUCH_STATUS_NULL_POINTER, + NOTMUCH_PRIVATE_STATUS_TAG_TOO_LONG = NOTMUCH_STATUS_TAG_TOO_LONG, + + /* Then add our own private values. */ + NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG, + NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND, + + NOTMUCH_PRIVATE_STATUS_LAST_STATUS +} notmuch_private_status_t; + +/* Coerce a notmuch_private_status_t value to a notmuch_status_t + * value, generating an internal error if the private value is equal + * to or greater than NOTMUCH_STATUS_LAST_STATUS. (The idea here is + * that the caller has previously handled any expected + * notmuch_private_status_t values.) + */ +#define COERCE_STATUS(private_status, format, ...) \ + ((private_status >= (notmuch_private_status_t) NOTMUCH_STATUS_LAST_STATUS)\ + ? \ + (notmuch_status_t) _internal_error (format " (%s).\n", \ + ##__VA_ARGS__, \ + __location__) \ + : \ + (notmuch_status_t) private_status) + +/* database.cc */ + +/* Lookup a prefix value by name. + * + * XXX: This should really be static inside of message.cc, and we can + * do that once we convert database.cc to use the + * _notmuch_message_add/remove_term functions. */ +const char * +_find_prefix (const char *name); + +/* thread.cc */ + +notmuch_thread_t * +_notmuch_thread_create (const void *talloc_owner, + notmuch_database_t *notmuch, + const char *thread_id); + +void +_notmuch_thread_add_message (notmuch_thread_t *thread, + notmuch_message_t *message); + +/* message.cc */ + +notmuch_message_t * +_notmuch_message_create (const void *talloc_owner, + notmuch_database_t *notmuch, + unsigned int doc_id, + notmuch_private_status_t *status); + +notmuch_message_t * +_notmuch_message_create_for_message_id (const void *talloc_owner, + notmuch_database_t *notmuch, + const char *message_id, + notmuch_private_status_t *status); + +const char * +_notmuch_message_get_subject (notmuch_message_t *message); + +notmuch_private_status_t +_notmuch_message_add_term (notmuch_message_t *message, + const char *prefix_name, + const char *value); + +notmuch_private_status_t +_notmuch_message_remove_term (notmuch_message_t *message, + const char *prefix_name, + const char *value); + +notmuch_private_status_t +_notmuch_message_gen_terms (notmuch_message_t *message, + const char *prefix_name, + const char *text); + +void +_notmuch_message_set_filename (notmuch_message_t *message, + const char *filename); + +void +_notmuch_message_add_thread_id (notmuch_message_t *message, + const char *thread_id); + +void +_notmuch_message_ensure_thread_id (notmuch_message_t *message); + +void +_notmuch_message_set_date (notmuch_message_t *message, + const char *date); + +void +_notmuch_message_sync (notmuch_message_t *message); + +/* index.cc */ + +notmuch_status_t +_notmuch_message_index_file (notmuch_message_t *message, + const char *filename); + +/* message-file.c */ + +/* XXX: I haven't decided yet whether these will actually get exported + * into the public interface in notmuch.h + */ + +typedef struct _notmuch_message_file notmuch_message_file_t; + +/* Open a file containing a single email message. + * + * The caller should call notmuch_message_close when done with this. + * + * Returns NULL if any error occurs. + */ +notmuch_message_file_t * +notmuch_message_file_open (const char *filename); + +/* Like notmuch_message_file_open but with 'ctx' as the talloc owner. */ +notmuch_message_file_t * +_notmuch_message_file_open_ctx (void *ctx, const char *filename); + +/* Close a notmuch message preivously opened with notmuch_message_open. */ +void +notmuch_message_file_close (notmuch_message_file_t *message); + +/* Restrict 'message' to only save the named headers. + * + * When the caller is only interested in a short list of headers, + * known in advance, calling this function can avoid wasted time and + * memory parsing/saving header values that will never be needed. + * + * The variable arguments should be a list of const char * with a + * final '(const char *) NULL' to terminate the list. + * + * If this function is called, it must be called before any calls to + * notmuch_message_get_header for this message. + * + * After calling this function, if notmuch_message_get_header is + * called with a header name not in this list, then NULL will be + * returned even if that header exists in the actual message. + */ +void +notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...); + +/* Identical to notmuch_message_restrict_headers but accepting a va_list. */ +void +notmuch_message_file_restrict_headersv (notmuch_message_file_t *message, + va_list va_headers); + +/* Get the value of the specified header from the message. + * + * The header name is case insensitive. + * + * The returned value is owned by the notmuch message and is valid + * only until the message is closed. The caller should copy it if + * needing to modify the value or to hold onto it for longer. + * + * Returns NULL if the message does not contain a header line matching + * 'header'. + */ +const char * +notmuch_message_file_get_header (notmuch_message_file_t *message, + const char *header); + +/* date.c */ + +/* Parse an RFC 8222 date string to a time_t value. + * + * The tz_offset argument can be used to also obtain the time-zone + * offset, (but can be NULL if the call is not interested in that). + * + * Returns 0 on error. + */ +time_t +notmuch_parse_date (const char *str, int *tz_offset); + +/* sha1.c */ + +char * +notmuch_sha1_of_string (const char *str); + +char * +notmuch_sha1_of_file (const char *filename); + +/* tags.c */ + +notmuch_tags_t * +_notmuch_tags_create (void *ctx); + +void +_notmuch_tags_add_tag (notmuch_tags_t *tags, const char *tag); + +void +_notmuch_tags_prepare_iterator (notmuch_tags_t *tags); + +NOTMUCH_END_DECLS + +#endif diff --git a/lib/notmuch.h b/lib/notmuch.h new file mode 100644 index 00000000..bab573dd --- /dev/null +++ b/lib/notmuch.h @@ -0,0 +1,834 @@ +/* notmuch - Not much of an email library, (just index and search) + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#ifndef NOTMUCH_H +#define NOTMUCH_H + +#ifdef __cplusplus +# define NOTMUCH_BEGIN_DECLS extern "C" { +# define NOTMUCH_END_DECLS } +#else +# define NOTMUCH_BEGIN_DECLS +# define NOTMUCH_END_DECLS +#endif + +NOTMUCH_BEGIN_DECLS + +#include + +#ifndef FALSE +#define FALSE 0 +#endif + +#ifndef TRUE +#define TRUE 1 +#endif + +typedef int notmuch_bool_t; + +/* Status codes used for the return values of most functions. + * + * A zero value (NOTMUCH_STATUS_SUCCESS) indicates that the function + * completed without error. Any other value indicates an error as + * follows: + * + * NOTMUCH_STATUS_SUCCESS: No error occurred. + * + * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory + * + * XXX: We don't really want to expose this lame XAPIAN_EXCEPTION + * value. Instead we should map to things like DATABASE_LOCKED or + * whatever. + * + * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred + * + * NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to read or + * write to a file (this could be file not found, permission + * denied, etc.) + * + * NOTMUCH_STATUS_FILE_NOT_EMAIL: A file was presented that doesn't + * appear to be an email message. + * + * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: A file contains a message ID + * that is identical to a message already in the database. + * + * NOTMUCH_STATUS_NULL_POINTER: The user erroneously passed a NULL + * pointer to a notmuch function. + * + * NOTMUCH_STATUS_TAG_TOO_LONG: A tag value is too long (exceeds + * NOTMUCH_TAG_MAX) + * + * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: The notmuch_message_thaw + * function has been called more times than notmuch_message_freeze. + * + * And finally: + * + * NOTMUCH_STATUS_LAST_STATUS: Not an actual status value. Just a way + * to find out how many valid status values there are. + */ +typedef enum _notmuch_status { + NOTMUCH_STATUS_SUCCESS = 0, + NOTMUCH_STATUS_OUT_OF_MEMORY, + NOTMUCH_STATUS_XAPIAN_EXCEPTION, + NOTMUCH_STATUS_FILE_ERROR, + NOTMUCH_STATUS_FILE_NOT_EMAIL, + NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID, + NOTMUCH_STATUS_NULL_POINTER, + NOTMUCH_STATUS_TAG_TOO_LONG, + NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW, + + NOTMUCH_STATUS_LAST_STATUS +} notmuch_status_t; + +/* Get a string representation of a notmuch_status_t value. + * + * The result is readonly. + */ +const char * +notmuch_status_to_string (notmuch_status_t status); + +/* Various opaque data types. For each notmuch__t see the various + * notmuch_ functions below. */ +typedef struct _notmuch_database notmuch_database_t; +typedef struct _notmuch_query notmuch_query_t; +typedef struct _notmuch_threads notmuch_threads_t; +typedef struct _notmuch_thread notmuch_thread_t; +typedef struct _notmuch_messages notmuch_messages_t; +typedef struct _notmuch_message notmuch_message_t; +typedef struct _notmuch_tags notmuch_tags_t; + +/* Lookup the default database path. + * + * This is the path that will be used by notmuch_database_create and + * notmuch_database_open if given a NULL path. Specifically it will be + * the value of the NOTMUCH_BASE environment variable if set, + * otherwise ${HOME}/mail + * + * Returns a newly allocated string which the caller should free() + * when finished with it. + */ +char * +notmuch_database_default_path (void); + +/* Create a new, empty notmuch database located at 'path'. + * + * The path should be a top-level directory to a collection of + * plain-text email messages (one message per file). This call will + * create a new ".notmuch" directory within 'path' where notmuch will + * store its data. + * + * Passing a value of NULL for 'path' will cause notmuch to open the + * default database. The default database path can be specified by the + * NOTMUCH_BASE environment variable, and is equivalent to + * ${HOME}/mail if NOTMUCH_BASE is not set. + * + * After a successful call to notmuch_database_create, the returned + * database will be open so the caller should call + * notmuch_database_close when finished with it. + * + * The database will not yet have any data in it + * (notmuch_database_create itself is a very cheap function). Messages + * contained within 'path' can be added to the database by calling + * notmuch_database_add_message. + * + * In case of any failure, this function returns NULL, (after printing + * an error message on stderr). + */ +notmuch_database_t * +notmuch_database_create (const char *path); + +/* XXX: I think I'd like this to take an extra argument of + * notmuch_status_t* for returning a status value on failure. */ + +/* Open a 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'. + * + * An existing notmuch database can be identified by the presence of a + * directory named ".notmuch" below 'path'. + * + * Passing a value of NULL for 'path' will cause notmuch to open the + * default database. The default database path can be specified by the + * NOTMUCH_BASE environment variable, and is equivalent to + * ${HOME}/mail if NOTMUCH_BASE is not set. + * + * The caller should call notmuch_database_close when finished with + * this database. + * + * In case of any failure, this function returns NULL, (after printing + * an error message on stderr). + */ +notmuch_database_t * +notmuch_database_open (const char *path); + +/* Close the given notmuch database, freeing all associated + * resources. See notmuch_database_open. */ +void +notmuch_database_close (notmuch_database_t *database); + +/* Return the database path of the given database. + * + * The return value is a string owned by notmuch so should not be + * modified nor freed by the caller. */ +const char * +notmuch_database_get_path (notmuch_database_t *database); + +/* Store a timestamp within the database. + * + * The Notmuch database will not interpret this key nor the timestamp + * values at all. It will merely store them together and return the + * timestamp when notmuch_database_get_timestamp is called with the + * same value for 'key'. + * + * The intention is for the caller to use the timestamp to allow + * efficient identification of new messages to be added to the + * database. The recommended usage is as follows: + * + * o Read the mtime of a directory from the filesystem + * + * o Call add_message for all mail files in the directory + * + * o Call notmuch_database_set_timestamp with the path of the + * directory as 'key' and the originally read mtime as 'value'. + * + * Then, when wanting to check for updates to the directory in the + * future, the client can call notmuch_database_get_timestamp and know + * that it only needs to add files if the mtime of the directory and + * files are newer than the stored timestamp. + * + * Note: The notmuch_database_get_timestamp function does not allow + * the caller to distinguish a timestamp of 0 from a non-existent + * timestamp. So don't store a timestamp of 0 unless you are + * comfortable with that. + * + * Return value: + * + * NOTMUCH_STATUS_SUCCESS: Timestamp successfully stored in database. + * + * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception + * occurred. Timestamp not stored. + */ +notmuch_status_t +notmuch_database_set_timestamp (notmuch_database_t *database, + const char *key, time_t timestamp); + +/* Retrieve a timestamp from the database. + * + * Returns the timestamp value previously stored by calling + * notmuch_database_set_timestamp with the same value for 'key'. + * + * Returns 0 if no timestamp is stored for 'key' or if any error + * occurred querying the database. + */ +time_t +notmuch_database_get_timestamp (notmuch_database_t *database, + const char *key); + +/* Add a new message to the given notmuch database. + * + * Here,'filename' should be a path relative to the 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_DUPLICATE_MESSAGE_ID: Message has the same message + * ID as another message already in the database. Nothing added + * to the database. + * + * 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_t +notmuch_database_add_message (notmuch_database_t *database, + const char *filename, + notmuch_message_t **message); + +/* Find a message with the given messsage_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. + * + * If no message is found with the given message_id or if an + * out-of-memory situation occurs, this function returns NULL. + */ +notmuch_message_t * +notmuch_database_find_message (notmuch_database_t *database, + const char *message_id); + +/* 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 a length-zero string, (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 and + * notmuch_query_search to actually execute the query. + * + * User should call notmuch_query_destroy when finished with this + * query. + * + * Will return NULL if insufficient memory is available. + */ +notmuch_query_t * +notmuch_query_create (notmuch_database_t *database, + const char *query_string); + +/* Sort values for notmuch_query_set_sort */ +typedef enum { + NOTMUCH_SORT_DATE_OLDEST_FIRST, + NOTMUCH_SORT_DATE_NEWEST_FIRST, + NOTMUCH_SORT_MESSAGE_ID +} notmuch_sort_t; + +/* Specify the sorting desired for this query. */ +void +notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort); + +/* 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_has_more (threads); + * notmuch_threads_advance (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). + */ +notmuch_threads_t * +notmuch_query_search_threads (notmuch_query_t *query); + +/* 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_has_more (messages); + * notmuch_messages_advance (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). + */ +notmuch_messages_t * +notmuch_query_search_messages (notmuch_query_t *query); + +/* 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_thrad_t and notmuch_message_t objects generated + * from those results, etc.), if such objects haven't already been + * destroyed. + */ +void +notmuch_query_destroy (notmuch_query_t *query); + +/* Does the given notmuch_threads_t object contain any more + * results. + * + * 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. + */ +notmuch_bool_t +notmuch_threads_has_more (notmuch_threads_t *threads); + +/* Get the current thread from 'threads' as a notmuch_thread_t. + * + * Note: The returned thread belongs to 'threads' and has a lifetime + * identical to it (and the query to which it belongs). + * + * See the documentation of notmuch_query_search_threads for example + * code showing how to iterate over a notmuch_threads_t object. + * + * If an out-of-memory situation occurs, this function will return + * NULL. + */ +notmuch_thread_t * +notmuch_threads_get (notmuch_threads_t *threads); + +/* Advance the 'threads' iterator to the next thread. + * + * See the documentation of notmuch_query_search_threads for example + * code showing how to iterate over a notmuch_threads_t object. + */ +void +notmuch_threads_advance (notmuch_threads_t *threads); + +/* 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. + */ +void +notmuch_threads_destroy (notmuch_threads_t *threads); + +/* Get the thread ID of 'thread'. + * + * The returned string belongs to 'thread' and as such, should not be + * modified by the caller and will only be valid for as long as the + * thread is valid, (which is until notmuch_thread_destroy or until + * the query from which it derived is destroyed). + */ +const char * +notmuch_thread_get_thread_id (notmuch_thread_t *thread); + +/* Get the subject of 'thread' + * + * The subject is taken from the first message (according to the query + * order---see notmuch_query_set_sort) in the query results that + * belongs to this thread. + * + * The returned string belongs to 'thread' and as such, should not be + * modified by the caller and will only be valid for as long as the + * thread is valid, (which is until notmuch_thread_destroy or until + * the query from which it derived is destroyed). + */ +const char * +notmuch_thread_get_subject (notmuch_thread_t *thread); + +/* Get the date of the oldest message in 'thread' as a time_t value. + */ +time_t +notmuch_thread_get_oldest_date (notmuch_thread_t *thread); + +/* Get the date of the oldest message in 'thread' as a time_t value. + */ +time_t +notmuch_thread_get_newest_date (notmuch_thread_t *thread); + +/* Get the tags for 'thread', returning a notmuch_tags_t object which + * can be used to iterate over all tags. + * + * Note: In the Notmuch database, tags are stored on individual + * messages, not on threads. So the tags returned here will be all + * tags of the messages which matched the search and which belong to + * this thread. + * + * The tags object is owned by the thread and as such, will only be + * valid for as long as the thread is valid, (for example, until + * notmuch_thread_destroy or until the query from which it derived is + * destroyed). + * + * Typical usage might be: + * + * notmuch_thread_t *thread; + * notmuch_tags_t *tags; + * const char *tag; + * + * thread = notmuch_threads_get (threads); + * + * for (tags = notmuch_thread_get_tags (thread); + * notmuch_tags_has_more (tags); + * notmuch_result_advance (tags)) + * { + * tag = notmuch_tags_get (tags); + * .... + * } + * + * notmuch_thread_destroy (thread); + * + * 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). + */ +notmuch_tags_t * +notmuch_thread_get_tags (notmuch_thread_t *thread); + +/* Destroy a notmuch_thread_t object. */ +void +notmuch_thread_destroy (notmuch_thread_t *thread); + +/* Does the given notmuch_messages_t object contain any more + * messages. + * + * 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. + */ +notmuch_bool_t +notmuch_messages_has_more (notmuch_messages_t *messages); + +/* 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. + */ +notmuch_message_t * +notmuch_messages_get (notmuch_messages_t *messages); + +/* Advance the 'messages' iterator to the next result. + * + * See the documentation of notmuch_query_search_messages for example + * code showing how to iterate over a notmuch_messages_t object. + */ +void +notmuch_messages_advance (notmuch_messages_t *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 containg + * query object is destroyed. + */ +void +notmuch_messages_destroy (notmuch_messages_t *messages); + +/* 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). + */ +const char * +notmuch_message_get_message_id (notmuch_message_t *message); + +/* 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. + */ +const char * +notmuch_message_get_thread_id (notmuch_message_t *message); + +/* Get the 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). */ +const char * +notmuch_message_get_filename (notmuch_message_t *message); + +/* Get the date of 'message' as a time_t value. + * + * For the original textual representation of the Date header from the + * message call notmuch_message_get_header() with a header value of + * "date". */ +time_t +notmuch_message_get_date (notmuch_message_t *message); + +/* 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 NULL if the message does not contain a header line matching + * 'header' of if any error occurs. + */ +const char * +notmuch_message_get_header (notmuch_message_t *message, const char *header); + +/* 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_has_more (tags); + * notmuch_result_advance (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). + */ +notmuch_tags_t * +notmuch_message_get_tags (notmuch_message_t *message); + +/* The longest possible tag value. */ +#define NOTMUCH_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_t +notmuch_message_add_tag (notmuch_message_t *message, const char *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_t +notmuch_message_remove_tag (notmuch_message_t *message, const char *tag); + +/* Remove all tags from the given message. + * + * See notmuch_message_freeze for an example showing how to safely + * replace tag values. + */ +void +notmuch_message_remove_all_tags (notmuch_message_t *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 with + * "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 value, 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. + */ +void +notmuch_message_freeze (notmuch_message_t *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_UNBALANCE_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. + */ +notmuch_status_t +notmuch_message_thaw (notmuch_message_t *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.) + */ +void +notmuch_message_destroy (notmuch_message_t *message); + +/* Does the given notmuch_tags_t object contain any more tags. + * + * 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. + */ +notmuch_bool_t +notmuch_tags_has_more (notmuch_tags_t *tags); + +/* 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 utlimately belongs). + * + * See the documentation of notmuch_message_get_tags for example code + * showing how to iterate over a notmuch_tags_t object. + */ +const char * +notmuch_tags_get (notmuch_tags_t *tags); + +/* Advance the 'tags' iterator to the next tag. + * + * See the documentation of notmuch_message_get_tags for example code + * showing how to iterate over a notmuch_tags_t object. + */ +void +notmuch_tags_advance (notmuch_tags_t *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 containg + * message or query objects are destroyed. + */ +void +notmuch_tags_destroy (notmuch_tags_t *tags); + +NOTMUCH_END_DECLS + +#endif diff --git a/lib/query.cc b/lib/query.cc new file mode 100644 index 00000000..ed576614 --- /dev/null +++ b/lib/query.cc @@ -0,0 +1,301 @@ +/* query.cc - Support for searching a notmuch database + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#include "notmuch-private.h" +#include "database-private.h" + +#include /* GHashTable, GPtrArray */ + +#include + +struct _notmuch_query { + notmuch_database_t *notmuch; + const char *query_string; + notmuch_sort_t sort; +}; + +struct _notmuch_messages { + notmuch_database_t *notmuch; + Xapian::MSetIterator iterator; + Xapian::MSetIterator iterator_end; +}; + +struct _notmuch_threads { + notmuch_database_t *notmuch; + GPtrArray *threads; + unsigned int index; +}; + +notmuch_query_t * +notmuch_query_create (notmuch_database_t *notmuch, + const char *query_string) +{ + notmuch_query_t *query; + +#ifdef DEBUG_QUERY + fprintf (stderr, "Query string is:\n%s\n", query_string); +#endif + + query = talloc (NULL, notmuch_query_t); + if (unlikely (query == NULL)) + return NULL; + + query->notmuch = notmuch; + + query->query_string = talloc_strdup (query, query_string); + + query->sort = NOTMUCH_SORT_DATE_OLDEST_FIRST; + + return query; +} + +void +notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort) +{ + query->sort = sort; +} + +/* We end up having to call the destructors explicitly because we had + * to use "placement new" in order to initialize C++ objects within a + * block that we allocated with talloc. So C++ is making talloc + * slightly less simple to use, (we wouldn't need + * talloc_set_destructor at all otherwise). + */ +static int +_notmuch_messages_destructor (notmuch_messages_t *messages) +{ + messages->iterator.~MSetIterator (); + messages->iterator_end.~MSetIterator (); + + return 0; +} + +notmuch_messages_t * +notmuch_query_search_messages (notmuch_query_t *query) +{ + notmuch_database_t *notmuch = query->notmuch; + const char *query_string = query->query_string; + notmuch_messages_t *messages; + + messages = talloc (query, notmuch_messages_t); + if (unlikely (messages == NULL)) + return NULL; + + try { + Xapian::Enquire enquire (*notmuch->xapian_db); + Xapian::Query mail_query (talloc_asprintf (query, "%s%s", + _find_prefix ("type"), + "mail")); + Xapian::Query string_query, final_query; + Xapian::MSet mset; + unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN | + Xapian::QueryParser::FLAG_PHRASE | + Xapian::QueryParser::FLAG_LOVEHATE | + Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE | + Xapian::QueryParser::FLAG_WILDCARD); + + if (strcmp (query_string, "") == 0) { + final_query = mail_query; + } else { + string_query = notmuch->query_parser-> + parse_query (query_string, flags); + final_query = Xapian::Query (Xapian::Query::OP_AND, + mail_query, string_query); + } + + switch (query->sort) { + case NOTMUCH_SORT_DATE_OLDEST_FIRST: + enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, FALSE); + break; + case NOTMUCH_SORT_DATE_NEWEST_FIRST: + enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, TRUE); + break; + case NOTMUCH_SORT_MESSAGE_ID: + enquire.set_sort_by_value (NOTMUCH_VALUE_MESSAGE_ID, FALSE); + break; + } + +#if DEBUG_QUERY + fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str()); +#endif + + enquire.set_query (final_query); + + mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ()); + + messages->notmuch = notmuch; + + new (&messages->iterator) Xapian::MSetIterator (); + new (&messages->iterator_end) Xapian::MSetIterator (); + + talloc_set_destructor (messages, _notmuch_messages_destructor); + + messages->iterator = mset.begin (); + messages->iterator_end = mset.end (); + + } catch (const Xapian::Error &error) { + fprintf (stderr, "A Xapian exception occurred: %s\n", + error.get_msg().c_str()); + } + + return messages; +} + +/* Glib objects force use to use a talloc destructor as well, (but not + * nearly as ugly as the for messages due to C++ objects). At + * this point, I'd really like to have some talloc-friendly + * equivalents for the few pieces of glib that I'm using. */ +static int +_notmuch_threads_destructor (notmuch_threads_t *threads) +{ + g_ptr_array_free (threads->threads, TRUE); + + return 0; +} + +notmuch_threads_t * +notmuch_query_search_threads (notmuch_query_t *query) +{ + notmuch_threads_t *threads; + notmuch_thread_t *thread; + const char *thread_id; + notmuch_messages_t *messages; + notmuch_message_t *message; + GHashTable *seen; + + threads = talloc (query, notmuch_threads_t); + if (threads == NULL) + return NULL; + + threads->notmuch = query->notmuch; + threads->threads = g_ptr_array_new (); + threads->index = 0; + + talloc_set_destructor (threads, _notmuch_threads_destructor); + + seen = g_hash_table_new_full (g_str_hash, g_str_equal, + free, NULL); + + for (messages = notmuch_query_search_messages (query); + notmuch_messages_has_more (messages); + notmuch_messages_advance (messages)) + { + message = notmuch_messages_get (messages); + + thread_id = notmuch_message_get_thread_id (message); + + if (! g_hash_table_lookup_extended (seen, + thread_id, NULL, + (void **) &thread)) + { + thread = _notmuch_thread_create (query, query->notmuch, + thread_id); + + g_hash_table_insert (seen, xstrdup (thread_id), thread); + + g_ptr_array_add (threads->threads, thread); + } + + _notmuch_thread_add_message (thread, message); + + notmuch_message_destroy (message); + } + + g_hash_table_unref (seen); + + return threads; +} + +void +notmuch_query_destroy (notmuch_query_t *query) +{ + talloc_free (query); +} + +notmuch_bool_t +notmuch_messages_has_more (notmuch_messages_t *messages) +{ + return (messages->iterator != messages->iterator_end); +} + +notmuch_message_t * +notmuch_messages_get (notmuch_messages_t *messages) +{ + notmuch_message_t *message; + Xapian::docid doc_id; + notmuch_private_status_t status; + + if (! notmuch_messages_has_more (messages)) + return NULL; + + doc_id = *messages->iterator; + + message = _notmuch_message_create (messages, + messages->notmuch, doc_id, + &status); + + if (message == NULL && + status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) + { + INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n"); + } + + return message; +} + +void +notmuch_messages_advance (notmuch_messages_t *messages) +{ + messages->iterator++; +} + +void +notmuch_messages_destroy (notmuch_messages_t *messages) +{ + talloc_free (messages); +} + +notmuch_bool_t +notmuch_threads_has_more (notmuch_threads_t *threads) +{ + return (threads->index < threads->threads->len); +} + +notmuch_thread_t * +notmuch_threads_get (notmuch_threads_t *threads) +{ + if (! notmuch_threads_has_more (threads)) + return NULL; + + return (notmuch_thread_t *) g_ptr_array_index (threads->threads, + threads->index); +} + +void +notmuch_threads_advance (notmuch_threads_t *threads) +{ + threads->index++; +} + +void +notmuch_threads_destroy (notmuch_threads_t *threads) +{ + talloc_free (threads); +} diff --git a/lib/sha1.c b/lib/sha1.c new file mode 100644 index 00000000..ff4dd164 --- /dev/null +++ b/lib/sha1.c @@ -0,0 +1,115 @@ +/* sha1.c - Interfaces to SHA-1 hash for the notmuch mail system + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#include "notmuch-private.h" + +#include "libsha1.h" + +/* Just some simple interfaces on top of libsha1 so that we can leave + * libsha1 as untouched as possible. */ + +static char * +_hex_of_sha1_digest (const unsigned char digest[SHA1_DIGEST_SIZE]) +{ + char *result, *r; + int i; + + result = xcalloc (SHA1_DIGEST_SIZE * 2 + 1, 1); + + for (r = result, i = 0; + i < SHA1_DIGEST_SIZE; + r += 2, i++) + { + sprintf (r, "%02x", digest[i]); + } + + return result; +} + +/* Create a hexadcimal string version of the SHA-1 digest of 'str' + * (including its null terminating character). + * + * This function returns a newly allocated string which the caller + * should free() when finished. + */ +char * +notmuch_sha1_of_string (const char *str) +{ + sha1_ctx sha1; + unsigned char digest[SHA1_DIGEST_SIZE]; + + sha1_begin (&sha1); + + sha1_hash ((unsigned char *) str, strlen (str) + 1, &sha1); + + sha1_end (digest, &sha1); + + return _hex_of_sha1_digest (digest); +} + +/* Create a hexadecimal string version of the SHA-1 digest of the + * contents of the named file. + * + * This function returns a newly allocated string which the caller + * should free() when finished. + * + * If any error occurs while reading the file, (permission denied, + * file not found, etc.), this function returns NULL. + */ +char * +notmuch_sha1_of_file (const char *filename) +{ + FILE *file; +#define BLOCK_SIZE 4096 + unsigned char block[BLOCK_SIZE]; + size_t bytes_read; + sha1_ctx sha1; + unsigned char digest[SHA1_DIGEST_SIZE]; + char *result; + + file = fopen (filename, "r"); + if (file == NULL) + return NULL; + + sha1_begin (&sha1); + + while (1) { + bytes_read = fread (block, 1, 4096, file); + if (bytes_read == 0) { + if (feof (file)) { + break; + } else if (ferror (file)) { + fclose (file); + return NULL; + } + } else { + sha1_hash (block, bytes_read, &sha1); + } + } + + sha1_end (digest, &sha1); + + result = _hex_of_sha1_digest (digest); + + fclose (file); + + return result; +} + diff --git a/lib/tags.c b/lib/tags.c new file mode 100644 index 00000000..afc132c5 --- /dev/null +++ b/lib/tags.c @@ -0,0 +1,116 @@ +/* tags.c - Iterator for tags returned from message or thread + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#include "notmuch-private.h" + +#include /* GList */ + +struct _notmuch_tags { + int sorted; + GList *tags; + GList *iterator; +}; + +/* XXX: Should write some talloc-friendly list to avoid the need for + * this. */ +static int +_notmuch_tags_destructor (notmuch_tags_t *tags) +{ + g_list_free (tags->tags); + + return 0; +} + +/* Create a new notmuch_tags_t object, with 'ctx' as its talloc owner. + * + * This function can return NULL in case of out-of-memory. + */ +notmuch_tags_t * +_notmuch_tags_create (void *ctx) +{ + notmuch_tags_t *tags; + + tags = talloc (ctx, notmuch_tags_t); + if (unlikely (tags == NULL)) + return NULL; + + talloc_set_destructor (tags, _notmuch_tags_destructor); + + tags->sorted = 1; + tags->tags = NULL; + tags->iterator = NULL; + + return tags; +} + +/* Add a new tag to 'tags'. The tags object will create its own copy + * of the string. + * + * Note: The tags object will not do anything to prevent duplicate + * tags being stored, so the caller really shouldn't pass + * duplicates. */ +void +_notmuch_tags_add_tag (notmuch_tags_t *tags, const char *tag) +{ + tags->tags = g_list_prepend (tags->tags, talloc_strdup (tags, tag)); + tags->sorted = 0; +} + +/* Prepare 'tag' for iteration. + * + * The internal creator of 'tags' should call this function before + * returning 'tags' to the user to call the public functions such as + * notmuch_tags_has_more, notmuch_tags_get, and notmuch_tags_advance. */ +void +_notmuch_tags_prepare_iterator (notmuch_tags_t *tags) +{ + if (! tags->sorted) + tags->tags = g_list_sort (tags->tags, (GCompareFunc) strcmp); + tags->sorted = 1; + + tags->iterator = tags->tags; +} + +notmuch_bool_t +notmuch_tags_has_more (notmuch_tags_t *tags) +{ + return tags->iterator != NULL; +} + +const char * +notmuch_tags_get (notmuch_tags_t *tags) +{ + if (tags->iterator) + return (char *) tags->iterator->data; + else + return NULL; +} + +void +notmuch_tags_advance (notmuch_tags_t *tags) +{ + tags->iterator = tags->iterator->next; +} + +void +notmuch_tags_destroy (notmuch_tags_t *tags) +{ + talloc_free (tags); +} diff --git a/lib/thread.cc b/lib/thread.cc new file mode 100644 index 00000000..b67dfade --- /dev/null +++ b/lib/thread.cc @@ -0,0 +1,171 @@ +/* thread.cc - Results of thread-based searches from a notmuch database + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#include "notmuch-private.h" +#include "database-private.h" + +#include + +#include /* GHashTable */ + +struct _notmuch_thread { + notmuch_database_t *notmuch; + char *thread_id; + char *subject; + GHashTable *tags; + + notmuch_bool_t has_message; + time_t oldest; + time_t newest; +}; + +static int +_notmuch_thread_destructor (notmuch_thread_t *thread) +{ + g_hash_table_unref (thread->tags); + + return 0; +} + +/* Create a new notmuch_thread_t object for an existing document in + * the database. + * + * Here, 'talloc owner' is an optional talloc context to which the new + * thread will belong. This allows for the caller to not bother + * calling notmuch_thread_destroy on the thread, and know that all + * memory will be reclaimed with 'talloc_owner' is freed. The caller + * still can call notmuch_thread_destroy when finished with the + * thread if desired. + * + * The 'talloc_owner' argument can also be NULL, in which case the + * caller *is* responsible for calling notmuch_thread_destroy. + * + * This function returns NULL in the case of any error. + */ +notmuch_thread_t * +_notmuch_thread_create (const void *talloc_owner, + notmuch_database_t *notmuch, + const char *thread_id) +{ + notmuch_thread_t *thread; + + thread = talloc (talloc_owner, notmuch_thread_t); + if (unlikely (thread == NULL)) + return NULL; + + talloc_set_destructor (thread, _notmuch_thread_destructor); + + thread->notmuch = notmuch; + thread->thread_id = talloc_strdup (thread, thread_id); + thread->subject = NULL; + thread->tags = g_hash_table_new_full (g_str_hash, g_str_equal, + free, NULL); + + thread->has_message = 0; + thread->oldest = 0; + thread->newest = 0; + + return thread; +} + +const char * +notmuch_thread_get_thread_id (notmuch_thread_t *thread) +{ + return thread->thread_id; +} + +void +_notmuch_thread_add_message (notmuch_thread_t *thread, + notmuch_message_t *message) +{ + notmuch_tags_t *tags; + const char *tag; + time_t date; + + if (! thread->subject) { + const char *subject; + subject = notmuch_message_get_header (message, "subject"); + thread->subject = talloc_strdup (thread, subject); + } + + for (tags = notmuch_message_get_tags (message); + notmuch_tags_has_more (tags); + notmuch_tags_advance (tags)) + { + tag = notmuch_tags_get (tags); + g_hash_table_insert (thread->tags, xstrdup (tag), NULL); + } + + date = notmuch_message_get_date (message); + + if (date < thread->oldest || ! thread->has_message) + thread->oldest = date; + + if (date > thread->newest || ! thread->has_message) + thread->newest = date; + + thread->has_message = 1; +} + +const char * +notmuch_thread_get_subject (notmuch_thread_t *thread) +{ + return thread->subject; +} + +time_t +notmuch_thread_get_oldest_date (notmuch_thread_t *thread) +{ + return thread->oldest; +} + +time_t +notmuch_thread_get_newest_date (notmuch_thread_t *thread) +{ + return thread->newest; +} + +notmuch_tags_t * +notmuch_thread_get_tags (notmuch_thread_t *thread) +{ + notmuch_tags_t *tags; + GList *keys, *l; + + tags = _notmuch_tags_create (thread); + if (unlikely (tags == NULL)) + return NULL; + + keys = g_hash_table_get_keys (thread->tags); + + for (l = keys; l; l = l->next) + _notmuch_tags_add_tag (tags, (char *) l->data); + + g_list_free (keys); + + _notmuch_tags_prepare_iterator (tags); + + return tags; +} + +void +notmuch_thread_destroy (notmuch_thread_t *thread) +{ + talloc_free (thread); +} diff --git a/lib/xutil.c b/lib/xutil.c new file mode 100644 index 00000000..6fa5eb0d --- /dev/null +++ b/lib/xutil.c @@ -0,0 +1,130 @@ +/* xutil.c - Various wrapper functions to abort on error. + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#define _GNU_SOURCE /* For strndup */ +#include "notmuch-private.h" + +#include + +void * +xcalloc (size_t nmemb, size_t size) +{ + void *ret; + + ret = calloc (nmemb, size); + if (ret == NULL) { + fprintf (stderr, "Out of memory.\n"); + exit (1); + } + + return ret; +} + +void * +xmalloc (size_t size) +{ + void *ret; + + ret = malloc (size); + if (ret == NULL) { + fprintf (stderr, "Out of memory.\n"); + exit (1); + } + + return ret; +} + +void * +xrealloc (void *ptr, size_t size) +{ + void *ret; + + ret = realloc (ptr, size); + if (ret == NULL) { + fprintf (stderr, "Out of memory.\n"); + exit (1); + } + + return ret; +} + +char * +xstrdup (const char *s) +{ + char *ret; + + ret = strdup (s); + if (ret == NULL) { + fprintf (stderr, "Out of memory.\n"); + exit (1); + } + + return ret; +} + +char * +xstrndup (const char *s, size_t n) +{ + char *ret; + + ret = strndup (s, n); + if (ret == NULL) { + fprintf (stderr, "Out of memory.\n"); + exit (1); + } + + return ret; +} + +void +xregcomp (regex_t *preg, const char *regex, int cflags) +{ + int rerr; + + rerr = regcomp (preg, regex, cflags); + if (rerr) { + size_t error_size = regerror (rerr, preg, NULL, 0); + char *error = xmalloc (error_size); + + regerror (rerr, preg, error, error_size); + INTERNAL_ERROR ("compiling regex %s: %s\n", + regex, error); + } +} + +int +xregexec (const regex_t *preg, const char *string, + size_t nmatch, regmatch_t pmatch[], int eflags) +{ + unsigned int i; + int rerr; + + rerr = regexec (preg, string, nmatch, pmatch, eflags); + if (rerr) + return rerr; + + for (i = 0; i < nmatch; i++) { + if (pmatch[i].rm_so == -1) + INTERNAL_ERROR ("matching regex against %s: Sub-match %d not found\n", + string, i); + } + + return 0; +} diff --git a/lib/xutil.h b/lib/xutil.h new file mode 100644 index 00000000..b973f7dc --- /dev/null +++ b/lib/xutil.h @@ -0,0 +1,51 @@ +/* xutil.h - Various wrapper functions to abort on error. + * + * Copyright © 2009 Carl Worth + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Author: Carl Worth + */ + +#ifndef NOTMUCH_XUTIL_H +#define NOTMUCH_XUTIL_H + +#include +#include +#include + +/* xutil.c */ +void * +xcalloc (size_t nmemb, size_t size); + +void * +xmalloc (size_t size); + +void * +xrealloc (void *ptrr, size_t size); + +char * +xstrdup (const char *s); + +char * +xstrndup (const char *s, size_t n); + +void +xregcomp (regex_t *preg, const char *regex, int cflags); + +int +xregexec (const regex_t *preg, const char *string, + size_t nmatch, regmatch_t pmatch[], int eflags); + +#endif diff --git a/libsha1.c b/libsha1.c deleted file mode 100644 index c39a5a17..00000000 --- a/libsha1.c +++ /dev/null @@ -1,242 +0,0 @@ -/* - --------------------------------------------------------------------------- - Copyright (c) 2002, Dr Brian Gladman, Worcester, UK. All rights reserved. - - LICENSE TERMS - - The free distribution and use of this software in both source and binary - form is allowed (with or without changes) provided that: - - 1. distributions of this source code include the above copyright - notice, this list of conditions and the following disclaimer; - - 2. distributions in binary form include the above copyright - notice, this list of conditions and the following disclaimer - in the documentation and/or other associated materials; - - 3. the copyright holder's name is not used to endorse products - built using this software without specific written permission. - - ALTERNATIVELY, provided that this notice is retained in full, this product - may be distributed under the terms of the GNU General Public License (GPL), - in which case the provisions of the GPL apply INSTEAD OF those given above. - - DISCLAIMER - - This software is provided 'as is' with no explicit or implied warranties - in respect of its properties, including, but not limited to, correctness - and/or fitness for purpose. - --------------------------------------------------------------------------- - Issue Date: 01/08/2005 - - This is a byte oriented version of SHA1 that operates on arrays of bytes - stored in memory. -*/ - -#include /* for memcpy() etc. */ - -#include "libsha1.h" - -#if defined(__cplusplus) -extern "C" -{ -#endif - -#define SHA1_BLOCK_SIZE 64 - -#define rotl32(x,n) (((x) << n) | ((x) >> (32 - n))) -#define rotr32(x,n) (((x) >> n) | ((x) << (32 - n))) - -#define bswap_32(x) ((rotr32((x), 24) & 0x00ff00ff) | (rotr32((x), 8) & 0xff00ff00)) - -#if (PLATFORM_BYTE_ORDER == IS_LITTLE_ENDIAN) -#define bsw_32(p,n) \ - { int _i = (n); while(_i--) ((uint32_t*)p)[_i] = bswap_32(((uint32_t*)p)[_i]); } -#else -#define bsw_32(p,n) -#endif - -#define SHA1_MASK (SHA1_BLOCK_SIZE - 1) - -#if 0 - -#define ch(x,y,z) (((x) & (y)) ^ (~(x) & (z))) -#define parity(x,y,z) ((x) ^ (y) ^ (z)) -#define maj(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) - -#else /* Discovered by Rich Schroeppel and Colin Plumb */ - -#define ch(x,y,z) ((z) ^ ((x) & ((y) ^ (z)))) -#define parity(x,y,z) ((x) ^ (y) ^ (z)) -#define maj(x,y,z) (((x) & (y)) | ((z) & ((x) ^ (y)))) - -#endif - -/* Compile 64 bytes of hash data into SHA1 context. Note */ -/* that this routine assumes that the byte order in the */ -/* ctx->wbuf[] at this point is in such an order that low */ -/* address bytes in the ORIGINAL byte stream will go in */ -/* this buffer to the high end of 32-bit words on BOTH big */ -/* and little endian systems */ - -#ifdef ARRAY -#define q(v,n) v[n] -#else -#define q(v,n) v##n -#endif - -#define one_cycle(v,a,b,c,d,e,f,k,h) \ - q(v,e) += rotr32(q(v,a),27) + \ - f(q(v,b),q(v,c),q(v,d)) + k + h; \ - q(v,b) = rotr32(q(v,b), 2) - -#define five_cycle(v,f,k,i) \ - one_cycle(v, 0,1,2,3,4, f,k,hf(i )); \ - one_cycle(v, 4,0,1,2,3, f,k,hf(i+1)); \ - one_cycle(v, 3,4,0,1,2, f,k,hf(i+2)); \ - one_cycle(v, 2,3,4,0,1, f,k,hf(i+3)); \ - one_cycle(v, 1,2,3,4,0, f,k,hf(i+4)) - -static void sha1_compile(sha1_ctx ctx[1]) -{ uint32_t *w = ctx->wbuf; - -#ifdef ARRAY - uint32_t v[5]; - memcpy(v, ctx->hash, 5 * sizeof(uint32_t)); -#else - uint32_t v0, v1, v2, v3, v4; - v0 = ctx->hash[0]; v1 = ctx->hash[1]; - v2 = ctx->hash[2]; v3 = ctx->hash[3]; - v4 = ctx->hash[4]; -#endif - -#define hf(i) w[i] - - five_cycle(v, ch, 0x5a827999, 0); - five_cycle(v, ch, 0x5a827999, 5); - five_cycle(v, ch, 0x5a827999, 10); - one_cycle(v,0,1,2,3,4, ch, 0x5a827999, hf(15)); \ - -#undef hf -#define hf(i) (w[(i) & 15] = rotl32( \ - w[((i) + 13) & 15] ^ w[((i) + 8) & 15] \ - ^ w[((i) + 2) & 15] ^ w[(i) & 15], 1)) - - one_cycle(v,4,0,1,2,3, ch, 0x5a827999, hf(16)); - one_cycle(v,3,4,0,1,2, ch, 0x5a827999, hf(17)); - one_cycle(v,2,3,4,0,1, ch, 0x5a827999, hf(18)); - one_cycle(v,1,2,3,4,0, ch, 0x5a827999, hf(19)); - - five_cycle(v, parity, 0x6ed9eba1, 20); - five_cycle(v, parity, 0x6ed9eba1, 25); - five_cycle(v, parity, 0x6ed9eba1, 30); - five_cycle(v, parity, 0x6ed9eba1, 35); - - five_cycle(v, maj, 0x8f1bbcdc, 40); - five_cycle(v, maj, 0x8f1bbcdc, 45); - five_cycle(v, maj, 0x8f1bbcdc, 50); - five_cycle(v, maj, 0x8f1bbcdc, 55); - - five_cycle(v, parity, 0xca62c1d6, 60); - five_cycle(v, parity, 0xca62c1d6, 65); - five_cycle(v, parity, 0xca62c1d6, 70); - five_cycle(v, parity, 0xca62c1d6, 75); - -#ifdef ARRAY - ctx->hash[0] += v[0]; ctx->hash[1] += v[1]; - ctx->hash[2] += v[2]; ctx->hash[3] += v[3]; - ctx->hash[4] += v[4]; -#else - ctx->hash[0] += v0; ctx->hash[1] += v1; - ctx->hash[2] += v2; ctx->hash[3] += v3; - ctx->hash[4] += v4; -#endif -} - -void sha1_begin(sha1_ctx ctx[1]) -{ - ctx->count[0] = ctx->count[1] = 0; - ctx->hash[0] = 0x67452301; - ctx->hash[1] = 0xefcdab89; - ctx->hash[2] = 0x98badcfe; - ctx->hash[3] = 0x10325476; - ctx->hash[4] = 0xc3d2e1f0; -} - -/* SHA1 hash data in an array of bytes into hash buffer and */ -/* call the hash_compile function as required. */ - -void sha1_hash(const unsigned char data[], unsigned long len, sha1_ctx ctx[1]) -{ uint32_t pos = (uint32_t)(ctx->count[0] & SHA1_MASK), - space = SHA1_BLOCK_SIZE - pos; - const unsigned char *sp = data; - - if((ctx->count[0] += len) < len) - ++(ctx->count[1]); - - while(len >= space) /* tranfer whole blocks if possible */ - { - memcpy(((unsigned char*)ctx->wbuf) + pos, sp, space); - sp += space; len -= space; space = SHA1_BLOCK_SIZE; pos = 0; - bsw_32(ctx->wbuf, SHA1_BLOCK_SIZE >> 2); - sha1_compile(ctx); - } - - memcpy(((unsigned char*)ctx->wbuf) + pos, sp, len); -} - -/* SHA1 final padding and digest calculation */ - -void sha1_end(unsigned char hval[], sha1_ctx ctx[1]) -{ uint32_t i = (uint32_t)(ctx->count[0] & SHA1_MASK); - - /* put bytes in the buffer in an order in which references to */ - /* 32-bit words will put bytes with lower addresses into the */ - /* top of 32 bit words on BOTH big and little endian machines */ - bsw_32(ctx->wbuf, (i + 3) >> 2); - - /* we now need to mask valid bytes and add the padding which is */ - /* a single 1 bit and as many zero bits as necessary. Note that */ - /* we can always add the first padding byte here because the */ - /* buffer always has at least one empty slot */ - ctx->wbuf[i >> 2] &= 0xffffff80 << 8 * (~i & 3); - ctx->wbuf[i >> 2] |= 0x00000080 << 8 * (~i & 3); - - /* we need 9 or more empty positions, one for the padding byte */ - /* (above) and eight for the length count. If there is not */ - /* enough space, pad and empty the buffer */ - if(i > SHA1_BLOCK_SIZE - 9) - { - if(i < 60) ctx->wbuf[15] = 0; - sha1_compile(ctx); - i = 0; - } - else /* compute a word index for the empty buffer positions */ - i = (i >> 2) + 1; - - while(i < 14) /* and zero pad all but last two positions */ - ctx->wbuf[i++] = 0; - - /* the following 32-bit length fields are assembled in the */ - /* wrong byte order on little endian machines but this is */ - /* corrected later since they are only ever used as 32-bit */ - /* word values. */ - ctx->wbuf[14] = (ctx->count[1] << 3) | (ctx->count[0] >> 29); - ctx->wbuf[15] = ctx->count[0] << 3; - sha1_compile(ctx); - - /* extract the hash value as bytes in case the hash buffer is */ - /* misaligned for 32-bit words */ - for(i = 0; i < SHA1_DIGEST_SIZE; ++i) - hval[i] = (unsigned char)(ctx->hash[i >> 2] >> (8 * (~i & 3))); -} - -void sha1(unsigned char hval[], const unsigned char data[], unsigned long len) -{ sha1_ctx cx[1]; - - sha1_begin(cx); sha1_hash(data, len, cx); sha1_end(hval, cx); -} - -#if defined(__cplusplus) -} -#endif diff --git a/libsha1.h b/libsha1.h deleted file mode 100644 index b4dca93b..00000000 --- a/libsha1.h +++ /dev/null @@ -1,67 +0,0 @@ -/* - --------------------------------------------------------------------------- - Copyright (c) 2002, Dr Brian Gladman, Worcester, UK. All rights reserved. - - LICENSE TERMS - - The free distribution and use of this software in both source and binary - form is allowed (with or without changes) provided that: - - 1. distributions of this source code include the above copyright - notice, this list of conditions and the following disclaimer; - - 2. distributions in binary form include the above copyright - notice, this list of conditions and the following disclaimer - in the documentation and/or other associated materials; - - 3. the copyright holder's name is not used to endorse products - built using this software without specific written permission. - - ALTERNATIVELY, provided that this notice is retained in full, this product - may be distributed under the terms of the GNU General Public License (GPL), - in which case the provisions of the GPL apply INSTEAD OF those given above. - - DISCLAIMER - - This software is provided 'as is' with no explicit or implied warranties - in respect of its properties, including, but not limited to, correctness - and/or fitness for purpose. - --------------------------------------------------------------------------- - Issue Date: 01/08/2005 -*/ - -#ifndef _SHA1_H -#define _SHA1_H - -#if defined(__cplusplus) -extern "C" -{ -#endif -#if 0 -} /* Appleasing Emacs */ -#endif - -#include - -/* Size of SHA1 digest */ - -#define SHA1_DIGEST_SIZE 20 - -/* type to hold the SHA1 context */ - -typedef struct -{ uint32_t count[2]; - uint32_t hash[5]; - uint32_t wbuf[16]; -} sha1_ctx; - -void sha1_begin(sha1_ctx ctx[1]); -void sha1_hash(const unsigned char data[], unsigned long len, sha1_ctx ctx[1]); -void sha1_end(unsigned char hval[], sha1_ctx ctx[1]); -void sha1(unsigned char hval[], const unsigned char data[], unsigned long len); - -#if defined(__cplusplus) -} -#endif - -#endif diff --git a/message-file.c b/message-file.c deleted file mode 100644 index 75caba6d..00000000 --- a/message-file.c +++ /dev/null @@ -1,352 +0,0 @@ -/* message.c - Utility functions for parsing an email message for notmuch. - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#include - -#include "notmuch-private.h" - -#include - -#include /* GHashTable */ - -typedef struct { - char *str; - size_t size; - size_t len; -} header_value_closure_t; - -struct _notmuch_message_file { - /* File object */ - FILE *file; - - /* Header storage */ - int restrict_headers; - GHashTable *headers; - int broken_headers; - int good_headers; - size_t header_size; /* Length of full message header in bytes. */ - - /* Parsing state */ - char *line; - size_t line_size; - header_value_closure_t value; - - int parsing_started; - int parsing_finished; -}; - -static int -strcase_equal (const void *a, const void *b) -{ - return strcasecmp (a, b) == 0; -} - -static unsigned int -strcase_hash (const void *ptr) -{ - const char *s = ptr; - - /* This is the djb2 hash. */ - unsigned int hash = 5381; - while (s && *s) { - hash = ((hash << 5) + hash) + tolower (*s); - s++; - } - - return hash; -} - -static int -_notmuch_message_file_destructor (notmuch_message_file_t *message) -{ - if (message->line) - free (message->line); - - if (message->value.size) - free (message->value.str); - - if (message->headers) - g_hash_table_destroy (message->headers); - - if (message->file) - fclose (message->file); - - return 0; -} - -/* Create a new notmuch_message_file_t for 'filename' with 'ctx' as - * the talloc owner. */ -notmuch_message_file_t * -_notmuch_message_file_open_ctx (void *ctx, const char *filename) -{ - notmuch_message_file_t *message; - - message = talloc_zero (ctx, notmuch_message_file_t); - if (unlikely (message == NULL)) - return NULL; - - talloc_set_destructor (message, _notmuch_message_file_destructor); - - message->file = fopen (filename, "r"); - if (message->file == NULL) - goto FAIL; - - message->headers = g_hash_table_new_full (strcase_hash, - strcase_equal, - free, - free); - - message->parsing_started = 0; - message->parsing_finished = 0; - - return message; - - FAIL: - fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno)); - notmuch_message_file_close (message); - - return NULL; -} - -notmuch_message_file_t * -notmuch_message_file_open (const char *filename) -{ - return _notmuch_message_file_open_ctx (NULL, filename); -} - -void -notmuch_message_file_close (notmuch_message_file_t *message) -{ - talloc_free (message); -} - -void -notmuch_message_file_restrict_headersv (notmuch_message_file_t *message, - va_list va_headers) -{ - char *header; - - if (message->parsing_started) - INTERNAL_ERROR ("notmuch_message_file_restrict_headers called after parsing has started"); - - while (1) { - header = va_arg (va_headers, char*); - if (header == NULL) - break; - g_hash_table_insert (message->headers, - xstrdup (header), NULL); - } - - message->restrict_headers = 1; -} - -void -notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...) -{ - va_list va_headers; - - va_start (va_headers, message); - - notmuch_message_file_restrict_headersv (message, va_headers); -} - -static void -copy_header_unfolding (header_value_closure_t *value, - const char *chunk) -{ - char *last; - - if (chunk == NULL) - return; - - while (*chunk == ' ' || *chunk == '\t') - chunk++; - - if (value->len + 1 + strlen (chunk) + 1 > value->size) { - unsigned int new_size = value->size; - if (value->size == 0) - new_size = strlen (chunk) + 1; - else - while (value->len + 1 + strlen (chunk) + 1 > new_size) - new_size *= 2; - value->str = xrealloc (value->str, new_size); - value->size = new_size; - } - - last = value->str + value->len; - if (value->len) { - *last = ' '; - last++; - value->len++; - } - - strcpy (last, chunk); - value->len += strlen (chunk); - - last = value->str + value->len - 1; - if (*last == '\n') { - *last = '\0'; - value->len--; - } -} - -/* As a special-case, a value of NULL for header_desired will force - * the entire header to be parsed if it is not parsed already. This is - * used by the _notmuch_message_file_get_headers_end function. */ -const char * -notmuch_message_file_get_header (notmuch_message_file_t *message, - const char *header_desired) -{ - int contains; - char *header, *decoded_value; - const char *s, *colon; - int match; - static int initialized = 0; - - if (! initialized) { - g_mime_init (0); - initialized = 1; - } - - message->parsing_started = 1; - - if (header_desired == NULL) - contains = 0; - else - contains = g_hash_table_lookup_extended (message->headers, - header_desired, NULL, - (gpointer *) &decoded_value); - - if (contains && decoded_value) - return decoded_value; - - if (message->parsing_finished) - return NULL; - -#define NEXT_HEADER_LINE(closure) \ - while (1) { \ - ssize_t bytes_read = getline (&message->line, \ - &message->line_size, \ - message->file); \ - if (bytes_read == -1) { \ - message->parsing_finished = 1; \ - break; \ - } \ - if (*message->line == '\n') { \ - message->parsing_finished = 1; \ - break; \ - } \ - if (closure && \ - (*message->line == ' ' || *message->line == '\t')) \ - { \ - copy_header_unfolding ((closure), message->line); \ - } \ - if (*message->line == ' ' || *message->line == '\t') \ - message->header_size += strlen (message->line); \ - else \ - break; \ - } - - if (message->line == NULL) - NEXT_HEADER_LINE (NULL); - - while (1) { - - if (message->parsing_finished) - break; - - colon = strchr (message->line, ':'); - - if (colon == NULL) { - message->broken_headers++; - /* A simple heuristic for giving up on things that just - * don't look like mail messages. */ - if (message->broken_headers >= 10 && - message->good_headers < 5) - { - message->parsing_finished = 1; - continue; - } - NEXT_HEADER_LINE (NULL); - continue; - } - - message->header_size += strlen (message->line); - - message->good_headers++; - - header = xstrndup (message->line, colon - message->line); - - if (message->restrict_headers && - ! g_hash_table_lookup_extended (message->headers, - header, NULL, NULL)) - { - free (header); - NEXT_HEADER_LINE (NULL); - continue; - } - - s = colon + 1; - while (*s == ' ' || *s == '\t') - s++; - - message->value.len = 0; - copy_header_unfolding (&message->value, s); - - NEXT_HEADER_LINE (&message->value); - - if (header_desired == 0) - match = 0; - else - match = (strcasecmp (header, header_desired) == 0); - - decoded_value = g_mime_utils_header_decode_text (message->value.str); - - g_hash_table_insert (message->headers, header, decoded_value); - - if (match) - return decoded_value; - } - - if (message->line) - free (message->line); - message->line = NULL; - - if (message->value.size) { - free (message->value.str); - message->value.str = NULL; - message->value.size = 0; - message->value.len = 0; - } - - /* We've parsed all headers and never found the one we're looking - * for. It's probably just not there, but let's check that we - * didn't make a mistake preventing us from seeing it. */ - if (message->restrict_headers && header_desired && - ! g_hash_table_lookup_extended (message->headers, - header_desired, NULL, NULL)) - { - INTERNAL_ERROR ("Attempt to get header \"%s\" which was not\n" - "included in call to notmuch_message_file_restrict_headers\n", - header_desired); - } - - return NULL; -} diff --git a/message.cc b/message.cc deleted file mode 100644 index 28f19a88..00000000 --- a/message.cc +++ /dev/null @@ -1,658 +0,0 @@ -/* message.cc - Results of message-based searches from a notmuch database - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#include "notmuch-private.h" -#include "database-private.h" - -#include - -#include - -struct _notmuch_message { - notmuch_database_t *notmuch; - Xapian::docid doc_id; - int frozen; - char *message_id; - char *thread_id; - char *filename; - notmuch_message_file_t *message_file; - Xapian::Document doc; -}; - -/* "128 bits of thread-id ought to be enough for anybody" */ -#define NOTMUCH_THREAD_ID_BITS 128 -#define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4) -typedef struct _thread_id { - char str[NOTMUCH_THREAD_ID_DIGITS + 1]; -} thread_id_t; - -/* We end up having to call the destructor explicitly because we had - * to use "placement new" in order to initialize C++ objects within a - * block that we allocated with talloc. So C++ is making talloc - * slightly less simple to use, (we wouldn't need - * talloc_set_destructor at all otherwise). - */ -static int -_notmuch_message_destructor (notmuch_message_t *message) -{ - message->doc.~Document (); - - return 0; -} - -/* Create a new notmuch_message_t object for an existing document in - * the database. - * - * Here, 'talloc owner' is an optional talloc context to which the new - * message will belong. This allows for the caller to not bother - * calling notmuch_message_destroy on the message, and no that all - * memory will be reclaimed with 'talloc_owner' is free. The caller - * still can call notmuch_message_destroy when finished with the - * message if desired. - * - * The 'talloc_owner' argument can also be NULL, in which case the - * caller *is* responsible for calling notmuch_message_destroy. - * - * If no document exists in the database with document ID of 'doc_id' - * then this function returns NULL and optionally sets *status to - * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND. - * - * This function can also fail to due lack of available memory, - * returning NULL and optionally setting *status to - * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY. - * - * The caller can pass NULL for status if uninterested in - * distinguishing these two cases. - */ -notmuch_message_t * -_notmuch_message_create (const void *talloc_owner, - notmuch_database_t *notmuch, - unsigned int doc_id, - notmuch_private_status_t *status) -{ - notmuch_message_t *message; - - if (status) - *status = NOTMUCH_PRIVATE_STATUS_SUCCESS; - - message = talloc (talloc_owner, notmuch_message_t); - if (unlikely (message == NULL)) { - if (status) - *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY; - return NULL; - } - - message->notmuch = notmuch; - message->doc_id = doc_id; - - message->frozen = 0; - - /* Each of these will be lazily created as needed. */ - message->message_id = NULL; - message->thread_id = NULL; - message->filename = NULL; - message->message_file = NULL; - - /* This is C++'s creepy "placement new", which is really just an - * ugly way to call a constructor for a pre-allocated object. So - * it's really not an error to not be checking for OUT_OF_MEMORY - * here, since this "new" isn't actually allocating memory. This - * is language-design comedy of the wrong kind. */ - - new (&message->doc) Xapian::Document; - - talloc_set_destructor (message, _notmuch_message_destructor); - - try { - message->doc = notmuch->xapian_db->get_document (doc_id); - } catch (const Xapian::DocNotFoundError &error) { - talloc_free (message); - if (status) - *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND; - return NULL; - } - - return message; -} - -/* Create a new notmuch_message_t object for a specific message ID, - * (which may or may not already exist in the databas). - * - * Here, 'talloc owner' is an optional talloc context to which the new - * message will belong. This allows for the caller to not bother - * calling notmuch_message_destroy on the message, and no that all - * memory will be reclaimed with 'talloc_owner' is free. The caller - * still can call notmuch_message_destroy when finished with the - * message if desired. - * - * The 'talloc_owner' argument can also be NULL, in which case the - * caller *is* responsible for calling notmuch_message_destroy. - * - * If there is already a document with message ID 'message_id' in the - * database, then the returned message can be used to query/modify the - * document. Otherwise, a new document will be inserted into the - * database before this function returns, (and *status will be set - * to NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND). - * - * If an error occurs, this function will return NULL and *status - * will be set as appropriate. (The status pointer argument must - * not be NULL.) - */ -notmuch_message_t * -_notmuch_message_create_for_message_id (const void *talloc_owner, - notmuch_database_t *notmuch, - const char *message_id, - notmuch_private_status_t *status_ret) -{ - notmuch_message_t *message; - Xapian::Document doc; - unsigned int doc_id; - char *term; - - *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS; - - message = notmuch_database_find_message (notmuch, message_id); - if (message) - return talloc_steal (talloc_owner, message); - - term = talloc_asprintf (NULL, "%s%s", - _find_prefix ("id"), message_id); - if (term == NULL) { - *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY; - return NULL; - } - - try { - doc.add_term (term); - talloc_free (term); - - doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id); - - doc_id = notmuch->xapian_db->add_document (doc); - } catch (const Xapian::Error &error) { - *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION; - return NULL; - } - - message = _notmuch_message_create (talloc_owner, notmuch, - doc_id, status_ret); - - /* We want to inform the caller that we had to create a new - * document. */ - if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS) - *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND; - - return message; -} - -const char * -notmuch_message_get_message_id (notmuch_message_t *message) -{ - Xapian::TermIterator i; - - if (message->message_id) - return message->message_id; - - i = message->doc.termlist_begin (); - i.skip_to (_find_prefix ("id")); - - if (i == message->doc.termlist_end ()) - INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n", - message->doc_id); - - message->message_id = talloc_strdup (message, (*i).c_str () + 1); - -#if DEBUG_DATABASE_SANITY - i++; - - if (i != message->doc.termlist_end () && - strncmp ((*i).c_str (), _find_prefix ("id"), - strlen (_find_prefix ("id"))) == 0) - { - INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs", - message->doc_id); - } -#endif - - return message->message_id; -} - -static void -_notmuch_message_ensure_message_file (notmuch_message_t *message) -{ - const char *filename; - - if (message->message_file) - return; - - filename = notmuch_message_get_filename (message); - if (unlikely (filename == NULL)) - return; - - message->message_file = _notmuch_message_file_open_ctx (message, filename); -} - -const char * -notmuch_message_get_header (notmuch_message_t *message, const char *header) -{ - _notmuch_message_ensure_message_file (message); - if (message->message_file == NULL) - return NULL; - - return notmuch_message_file_get_header (message->message_file, header); -} - -const char * -notmuch_message_get_thread_id (notmuch_message_t *message) -{ - Xapian::TermIterator i; - - if (message->thread_id) - return message->thread_id; - - i = message->doc.termlist_begin (); - i.skip_to (_find_prefix ("thread")); - - if (i == message->doc.termlist_end ()) - INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n", - message->doc_id); - - message->thread_id = talloc_strdup (message, (*i).c_str () + 1); - -#if DEBUG_DATABASE_SANITY - i++; - - if (i != message->doc.termlist_end () && - strncmp ((*i).c_str (), _find_prefix ("thread"), - strlen (_find_prefix ("thread"))) == 0) - { - INTERNAL_ERROR ("Message %s has duplicate thread IDs: %s and %s\n", - notmuch_message_get_message_id (message), - message->thread_id, - (*i).c_str () + 1); - } -#endif - - return message->thread_id; -} - -/* Set the filename for 'message' to 'filename'. - * - * XXX: We should still figure out if we think it's important to store - * multiple filenames for email messages with identical message IDs. - * - * This change will not be reflected in the database until the next - * call to _notmuch_message_set_sync. */ -void -_notmuch_message_set_filename (notmuch_message_t *message, - const char *filename) -{ - const char *s; - const char *db_path; - unsigned int db_path_len; - - if (message->filename) { - talloc_free (message->filename); - message->filename = NULL; - } - - if (filename == NULL) - INTERNAL_ERROR ("Message filename cannot be NULL."); - - s = filename; - - db_path = notmuch_database_get_path (message->notmuch); - db_path_len = strlen (db_path); - - if (*s == '/' && strncmp (s, db_path, db_path_len) == 0 - && strlen (s) > db_path_len) - { - s += db_path_len + 1; - } - - message->doc.set_data (s); -} - -const char * -notmuch_message_get_filename (notmuch_message_t *message) -{ - std::string filename_str; - const char *db_path; - - if (message->filename) - return message->filename; - - filename_str = message->doc.get_data (); - db_path = notmuch_database_get_path (message->notmuch); - - if (filename_str[0] != '/') - message->filename = talloc_asprintf (message, "%s/%s", db_path, - filename_str.c_str ()); - else - message->filename = talloc_strdup (message, filename_str.c_str ()); - - return message->filename; -} - -time_t -notmuch_message_get_date (notmuch_message_t *message) -{ - std::string value; - - try { - value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP); - } catch (Xapian::Error &error) { - INTERNAL_ERROR ("Failed to read timestamp value from document."); - return 0; - } - - return Xapian::sortable_unserialise (value); -} - -notmuch_tags_t * -notmuch_message_get_tags (notmuch_message_t *message) -{ - const char *prefix = _find_prefix ("tag"); - Xapian::TermIterator i, end; - notmuch_tags_t *tags; - std::string tag; - - /* Currently this iteration is written with the assumption that - * "tag" has a single-character prefix. */ - assert (strlen (prefix) == 1); - - tags = _notmuch_tags_create (message); - if (unlikely (tags == NULL)) - return NULL; - - i = message->doc.termlist_begin (); - end = message->doc.termlist_end (); - - i.skip_to (prefix); - - while (1) { - tag = *i; - - if (tag.empty () || tag[0] != *prefix) - break; - - _notmuch_tags_add_tag (tags, tag.c_str () + 1); - - i++; - } - - _notmuch_tags_prepare_iterator (tags); - - return tags; -} - -void -_notmuch_message_set_date (notmuch_message_t *message, - const char *date) -{ - time_t time_value; - - /* GMime really doesn't want to see a NULL date, so protect its - * sensibilities. */ - if (date == NULL) - time_value = 0; - else - time_value = g_mime_utils_header_decode_date (date, NULL); - - message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP, - Xapian::sortable_serialise (time_value)); -} - -static void -thread_id_generate (thread_id_t *thread_id) -{ - static int seeded = 0; - FILE *dev_random; - uint32_t value; - char *s; - int i; - - if (! seeded) { - dev_random = fopen ("/dev/random", "r"); - if (dev_random == NULL) { - srand (time (NULL)); - } else { - fread ((void *) &value, sizeof (value), 1, dev_random); - srand (value); - fclose (dev_random); - } - seeded = 1; - } - - s = thread_id->str; - for (i = 0; i < NOTMUCH_THREAD_ID_DIGITS; i += 8) { - value = rand (); - sprintf (s, "%08x", value); - s += 8; - } -} - -void -_notmuch_message_ensure_thread_id (notmuch_message_t *message) -{ - /* If not part of any existing thread, generate a new thread_id. */ - thread_id_t thread_id; - - thread_id_generate (&thread_id); - _notmuch_message_add_term (message, "thread", thread_id.str); -} - -/* Synchronize changes made to message->doc out into the database. */ -void -_notmuch_message_sync (notmuch_message_t *message) -{ - Xapian::WritableDatabase *db = message->notmuch->xapian_db; - - db->replace_document (message->doc_id, message->doc); -} - -/* Add a name:value term to 'message', (the actual term will be - * encoded by prefixing the value with a short prefix). See - * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term - * names to prefix values. - * - * This change will not be reflected in the database until the next - * call to _notmuch_message_set_sync. */ -notmuch_private_status_t -_notmuch_message_add_term (notmuch_message_t *message, - const char *prefix_name, - const char *value) -{ - - char *term; - - if (value == NULL) - return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; - - term = talloc_asprintf (message, "%s%s", - _find_prefix (prefix_name), value); - - if (strlen (term) > NOTMUCH_TERM_MAX) - return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; - - message->doc.add_term (term); - - talloc_free (term); - - return NOTMUCH_PRIVATE_STATUS_SUCCESS; -} - -/* Parse 'text' and add a term to 'message' for each parsed word. Each - * term will be added both prefixed (if prefix_name is not NULL) and - * also unprefixed). */ -notmuch_private_status_t -_notmuch_message_gen_terms (notmuch_message_t *message, - const char *prefix_name, - const char *text) -{ - Xapian::TermGenerator *term_gen = message->notmuch->term_gen; - - if (text == NULL) - return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; - - term_gen->set_document (message->doc); - - if (prefix_name) { - const char *prefix = _find_prefix (prefix_name); - - term_gen->index_text (text, 1, prefix); - } - - term_gen->index_text (text); - - return NOTMUCH_PRIVATE_STATUS_SUCCESS; -} - -/* Remove a name:value term from 'message', (the actual term will be - * encoded by prefixing the value with a short prefix). See - * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term - * names to prefix values. - * - * This change will not be reflected in the database until the next - * call to _notmuch_message_set_sync. */ -notmuch_private_status_t -_notmuch_message_remove_term (notmuch_message_t *message, - const char *prefix_name, - const char *value) -{ - char *term; - - if (value == NULL) - return NOTMUCH_PRIVATE_STATUS_NULL_POINTER; - - term = talloc_asprintf (message, "%s%s", - _find_prefix (prefix_name), value); - - if (strlen (term) > NOTMUCH_TERM_MAX) - return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG; - - try { - message->doc.remove_term (term); - } catch (const Xapian::InvalidArgumentError) { - /* We'll let the philosopher's try to wrestle with the - * question of whether failing to remove that which was not - * there in the first place is failure. For us, we'll silently - * consider it all good. */ - } - - talloc_free (term); - - return NOTMUCH_PRIVATE_STATUS_SUCCESS; -} - -notmuch_status_t -notmuch_message_add_tag (notmuch_message_t *message, const char *tag) -{ - notmuch_private_status_t status; - - if (tag == NULL) - return NOTMUCH_STATUS_NULL_POINTER; - - if (strlen (tag) > NOTMUCH_TAG_MAX) - return NOTMUCH_STATUS_TAG_TOO_LONG; - - status = _notmuch_message_add_term (message, "tag", tag); - if (status) { - INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n", - status); - } - - if (! message->frozen) - _notmuch_message_sync (message); - - return NOTMUCH_STATUS_SUCCESS; -} - -notmuch_status_t -notmuch_message_remove_tag (notmuch_message_t *message, const char *tag) -{ - notmuch_private_status_t status; - - if (tag == NULL) - return NOTMUCH_STATUS_NULL_POINTER; - - if (strlen (tag) > NOTMUCH_TAG_MAX) - return NOTMUCH_STATUS_TAG_TOO_LONG; - - status = _notmuch_message_remove_term (message, "tag", tag); - if (status) { - INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n", - status); - } - - if (! message->frozen) - _notmuch_message_sync (message); - - return NOTMUCH_STATUS_SUCCESS; -} - -void -notmuch_message_remove_all_tags (notmuch_message_t *message) -{ - notmuch_private_status_t status; - notmuch_tags_t *tags; - const char *tag; - - for (tags = notmuch_message_get_tags (message); - notmuch_tags_has_more (tags); - notmuch_tags_advance (tags)) - { - tag = notmuch_tags_get (tags); - - status = _notmuch_message_remove_term (message, "tag", tag); - if (status) { - INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n", - status); - } - } - - if (! message->frozen) - _notmuch_message_sync (message); -} - -void -notmuch_message_freeze (notmuch_message_t *message) -{ - message->frozen++; -} - -notmuch_status_t -notmuch_message_thaw (notmuch_message_t *message) -{ - if (message->frozen > 0) { - message->frozen--; - if (message->frozen == 0) - _notmuch_message_sync (message); - return NOTMUCH_STATUS_SUCCESS; - } else { - return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW; - } -} - -void -notmuch_message_destroy (notmuch_message_t *message) -{ - talloc_free (message); -} diff --git a/notmuch-private.h b/notmuch-private.h deleted file mode 100644 index 92b46343..00000000 --- a/notmuch-private.h +++ /dev/null @@ -1,314 +0,0 @@ -/* notmuch-private.h - Internal interfaces for notmuch. - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#ifndef NOTMUCH_PRIVATE_H -#define NOTMUCH_PRIVATE_H - -#ifndef _GNU_SOURCE -#define _GNU_SOURCE /* For getline and asprintf */ -#endif -#include - -#include "notmuch.h" - -NOTMUCH_BEGIN_DECLS - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "xutil.h" - -#ifdef DEBUG -# define DEBUG_DATABASE_SANITY 1 -# define DEBUG_QUERY 1 -#endif - -#define COMPILE_TIME_ASSERT(pred) ((void)sizeof(char[1 - 2*!(pred)])) - -/* There's no point in continuing when we've detected that we've done - * something wrong internally (as opposed to the user passing in a - * bogus value). - * - * Note that PRINTF_ATTRIBUTE comes from talloc.h - */ -int -_internal_error (const char *format, ...) PRINTF_ATTRIBUTE (1, 2); - -/* There's no point in continuing when we've detected that we've done - * something wrong internally (as opposed to the user passing in a - * bogus value). - * - * Note that __location__ comes from talloc.h. - */ -#define INTERNAL_ERROR(format, ...) \ - _internal_error (format " (%s).\n", \ - ##__VA_ARGS__, __location__) - -#define unused(x) x __attribute__ ((unused)) - -/* Thanks to Andrew Tridgell's (SAMBA's) talloc for this definition of - * unlikely. The talloc source code comes to us via the GNU LGPL v. 3. - */ -/* these macros gain us a few percent of speed on gcc */ -#if (__GNUC__ >= 3) -/* the strange !! is to ensure that __builtin_expect() takes either 0 or 1 - as its first argument */ -#ifndef likely -#define likely(x) __builtin_expect(!!(x), 1) -#endif -#ifndef unlikely -#define unlikely(x) __builtin_expect(!!(x), 0) -#endif -#else -#ifndef likely -#define likely(x) (x) -#endif -#ifndef unlikely -#define unlikely(x) (x) -#endif -#endif - -typedef enum { - NOTMUCH_VALUE_TIMESTAMP = 0, - NOTMUCH_VALUE_MESSAGE_ID -} notmuch_value_t; - -/* Xapian (with flint backend) complains if we provide a term longer - * than this, but I haven't yet found a way to query the limit - * programmatically. */ -#define NOTMUCH_TERM_MAX 245 - -typedef enum _notmuch_private_status { - /* First, copy all the public status values. */ - NOTMUCH_PRIVATE_STATUS_SUCCESS = NOTMUCH_STATUS_SUCCESS, - NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY = NOTMUCH_STATUS_OUT_OF_MEMORY, - NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION = NOTMUCH_STATUS_XAPIAN_EXCEPTION, - NOTMUCH_PRIVATE_STATUS_FILE_NOT_EMAIL = NOTMUCH_STATUS_FILE_NOT_EMAIL, - NOTMUCH_PRIVATE_STATUS_NULL_POINTER = NOTMUCH_STATUS_NULL_POINTER, - NOTMUCH_PRIVATE_STATUS_TAG_TOO_LONG = NOTMUCH_STATUS_TAG_TOO_LONG, - - /* Then add our own private values. */ - NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG, - NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND, - - NOTMUCH_PRIVATE_STATUS_LAST_STATUS -} notmuch_private_status_t; - -/* Coerce a notmuch_private_status_t value to a notmuch_status_t - * value, generating an internal error if the private value is equal - * to or greater than NOTMUCH_STATUS_LAST_STATUS. (The idea here is - * that the caller has previously handled any expected - * notmuch_private_status_t values.) - */ -#define COERCE_STATUS(private_status, format, ...) \ - ((private_status >= (notmuch_private_status_t) NOTMUCH_STATUS_LAST_STATUS)\ - ? \ - (notmuch_status_t) _internal_error (format " (%s).\n", \ - ##__VA_ARGS__, \ - __location__) \ - : \ - (notmuch_status_t) private_status) - -/* database.cc */ - -/* Lookup a prefix value by name. - * - * XXX: This should really be static inside of message.cc, and we can - * do that once we convert database.cc to use the - * _notmuch_message_add/remove_term functions. */ -const char * -_find_prefix (const char *name); - -/* thread.cc */ - -notmuch_thread_t * -_notmuch_thread_create (const void *talloc_owner, - notmuch_database_t *notmuch, - const char *thread_id); - -void -_notmuch_thread_add_message (notmuch_thread_t *thread, - notmuch_message_t *message); - -/* message.cc */ - -notmuch_message_t * -_notmuch_message_create (const void *talloc_owner, - notmuch_database_t *notmuch, - unsigned int doc_id, - notmuch_private_status_t *status); - -notmuch_message_t * -_notmuch_message_create_for_message_id (const void *talloc_owner, - notmuch_database_t *notmuch, - const char *message_id, - notmuch_private_status_t *status); - -const char * -_notmuch_message_get_subject (notmuch_message_t *message); - -notmuch_private_status_t -_notmuch_message_add_term (notmuch_message_t *message, - const char *prefix_name, - const char *value); - -notmuch_private_status_t -_notmuch_message_remove_term (notmuch_message_t *message, - const char *prefix_name, - const char *value); - -notmuch_private_status_t -_notmuch_message_gen_terms (notmuch_message_t *message, - const char *prefix_name, - const char *text); - -void -_notmuch_message_set_filename (notmuch_message_t *message, - const char *filename); - -void -_notmuch_message_add_thread_id (notmuch_message_t *message, - const char *thread_id); - -void -_notmuch_message_ensure_thread_id (notmuch_message_t *message); - -void -_notmuch_message_set_date (notmuch_message_t *message, - const char *date); - -void -_notmuch_message_sync (notmuch_message_t *message); - -/* index.cc */ - -notmuch_status_t -_notmuch_message_index_file (notmuch_message_t *message, - const char *filename); - -/* message-file.c */ - -/* XXX: I haven't decided yet whether these will actually get exported - * into the public interface in notmuch.h - */ - -typedef struct _notmuch_message_file notmuch_message_file_t; - -/* Open a file containing a single email message. - * - * The caller should call notmuch_message_close when done with this. - * - * Returns NULL if any error occurs. - */ -notmuch_message_file_t * -notmuch_message_file_open (const char *filename); - -/* Like notmuch_message_file_open but with 'ctx' as the talloc owner. */ -notmuch_message_file_t * -_notmuch_message_file_open_ctx (void *ctx, const char *filename); - -/* Close a notmuch message preivously opened with notmuch_message_open. */ -void -notmuch_message_file_close (notmuch_message_file_t *message); - -/* Restrict 'message' to only save the named headers. - * - * When the caller is only interested in a short list of headers, - * known in advance, calling this function can avoid wasted time and - * memory parsing/saving header values that will never be needed. - * - * The variable arguments should be a list of const char * with a - * final '(const char *) NULL' to terminate the list. - * - * If this function is called, it must be called before any calls to - * notmuch_message_get_header for this message. - * - * After calling this function, if notmuch_message_get_header is - * called with a header name not in this list, then NULL will be - * returned even if that header exists in the actual message. - */ -void -notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...); - -/* Identical to notmuch_message_restrict_headers but accepting a va_list. */ -void -notmuch_message_file_restrict_headersv (notmuch_message_file_t *message, - va_list va_headers); - -/* Get the value of the specified header from the message. - * - * The header name is case insensitive. - * - * The returned value is owned by the notmuch message and is valid - * only until the message is closed. The caller should copy it if - * needing to modify the value or to hold onto it for longer. - * - * Returns NULL if the message does not contain a header line matching - * 'header'. - */ -const char * -notmuch_message_file_get_header (notmuch_message_file_t *message, - const char *header); - -/* date.c */ - -/* Parse an RFC 8222 date string to a time_t value. - * - * The tz_offset argument can be used to also obtain the time-zone - * offset, (but can be NULL if the call is not interested in that). - * - * Returns 0 on error. - */ -time_t -notmuch_parse_date (const char *str, int *tz_offset); - -/* sha1.c */ - -char * -notmuch_sha1_of_string (const char *str); - -char * -notmuch_sha1_of_file (const char *filename); - -/* tags.c */ - -notmuch_tags_t * -_notmuch_tags_create (void *ctx); - -void -_notmuch_tags_add_tag (notmuch_tags_t *tags, const char *tag); - -void -_notmuch_tags_prepare_iterator (notmuch_tags_t *tags); - -NOTMUCH_END_DECLS - -#endif diff --git a/notmuch.h b/notmuch.h deleted file mode 100644 index bab573dd..00000000 --- a/notmuch.h +++ /dev/null @@ -1,834 +0,0 @@ -/* notmuch - Not much of an email library, (just index and search) - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#ifndef NOTMUCH_H -#define NOTMUCH_H - -#ifdef __cplusplus -# define NOTMUCH_BEGIN_DECLS extern "C" { -# define NOTMUCH_END_DECLS } -#else -# define NOTMUCH_BEGIN_DECLS -# define NOTMUCH_END_DECLS -#endif - -NOTMUCH_BEGIN_DECLS - -#include - -#ifndef FALSE -#define FALSE 0 -#endif - -#ifndef TRUE -#define TRUE 1 -#endif - -typedef int notmuch_bool_t; - -/* Status codes used for the return values of most functions. - * - * A zero value (NOTMUCH_STATUS_SUCCESS) indicates that the function - * completed without error. Any other value indicates an error as - * follows: - * - * NOTMUCH_STATUS_SUCCESS: No error occurred. - * - * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory - * - * XXX: We don't really want to expose this lame XAPIAN_EXCEPTION - * value. Instead we should map to things like DATABASE_LOCKED or - * whatever. - * - * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred - * - * NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to read or - * write to a file (this could be file not found, permission - * denied, etc.) - * - * NOTMUCH_STATUS_FILE_NOT_EMAIL: A file was presented that doesn't - * appear to be an email message. - * - * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: A file contains a message ID - * that is identical to a message already in the database. - * - * NOTMUCH_STATUS_NULL_POINTER: The user erroneously passed a NULL - * pointer to a notmuch function. - * - * NOTMUCH_STATUS_TAG_TOO_LONG: A tag value is too long (exceeds - * NOTMUCH_TAG_MAX) - * - * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: The notmuch_message_thaw - * function has been called more times than notmuch_message_freeze. - * - * And finally: - * - * NOTMUCH_STATUS_LAST_STATUS: Not an actual status value. Just a way - * to find out how many valid status values there are. - */ -typedef enum _notmuch_status { - NOTMUCH_STATUS_SUCCESS = 0, - NOTMUCH_STATUS_OUT_OF_MEMORY, - NOTMUCH_STATUS_XAPIAN_EXCEPTION, - NOTMUCH_STATUS_FILE_ERROR, - NOTMUCH_STATUS_FILE_NOT_EMAIL, - NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID, - NOTMUCH_STATUS_NULL_POINTER, - NOTMUCH_STATUS_TAG_TOO_LONG, - NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW, - - NOTMUCH_STATUS_LAST_STATUS -} notmuch_status_t; - -/* Get a string representation of a notmuch_status_t value. - * - * The result is readonly. - */ -const char * -notmuch_status_to_string (notmuch_status_t status); - -/* Various opaque data types. For each notmuch__t see the various - * notmuch_ functions below. */ -typedef struct _notmuch_database notmuch_database_t; -typedef struct _notmuch_query notmuch_query_t; -typedef struct _notmuch_threads notmuch_threads_t; -typedef struct _notmuch_thread notmuch_thread_t; -typedef struct _notmuch_messages notmuch_messages_t; -typedef struct _notmuch_message notmuch_message_t; -typedef struct _notmuch_tags notmuch_tags_t; - -/* Lookup the default database path. - * - * This is the path that will be used by notmuch_database_create and - * notmuch_database_open if given a NULL path. Specifically it will be - * the value of the NOTMUCH_BASE environment variable if set, - * otherwise ${HOME}/mail - * - * Returns a newly allocated string which the caller should free() - * when finished with it. - */ -char * -notmuch_database_default_path (void); - -/* Create a new, empty notmuch database located at 'path'. - * - * The path should be a top-level directory to a collection of - * plain-text email messages (one message per file). This call will - * create a new ".notmuch" directory within 'path' where notmuch will - * store its data. - * - * Passing a value of NULL for 'path' will cause notmuch to open the - * default database. The default database path can be specified by the - * NOTMUCH_BASE environment variable, and is equivalent to - * ${HOME}/mail if NOTMUCH_BASE is not set. - * - * After a successful call to notmuch_database_create, the returned - * database will be open so the caller should call - * notmuch_database_close when finished with it. - * - * The database will not yet have any data in it - * (notmuch_database_create itself is a very cheap function). Messages - * contained within 'path' can be added to the database by calling - * notmuch_database_add_message. - * - * In case of any failure, this function returns NULL, (after printing - * an error message on stderr). - */ -notmuch_database_t * -notmuch_database_create (const char *path); - -/* XXX: I think I'd like this to take an extra argument of - * notmuch_status_t* for returning a status value on failure. */ - -/* Open a 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'. - * - * An existing notmuch database can be identified by the presence of a - * directory named ".notmuch" below 'path'. - * - * Passing a value of NULL for 'path' will cause notmuch to open the - * default database. The default database path can be specified by the - * NOTMUCH_BASE environment variable, and is equivalent to - * ${HOME}/mail if NOTMUCH_BASE is not set. - * - * The caller should call notmuch_database_close when finished with - * this database. - * - * In case of any failure, this function returns NULL, (after printing - * an error message on stderr). - */ -notmuch_database_t * -notmuch_database_open (const char *path); - -/* Close the given notmuch database, freeing all associated - * resources. See notmuch_database_open. */ -void -notmuch_database_close (notmuch_database_t *database); - -/* Return the database path of the given database. - * - * The return value is a string owned by notmuch so should not be - * modified nor freed by the caller. */ -const char * -notmuch_database_get_path (notmuch_database_t *database); - -/* Store a timestamp within the database. - * - * The Notmuch database will not interpret this key nor the timestamp - * values at all. It will merely store them together and return the - * timestamp when notmuch_database_get_timestamp is called with the - * same value for 'key'. - * - * The intention is for the caller to use the timestamp to allow - * efficient identification of new messages to be added to the - * database. The recommended usage is as follows: - * - * o Read the mtime of a directory from the filesystem - * - * o Call add_message for all mail files in the directory - * - * o Call notmuch_database_set_timestamp with the path of the - * directory as 'key' and the originally read mtime as 'value'. - * - * Then, when wanting to check for updates to the directory in the - * future, the client can call notmuch_database_get_timestamp and know - * that it only needs to add files if the mtime of the directory and - * files are newer than the stored timestamp. - * - * Note: The notmuch_database_get_timestamp function does not allow - * the caller to distinguish a timestamp of 0 from a non-existent - * timestamp. So don't store a timestamp of 0 unless you are - * comfortable with that. - * - * Return value: - * - * NOTMUCH_STATUS_SUCCESS: Timestamp successfully stored in database. - * - * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception - * occurred. Timestamp not stored. - */ -notmuch_status_t -notmuch_database_set_timestamp (notmuch_database_t *database, - const char *key, time_t timestamp); - -/* Retrieve a timestamp from the database. - * - * Returns the timestamp value previously stored by calling - * notmuch_database_set_timestamp with the same value for 'key'. - * - * Returns 0 if no timestamp is stored for 'key' or if any error - * occurred querying the database. - */ -time_t -notmuch_database_get_timestamp (notmuch_database_t *database, - const char *key); - -/* Add a new message to the given notmuch database. - * - * Here,'filename' should be a path relative to the 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_DUPLICATE_MESSAGE_ID: Message has the same message - * ID as another message already in the database. Nothing added - * to the database. - * - * 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_t -notmuch_database_add_message (notmuch_database_t *database, - const char *filename, - notmuch_message_t **message); - -/* Find a message with the given messsage_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. - * - * If no message is found with the given message_id or if an - * out-of-memory situation occurs, this function returns NULL. - */ -notmuch_message_t * -notmuch_database_find_message (notmuch_database_t *database, - const char *message_id); - -/* 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 a length-zero string, (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 and - * notmuch_query_search to actually execute the query. - * - * User should call notmuch_query_destroy when finished with this - * query. - * - * Will return NULL if insufficient memory is available. - */ -notmuch_query_t * -notmuch_query_create (notmuch_database_t *database, - const char *query_string); - -/* Sort values for notmuch_query_set_sort */ -typedef enum { - NOTMUCH_SORT_DATE_OLDEST_FIRST, - NOTMUCH_SORT_DATE_NEWEST_FIRST, - NOTMUCH_SORT_MESSAGE_ID -} notmuch_sort_t; - -/* Specify the sorting desired for this query. */ -void -notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort); - -/* 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_has_more (threads); - * notmuch_threads_advance (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). - */ -notmuch_threads_t * -notmuch_query_search_threads (notmuch_query_t *query); - -/* 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_has_more (messages); - * notmuch_messages_advance (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). - */ -notmuch_messages_t * -notmuch_query_search_messages (notmuch_query_t *query); - -/* 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_thrad_t and notmuch_message_t objects generated - * from those results, etc.), if such objects haven't already been - * destroyed. - */ -void -notmuch_query_destroy (notmuch_query_t *query); - -/* Does the given notmuch_threads_t object contain any more - * results. - * - * 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. - */ -notmuch_bool_t -notmuch_threads_has_more (notmuch_threads_t *threads); - -/* Get the current thread from 'threads' as a notmuch_thread_t. - * - * Note: The returned thread belongs to 'threads' and has a lifetime - * identical to it (and the query to which it belongs). - * - * See the documentation of notmuch_query_search_threads for example - * code showing how to iterate over a notmuch_threads_t object. - * - * If an out-of-memory situation occurs, this function will return - * NULL. - */ -notmuch_thread_t * -notmuch_threads_get (notmuch_threads_t *threads); - -/* Advance the 'threads' iterator to the next thread. - * - * See the documentation of notmuch_query_search_threads for example - * code showing how to iterate over a notmuch_threads_t object. - */ -void -notmuch_threads_advance (notmuch_threads_t *threads); - -/* 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. - */ -void -notmuch_threads_destroy (notmuch_threads_t *threads); - -/* Get the thread ID of 'thread'. - * - * The returned string belongs to 'thread' and as such, should not be - * modified by the caller and will only be valid for as long as the - * thread is valid, (which is until notmuch_thread_destroy or until - * the query from which it derived is destroyed). - */ -const char * -notmuch_thread_get_thread_id (notmuch_thread_t *thread); - -/* Get the subject of 'thread' - * - * The subject is taken from the first message (according to the query - * order---see notmuch_query_set_sort) in the query results that - * belongs to this thread. - * - * The returned string belongs to 'thread' and as such, should not be - * modified by the caller and will only be valid for as long as the - * thread is valid, (which is until notmuch_thread_destroy or until - * the query from which it derived is destroyed). - */ -const char * -notmuch_thread_get_subject (notmuch_thread_t *thread); - -/* Get the date of the oldest message in 'thread' as a time_t value. - */ -time_t -notmuch_thread_get_oldest_date (notmuch_thread_t *thread); - -/* Get the date of the oldest message in 'thread' as a time_t value. - */ -time_t -notmuch_thread_get_newest_date (notmuch_thread_t *thread); - -/* Get the tags for 'thread', returning a notmuch_tags_t object which - * can be used to iterate over all tags. - * - * Note: In the Notmuch database, tags are stored on individual - * messages, not on threads. So the tags returned here will be all - * tags of the messages which matched the search and which belong to - * this thread. - * - * The tags object is owned by the thread and as such, will only be - * valid for as long as the thread is valid, (for example, until - * notmuch_thread_destroy or until the query from which it derived is - * destroyed). - * - * Typical usage might be: - * - * notmuch_thread_t *thread; - * notmuch_tags_t *tags; - * const char *tag; - * - * thread = notmuch_threads_get (threads); - * - * for (tags = notmuch_thread_get_tags (thread); - * notmuch_tags_has_more (tags); - * notmuch_result_advance (tags)) - * { - * tag = notmuch_tags_get (tags); - * .... - * } - * - * notmuch_thread_destroy (thread); - * - * 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). - */ -notmuch_tags_t * -notmuch_thread_get_tags (notmuch_thread_t *thread); - -/* Destroy a notmuch_thread_t object. */ -void -notmuch_thread_destroy (notmuch_thread_t *thread); - -/* Does the given notmuch_messages_t object contain any more - * messages. - * - * 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. - */ -notmuch_bool_t -notmuch_messages_has_more (notmuch_messages_t *messages); - -/* 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. - */ -notmuch_message_t * -notmuch_messages_get (notmuch_messages_t *messages); - -/* Advance the 'messages' iterator to the next result. - * - * See the documentation of notmuch_query_search_messages for example - * code showing how to iterate over a notmuch_messages_t object. - */ -void -notmuch_messages_advance (notmuch_messages_t *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 containg - * query object is destroyed. - */ -void -notmuch_messages_destroy (notmuch_messages_t *messages); - -/* 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). - */ -const char * -notmuch_message_get_message_id (notmuch_message_t *message); - -/* 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. - */ -const char * -notmuch_message_get_thread_id (notmuch_message_t *message); - -/* Get the 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). */ -const char * -notmuch_message_get_filename (notmuch_message_t *message); - -/* Get the date of 'message' as a time_t value. - * - * For the original textual representation of the Date header from the - * message call notmuch_message_get_header() with a header value of - * "date". */ -time_t -notmuch_message_get_date (notmuch_message_t *message); - -/* 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 NULL if the message does not contain a header line matching - * 'header' of if any error occurs. - */ -const char * -notmuch_message_get_header (notmuch_message_t *message, const char *header); - -/* 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_has_more (tags); - * notmuch_result_advance (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). - */ -notmuch_tags_t * -notmuch_message_get_tags (notmuch_message_t *message); - -/* The longest possible tag value. */ -#define NOTMUCH_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_t -notmuch_message_add_tag (notmuch_message_t *message, const char *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_t -notmuch_message_remove_tag (notmuch_message_t *message, const char *tag); - -/* Remove all tags from the given message. - * - * See notmuch_message_freeze for an example showing how to safely - * replace tag values. - */ -void -notmuch_message_remove_all_tags (notmuch_message_t *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 with - * "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 value, 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. - */ -void -notmuch_message_freeze (notmuch_message_t *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_UNBALANCE_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. - */ -notmuch_status_t -notmuch_message_thaw (notmuch_message_t *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.) - */ -void -notmuch_message_destroy (notmuch_message_t *message); - -/* Does the given notmuch_tags_t object contain any more tags. - * - * 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. - */ -notmuch_bool_t -notmuch_tags_has_more (notmuch_tags_t *tags); - -/* 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 utlimately belongs). - * - * See the documentation of notmuch_message_get_tags for example code - * showing how to iterate over a notmuch_tags_t object. - */ -const char * -notmuch_tags_get (notmuch_tags_t *tags); - -/* Advance the 'tags' iterator to the next tag. - * - * See the documentation of notmuch_message_get_tags for example code - * showing how to iterate over a notmuch_tags_t object. - */ -void -notmuch_tags_advance (notmuch_tags_t *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 containg - * message or query objects are destroyed. - */ -void -notmuch_tags_destroy (notmuch_tags_t *tags); - -NOTMUCH_END_DECLS - -#endif diff --git a/query.cc b/query.cc deleted file mode 100644 index ed576614..00000000 --- a/query.cc +++ /dev/null @@ -1,301 +0,0 @@ -/* query.cc - Support for searching a notmuch database - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#include "notmuch-private.h" -#include "database-private.h" - -#include /* GHashTable, GPtrArray */ - -#include - -struct _notmuch_query { - notmuch_database_t *notmuch; - const char *query_string; - notmuch_sort_t sort; -}; - -struct _notmuch_messages { - notmuch_database_t *notmuch; - Xapian::MSetIterator iterator; - Xapian::MSetIterator iterator_end; -}; - -struct _notmuch_threads { - notmuch_database_t *notmuch; - GPtrArray *threads; - unsigned int index; -}; - -notmuch_query_t * -notmuch_query_create (notmuch_database_t *notmuch, - const char *query_string) -{ - notmuch_query_t *query; - -#ifdef DEBUG_QUERY - fprintf (stderr, "Query string is:\n%s\n", query_string); -#endif - - query = talloc (NULL, notmuch_query_t); - if (unlikely (query == NULL)) - return NULL; - - query->notmuch = notmuch; - - query->query_string = talloc_strdup (query, query_string); - - query->sort = NOTMUCH_SORT_DATE_OLDEST_FIRST; - - return query; -} - -void -notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort) -{ - query->sort = sort; -} - -/* We end up having to call the destructors explicitly because we had - * to use "placement new" in order to initialize C++ objects within a - * block that we allocated with talloc. So C++ is making talloc - * slightly less simple to use, (we wouldn't need - * talloc_set_destructor at all otherwise). - */ -static int -_notmuch_messages_destructor (notmuch_messages_t *messages) -{ - messages->iterator.~MSetIterator (); - messages->iterator_end.~MSetIterator (); - - return 0; -} - -notmuch_messages_t * -notmuch_query_search_messages (notmuch_query_t *query) -{ - notmuch_database_t *notmuch = query->notmuch; - const char *query_string = query->query_string; - notmuch_messages_t *messages; - - messages = talloc (query, notmuch_messages_t); - if (unlikely (messages == NULL)) - return NULL; - - try { - Xapian::Enquire enquire (*notmuch->xapian_db); - Xapian::Query mail_query (talloc_asprintf (query, "%s%s", - _find_prefix ("type"), - "mail")); - Xapian::Query string_query, final_query; - Xapian::MSet mset; - unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN | - Xapian::QueryParser::FLAG_PHRASE | - Xapian::QueryParser::FLAG_LOVEHATE | - Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE | - Xapian::QueryParser::FLAG_WILDCARD); - - if (strcmp (query_string, "") == 0) { - final_query = mail_query; - } else { - string_query = notmuch->query_parser-> - parse_query (query_string, flags); - final_query = Xapian::Query (Xapian::Query::OP_AND, - mail_query, string_query); - } - - switch (query->sort) { - case NOTMUCH_SORT_DATE_OLDEST_FIRST: - enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, FALSE); - break; - case NOTMUCH_SORT_DATE_NEWEST_FIRST: - enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, TRUE); - break; - case NOTMUCH_SORT_MESSAGE_ID: - enquire.set_sort_by_value (NOTMUCH_VALUE_MESSAGE_ID, FALSE); - break; - } - -#if DEBUG_QUERY - fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str()); -#endif - - enquire.set_query (final_query); - - mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ()); - - messages->notmuch = notmuch; - - new (&messages->iterator) Xapian::MSetIterator (); - new (&messages->iterator_end) Xapian::MSetIterator (); - - talloc_set_destructor (messages, _notmuch_messages_destructor); - - messages->iterator = mset.begin (); - messages->iterator_end = mset.end (); - - } catch (const Xapian::Error &error) { - fprintf (stderr, "A Xapian exception occurred: %s\n", - error.get_msg().c_str()); - } - - return messages; -} - -/* Glib objects force use to use a talloc destructor as well, (but not - * nearly as ugly as the for messages due to C++ objects). At - * this point, I'd really like to have some talloc-friendly - * equivalents for the few pieces of glib that I'm using. */ -static int -_notmuch_threads_destructor (notmuch_threads_t *threads) -{ - g_ptr_array_free (threads->threads, TRUE); - - return 0; -} - -notmuch_threads_t * -notmuch_query_search_threads (notmuch_query_t *query) -{ - notmuch_threads_t *threads; - notmuch_thread_t *thread; - const char *thread_id; - notmuch_messages_t *messages; - notmuch_message_t *message; - GHashTable *seen; - - threads = talloc (query, notmuch_threads_t); - if (threads == NULL) - return NULL; - - threads->notmuch = query->notmuch; - threads->threads = g_ptr_array_new (); - threads->index = 0; - - talloc_set_destructor (threads, _notmuch_threads_destructor); - - seen = g_hash_table_new_full (g_str_hash, g_str_equal, - free, NULL); - - for (messages = notmuch_query_search_messages (query); - notmuch_messages_has_more (messages); - notmuch_messages_advance (messages)) - { - message = notmuch_messages_get (messages); - - thread_id = notmuch_message_get_thread_id (message); - - if (! g_hash_table_lookup_extended (seen, - thread_id, NULL, - (void **) &thread)) - { - thread = _notmuch_thread_create (query, query->notmuch, - thread_id); - - g_hash_table_insert (seen, xstrdup (thread_id), thread); - - g_ptr_array_add (threads->threads, thread); - } - - _notmuch_thread_add_message (thread, message); - - notmuch_message_destroy (message); - } - - g_hash_table_unref (seen); - - return threads; -} - -void -notmuch_query_destroy (notmuch_query_t *query) -{ - talloc_free (query); -} - -notmuch_bool_t -notmuch_messages_has_more (notmuch_messages_t *messages) -{ - return (messages->iterator != messages->iterator_end); -} - -notmuch_message_t * -notmuch_messages_get (notmuch_messages_t *messages) -{ - notmuch_message_t *message; - Xapian::docid doc_id; - notmuch_private_status_t status; - - if (! notmuch_messages_has_more (messages)) - return NULL; - - doc_id = *messages->iterator; - - message = _notmuch_message_create (messages, - messages->notmuch, doc_id, - &status); - - if (message == NULL && - status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) - { - INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n"); - } - - return message; -} - -void -notmuch_messages_advance (notmuch_messages_t *messages) -{ - messages->iterator++; -} - -void -notmuch_messages_destroy (notmuch_messages_t *messages) -{ - talloc_free (messages); -} - -notmuch_bool_t -notmuch_threads_has_more (notmuch_threads_t *threads) -{ - return (threads->index < threads->threads->len); -} - -notmuch_thread_t * -notmuch_threads_get (notmuch_threads_t *threads) -{ - if (! notmuch_threads_has_more (threads)) - return NULL; - - return (notmuch_thread_t *) g_ptr_array_index (threads->threads, - threads->index); -} - -void -notmuch_threads_advance (notmuch_threads_t *threads) -{ - threads->index++; -} - -void -notmuch_threads_destroy (notmuch_threads_t *threads) -{ - talloc_free (threads); -} diff --git a/sha1.c b/sha1.c deleted file mode 100644 index ff4dd164..00000000 --- a/sha1.c +++ /dev/null @@ -1,115 +0,0 @@ -/* sha1.c - Interfaces to SHA-1 hash for the notmuch mail system - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#include "notmuch-private.h" - -#include "libsha1.h" - -/* Just some simple interfaces on top of libsha1 so that we can leave - * libsha1 as untouched as possible. */ - -static char * -_hex_of_sha1_digest (const unsigned char digest[SHA1_DIGEST_SIZE]) -{ - char *result, *r; - int i; - - result = xcalloc (SHA1_DIGEST_SIZE * 2 + 1, 1); - - for (r = result, i = 0; - i < SHA1_DIGEST_SIZE; - r += 2, i++) - { - sprintf (r, "%02x", digest[i]); - } - - return result; -} - -/* Create a hexadcimal string version of the SHA-1 digest of 'str' - * (including its null terminating character). - * - * This function returns a newly allocated string which the caller - * should free() when finished. - */ -char * -notmuch_sha1_of_string (const char *str) -{ - sha1_ctx sha1; - unsigned char digest[SHA1_DIGEST_SIZE]; - - sha1_begin (&sha1); - - sha1_hash ((unsigned char *) str, strlen (str) + 1, &sha1); - - sha1_end (digest, &sha1); - - return _hex_of_sha1_digest (digest); -} - -/* Create a hexadecimal string version of the SHA-1 digest of the - * contents of the named file. - * - * This function returns a newly allocated string which the caller - * should free() when finished. - * - * If any error occurs while reading the file, (permission denied, - * file not found, etc.), this function returns NULL. - */ -char * -notmuch_sha1_of_file (const char *filename) -{ - FILE *file; -#define BLOCK_SIZE 4096 - unsigned char block[BLOCK_SIZE]; - size_t bytes_read; - sha1_ctx sha1; - unsigned char digest[SHA1_DIGEST_SIZE]; - char *result; - - file = fopen (filename, "r"); - if (file == NULL) - return NULL; - - sha1_begin (&sha1); - - while (1) { - bytes_read = fread (block, 1, 4096, file); - if (bytes_read == 0) { - if (feof (file)) { - break; - } else if (ferror (file)) { - fclose (file); - return NULL; - } - } else { - sha1_hash (block, bytes_read, &sha1); - } - } - - sha1_end (digest, &sha1); - - result = _hex_of_sha1_digest (digest); - - fclose (file); - - return result; -} - diff --git a/tags.c b/tags.c deleted file mode 100644 index afc132c5..00000000 --- a/tags.c +++ /dev/null @@ -1,116 +0,0 @@ -/* tags.c - Iterator for tags returned from message or thread - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#include "notmuch-private.h" - -#include /* GList */ - -struct _notmuch_tags { - int sorted; - GList *tags; - GList *iterator; -}; - -/* XXX: Should write some talloc-friendly list to avoid the need for - * this. */ -static int -_notmuch_tags_destructor (notmuch_tags_t *tags) -{ - g_list_free (tags->tags); - - return 0; -} - -/* Create a new notmuch_tags_t object, with 'ctx' as its talloc owner. - * - * This function can return NULL in case of out-of-memory. - */ -notmuch_tags_t * -_notmuch_tags_create (void *ctx) -{ - notmuch_tags_t *tags; - - tags = talloc (ctx, notmuch_tags_t); - if (unlikely (tags == NULL)) - return NULL; - - talloc_set_destructor (tags, _notmuch_tags_destructor); - - tags->sorted = 1; - tags->tags = NULL; - tags->iterator = NULL; - - return tags; -} - -/* Add a new tag to 'tags'. The tags object will create its own copy - * of the string. - * - * Note: The tags object will not do anything to prevent duplicate - * tags being stored, so the caller really shouldn't pass - * duplicates. */ -void -_notmuch_tags_add_tag (notmuch_tags_t *tags, const char *tag) -{ - tags->tags = g_list_prepend (tags->tags, talloc_strdup (tags, tag)); - tags->sorted = 0; -} - -/* Prepare 'tag' for iteration. - * - * The internal creator of 'tags' should call this function before - * returning 'tags' to the user to call the public functions such as - * notmuch_tags_has_more, notmuch_tags_get, and notmuch_tags_advance. */ -void -_notmuch_tags_prepare_iterator (notmuch_tags_t *tags) -{ - if (! tags->sorted) - tags->tags = g_list_sort (tags->tags, (GCompareFunc) strcmp); - tags->sorted = 1; - - tags->iterator = tags->tags; -} - -notmuch_bool_t -notmuch_tags_has_more (notmuch_tags_t *tags) -{ - return tags->iterator != NULL; -} - -const char * -notmuch_tags_get (notmuch_tags_t *tags) -{ - if (tags->iterator) - return (char *) tags->iterator->data; - else - return NULL; -} - -void -notmuch_tags_advance (notmuch_tags_t *tags) -{ - tags->iterator = tags->iterator->next; -} - -void -notmuch_tags_destroy (notmuch_tags_t *tags) -{ - talloc_free (tags); -} diff --git a/thread.cc b/thread.cc deleted file mode 100644 index b67dfade..00000000 --- a/thread.cc +++ /dev/null @@ -1,171 +0,0 @@ -/* thread.cc - Results of thread-based searches from a notmuch database - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#include "notmuch-private.h" -#include "database-private.h" - -#include - -#include /* GHashTable */ - -struct _notmuch_thread { - notmuch_database_t *notmuch; - char *thread_id; - char *subject; - GHashTable *tags; - - notmuch_bool_t has_message; - time_t oldest; - time_t newest; -}; - -static int -_notmuch_thread_destructor (notmuch_thread_t *thread) -{ - g_hash_table_unref (thread->tags); - - return 0; -} - -/* Create a new notmuch_thread_t object for an existing document in - * the database. - * - * Here, 'talloc owner' is an optional talloc context to which the new - * thread will belong. This allows for the caller to not bother - * calling notmuch_thread_destroy on the thread, and know that all - * memory will be reclaimed with 'talloc_owner' is freed. The caller - * still can call notmuch_thread_destroy when finished with the - * thread if desired. - * - * The 'talloc_owner' argument can also be NULL, in which case the - * caller *is* responsible for calling notmuch_thread_destroy. - * - * This function returns NULL in the case of any error. - */ -notmuch_thread_t * -_notmuch_thread_create (const void *talloc_owner, - notmuch_database_t *notmuch, - const char *thread_id) -{ - notmuch_thread_t *thread; - - thread = talloc (talloc_owner, notmuch_thread_t); - if (unlikely (thread == NULL)) - return NULL; - - talloc_set_destructor (thread, _notmuch_thread_destructor); - - thread->notmuch = notmuch; - thread->thread_id = talloc_strdup (thread, thread_id); - thread->subject = NULL; - thread->tags = g_hash_table_new_full (g_str_hash, g_str_equal, - free, NULL); - - thread->has_message = 0; - thread->oldest = 0; - thread->newest = 0; - - return thread; -} - -const char * -notmuch_thread_get_thread_id (notmuch_thread_t *thread) -{ - return thread->thread_id; -} - -void -_notmuch_thread_add_message (notmuch_thread_t *thread, - notmuch_message_t *message) -{ - notmuch_tags_t *tags; - const char *tag; - time_t date; - - if (! thread->subject) { - const char *subject; - subject = notmuch_message_get_header (message, "subject"); - thread->subject = talloc_strdup (thread, subject); - } - - for (tags = notmuch_message_get_tags (message); - notmuch_tags_has_more (tags); - notmuch_tags_advance (tags)) - { - tag = notmuch_tags_get (tags); - g_hash_table_insert (thread->tags, xstrdup (tag), NULL); - } - - date = notmuch_message_get_date (message); - - if (date < thread->oldest || ! thread->has_message) - thread->oldest = date; - - if (date > thread->newest || ! thread->has_message) - thread->newest = date; - - thread->has_message = 1; -} - -const char * -notmuch_thread_get_subject (notmuch_thread_t *thread) -{ - return thread->subject; -} - -time_t -notmuch_thread_get_oldest_date (notmuch_thread_t *thread) -{ - return thread->oldest; -} - -time_t -notmuch_thread_get_newest_date (notmuch_thread_t *thread) -{ - return thread->newest; -} - -notmuch_tags_t * -notmuch_thread_get_tags (notmuch_thread_t *thread) -{ - notmuch_tags_t *tags; - GList *keys, *l; - - tags = _notmuch_tags_create (thread); - if (unlikely (tags == NULL)) - return NULL; - - keys = g_hash_table_get_keys (thread->tags); - - for (l = keys; l; l = l->next) - _notmuch_tags_add_tag (tags, (char *) l->data); - - g_list_free (keys); - - _notmuch_tags_prepare_iterator (tags); - - return tags; -} - -void -notmuch_thread_destroy (notmuch_thread_t *thread) -{ - talloc_free (thread); -} diff --git a/xutil.c b/xutil.c deleted file mode 100644 index 6fa5eb0d..00000000 --- a/xutil.c +++ /dev/null @@ -1,130 +0,0 @@ -/* xutil.c - Various wrapper functions to abort on error. - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#define _GNU_SOURCE /* For strndup */ -#include "notmuch-private.h" - -#include - -void * -xcalloc (size_t nmemb, size_t size) -{ - void *ret; - - ret = calloc (nmemb, size); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -void * -xmalloc (size_t size) -{ - void *ret; - - ret = malloc (size); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -void * -xrealloc (void *ptr, size_t size) -{ - void *ret; - - ret = realloc (ptr, size); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -char * -xstrdup (const char *s) -{ - char *ret; - - ret = strdup (s); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -char * -xstrndup (const char *s, size_t n) -{ - char *ret; - - ret = strndup (s, n); - if (ret == NULL) { - fprintf (stderr, "Out of memory.\n"); - exit (1); - } - - return ret; -} - -void -xregcomp (regex_t *preg, const char *regex, int cflags) -{ - int rerr; - - rerr = regcomp (preg, regex, cflags); - if (rerr) { - size_t error_size = regerror (rerr, preg, NULL, 0); - char *error = xmalloc (error_size); - - regerror (rerr, preg, error, error_size); - INTERNAL_ERROR ("compiling regex %s: %s\n", - regex, error); - } -} - -int -xregexec (const regex_t *preg, const char *string, - size_t nmatch, regmatch_t pmatch[], int eflags) -{ - unsigned int i; - int rerr; - - rerr = regexec (preg, string, nmatch, pmatch, eflags); - if (rerr) - return rerr; - - for (i = 0; i < nmatch; i++) { - if (pmatch[i].rm_so == -1) - INTERNAL_ERROR ("matching regex against %s: Sub-match %d not found\n", - string, i); - } - - return 0; -} diff --git a/xutil.h b/xutil.h deleted file mode 100644 index b973f7dc..00000000 --- a/xutil.h +++ /dev/null @@ -1,51 +0,0 @@ -/* xutil.h - Various wrapper functions to abort on error. - * - * Copyright © 2009 Carl Worth - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/ . - * - * Author: Carl Worth - */ - -#ifndef NOTMUCH_XUTIL_H -#define NOTMUCH_XUTIL_H - -#include -#include -#include - -/* xutil.c */ -void * -xcalloc (size_t nmemb, size_t size); - -void * -xmalloc (size_t size); - -void * -xrealloc (void *ptrr, size_t size); - -char * -xstrdup (const char *s); - -char * -xstrndup (const char *s, size_t n); - -void -xregcomp (regex_t *preg, const char *regex, int cflags); - -int -xregexec (const regex_t *preg, const char *string, - size_t nmatch, regmatch_t pmatch[], int eflags); - -#endif