+/* Construct a single query string from the passed arguments, using
+ * 'ctx' as the talloc owner for all allocations.
+ *
+ * Currently, the arguments are just connected with space characters,
+ * but we might do more processing in the future, (such as inserting
+ * any AND operators needed to work around Xapian QueryParser bugs).
+ *
+ * This function returns NULL in case of insufficient memory.
+ */
+static char *
+query_string_from_args (void *ctx, int argc, char *argv[])
+{
+ char *query_string;
+ int i;
+
+ query_string = talloc_strdup (ctx, "");
+ if (query_string == NULL)
+ return NULL;
+
+ for (i = 0; i < argc; i++) {
+ if (i != 0) {
+ query_string = talloc_strdup_append (query_string, " ");
+ if (query_string == NULL)
+ return NULL;
+ }
+
+ query_string = talloc_strdup_append (query_string, argv[i]);
+ if (query_string == NULL)
+ return NULL;
+ }
+
+ return query_string;
+}
+
+/* Format a nice representation of 'time' relative to the current time.
+ *
+ * Examples include:
+ *
+ * 5 minutes ago (For times less than 60 minutes ago)
+ * 12:30 (For times >60 minutes but still today)
+ * Yesterday
+ * Monday (Before yesterday but fewer than 7 days ago)
+ * Oct. 12 (Between 7 and 180 days ago (about 6 months))
+ * 2008-06-30 (More than 180 days ago)
+ */
+#define MINUTE (60)
+#define HOUR (60 * MINUTE)
+#define DAY (24 * HOUR)
+#define RELATIVE_DATE_MAX 20
+static const char *
+_format_relative_date (void *ctx, time_t then)
+{
+ struct tm tm_now, tm_then;
+ time_t now = time(NULL);
+ time_t delta;
+ char *result;
+
+ localtime_r (&now, &tm_now);
+ localtime_r (&then, &tm_then);
+
+ result = talloc_zero_size (ctx, RELATIVE_DATE_MAX);
+ if (result == NULL)
+ return "when?";
+
+ if (then > now)
+ return "the future";
+
+ delta = now - then;
+
+ if (delta > 180 * DAY) {
+ strftime (result, RELATIVE_DATE_MAX,
+ "%F", &tm_then); /* 2008-06-30 */
+ return result;
+ }
+
+ if (delta < 3600) {
+ snprintf (result, RELATIVE_DATE_MAX,
+ "%d minutes ago", (int) (delta / 60));
+ return result;
+ }
+
+ if (delta <= 7 * DAY) {
+ if (tm_then.tm_wday == tm_now.tm_wday &&
+ delta < DAY)
+ {
+ strftime (result, RELATIVE_DATE_MAX,
+ "%R", &tm_then); /* 12:30 */
+ return result;
+ } else if ((tm_now.tm_wday + 7 - tm_then.tm_wday) % 7 == 1) {
+ return "Yesterday";
+ } else {
+ if (tm_then.tm_wday != tm_now.tm_wday) {
+ strftime (result, RELATIVE_DATE_MAX,
+ "%A", &tm_then); /* Monday */
+ return result;
+ }
+ }
+ }
+
+ strftime (result, RELATIVE_DATE_MAX,
+ "%b %d", &tm_then); /* Oct. 12 */
+ return result;
+}
+#undef MINUTE
+#undef HOUR
+#undef DAY
+
+static int
+search_command (void *ctx, int argc, char *argv[])
+{
+ void *local = talloc_new (ctx);
+ notmuch_database_t *notmuch = NULL;
+ notmuch_query_t *query;
+ notmuch_threads_t *threads;
+ notmuch_thread_t *thread;
+ notmuch_tags_t *tags;
+ char *query_str;
+ const char *relative_date;
+ time_t date;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+ notmuch = notmuch_database_open (NULL);
+ if (notmuch == NULL) {
+ ret = 1;
+ goto DONE;
+ }
+
+ query_str = query_string_from_args (local, argc, argv);
+
+ query = notmuch_query_create (notmuch, query_str);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ ret = 1;
+ goto DONE;
+ }
+
+ for (threads = notmuch_query_search_threads (query);
+ notmuch_threads_has_more (threads);
+ notmuch_threads_advance (threads))
+ {
+ int first = 1;
+
+ thread = notmuch_threads_get (threads);
+
+ date = notmuch_thread_get_oldest_date (thread);
+ relative_date = _format_relative_date (local, date);
+
+ printf ("%s (%s) %s",
+ notmuch_thread_get_thread_id (thread),
+ relative_date,
+ notmuch_thread_get_subject (thread));
+
+ printf (" (");
+ for (tags = notmuch_thread_get_tags (thread);
+ notmuch_tags_has_more (tags);
+ notmuch_tags_advance (tags))
+ {
+ if (! first)
+ printf (" ");
+ printf ("%s", notmuch_tags_get (tags));
+ first = 0;
+ }
+ printf (")\n");
+
+ notmuch_thread_destroy (thread);
+ }
+
+ notmuch_query_destroy (query);
+
+ DONE:
+ if (notmuch)
+ notmuch_database_close (notmuch);
+ talloc_free (local);
+
+ return ret;
+}
+
+/* Get a nice, single-line summary of message. */
+static const char *
+_get_one_line_summary (void *ctx, notmuch_message_t *message)
+{
+ const char *from;
+ time_t date;
+ const char *relative_date;
+ const char *subject;
+
+ from = notmuch_message_get_header (message, "from");
+
+ date = notmuch_message_get_date (message);
+ relative_date = _format_relative_date (ctx, date);
+
+ subject = notmuch_message_get_header (message, "subject");
+
+ return talloc_asprintf (ctx, "%s (%s) %s",
+ from, relative_date, subject);
+}
+
+static void
+show_message_part (GMimeObject *part)
+{
+ GMimeStream *stream;
+ GMimeDataWrapper *wrapper;
+ GMimeContentDisposition *disposition;
+ GMimeContentType *content_type;
+
+ 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. Continuing.\n");
+ }
+ show_message_part (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));
+
+ show_message_part (g_mime_message_get_mime_part (mime_message));
+
+ return;
+ }
+
+ if (! (GMIME_IS_PART (part))) {
+ fprintf (stderr, "Warning: Not displaying 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));
+ content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+
+ printf ("%%attachment{ Content-type: %s\n",
+ g_mime_content_type_to_string (content_type));
+ printf ("%s\n", filename);
+ printf ("%%attachment}\n");
+
+ return;
+ }
+
+ /* Stream the MIME part out to stdout. */
+ content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+
+ printf ("%%part{ Content-type: %s\n",
+ g_mime_content_type_to_string (content_type));
+
+ stream = g_mime_stream_file_new (stdout);
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream), FALSE);
+
+ wrapper = g_mime_part_get_content_object (GMIME_PART (part));
+ if (wrapper)
+ g_mime_data_wrapper_write_to_stream (wrapper, stream);
+
+ printf ("%%part}\n");
+
+ g_object_unref (stream);
+}
+
+static notmuch_status_t
+show_message_body (const char *filename)
+{
+ GMimeStream *stream = NULL;
+ GMimeParser *parser = NULL;
+ GMimeMessage *mime_message = NULL;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+ static int initialized = 0;
+ FILE *file = NULL;
+
+ 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;
+ }
+
+ stream = g_mime_stream_file_new (file);
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream), FALSE);
+
+ parser = g_mime_parser_new_with_stream (stream);
+
+ mime_message = g_mime_parser_construct_message (parser);
+
+ show_message_part (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);
+
+ if (file)
+ fclose (file);
+
+ return ret;
+}
+
+static int
+show_command (void *ctx, unused (int argc), unused (char *argv[]))
+{
+ void *local = talloc_new (ctx);
+ char *query_string;
+ notmuch_database_t *notmuch = NULL;
+ notmuch_query_t *query = NULL;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ int ret = 0;
+
+ const char *headers[] = {
+ "Subject", "From", "To", "Cc", "Bcc", "Date"
+ };
+ const char *name, *value;
+ unsigned int i;
+
+ if (argc != 1) {
+ fprintf (stderr, "Error: \"notmuch show\" requires exactly one thread-ID argument.\n");
+ ret = 1;
+ goto DONE;
+ }
+
+ notmuch = notmuch_database_open (NULL);
+ if (notmuch == NULL) {
+ ret = 1;
+ goto DONE;
+ }
+
+ query_string = talloc_asprintf (local, "thread:%s", argv[0]);
+ if (query_string == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ ret = 1;
+ goto DONE;
+ }
+
+ query = notmuch_query_create (notmuch, query_string);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ ret = 1;
+ goto DONE;
+ }
+
+ for (messages = notmuch_query_search_messages (query);
+ notmuch_messages_has_more (messages);
+ notmuch_messages_advance (messages))
+ {
+ message = notmuch_messages_get (messages);
+
+ printf ("%%message{\n");
+
+ printf ("%%header{\n");
+
+ printf ("%s\n", _get_one_line_summary (local, message));
+
+ for (i = 0; i < ARRAY_SIZE (headers); i++) {
+ name = headers[i];
+ value = notmuch_message_get_header (message, name);
+ if (value)
+ printf ("%s: %s\n", name, value);
+ }
+
+ printf ("%%header}\n");
+
+ show_message_body (notmuch_message_get_filename (message));
+
+ printf ("%%message}\n");
+
+ notmuch_message_destroy (message);
+ }
+
+ DONE:
+ if (local)
+ talloc_free (local);
+
+ if (query)
+ notmuch_query_destroy (query);
+
+ if (notmuch)
+ notmuch_database_close (notmuch);
+
+ return ret;
+}
+
+static int
+tag_command (void *ctx, unused (int argc), unused (char *argv[]))
+{
+ void *local;
+ int *add_tags, *remove_tags;
+ int add_tags_count = 0;
+ int remove_tags_count = 0;
+ char *query_string;
+ notmuch_database_t *notmuch = NULL;
+ notmuch_query_t *query;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ int ret = 0;
+ int i;
+
+ local = talloc_new (ctx);
+ if (local == NULL) {
+ ret = 1;
+ goto DONE;
+ }
+
+ add_tags = talloc_size (local, argc * sizeof (int));
+ if (add_tags == NULL) {
+ ret = 1;
+ goto DONE;
+ }
+
+ remove_tags = talloc_size (local, argc * sizeof (int));
+ if (remove_tags == NULL) {
+ ret = 1;
+ goto DONE;
+ }
+
+ for (i = 0; i < argc; i++) {
+ if (strcmp (argv[i], "--") == 0) {
+ i++;
+ break;
+ }
+ if (argv[i][0] == '+') {
+ add_tags[add_tags_count++] = i;
+ } else if (argv[i][0] == '-') {
+ remove_tags[remove_tags_count++] = i;
+ } else {
+ break;
+ }
+ }
+
+ if (add_tags_count == 0 && remove_tags_count == 0) {
+ fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
+ ret = 1;
+ goto DONE;
+ }
+
+ if (i == argc) {
+ fprintf (stderr, "Error: 'notmuch tag' requires at least one search term.\n");
+ ret = 1;
+ goto DONE;
+ }
+
+ notmuch = notmuch_database_open (NULL);
+ if (notmuch == NULL) {
+ ret = 1;
+ goto DONE;
+ }
+
+ query_string = query_string_from_args (local, argc - i, &argv[i]);
+
+ query = notmuch_query_create (notmuch, query_string);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ ret = 1;
+ goto DONE;
+ }
+
+ for (messages = notmuch_query_search_messages (query);
+ notmuch_messages_has_more (messages);
+ notmuch_messages_advance (messages))
+ {
+ message = notmuch_messages_get (messages);
+
+ notmuch_message_freeze (message);
+
+ for (i = 0; i < remove_tags_count; i++)
+ notmuch_message_remove_tag (message,
+ argv[remove_tags[i]] + 1);
+
+ for (i = 0; i < add_tags_count; i++)
+ notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
+
+ notmuch_message_thaw (message);
+
+ notmuch_message_destroy (message);
+ }
+
+ notmuch_query_destroy (query);
+
+ DONE:
+ if (notmuch)
+ notmuch_database_close (notmuch);
+
+ talloc_free (local);
+
+ return ret;
+}
+
+static int
+dump_command (unused (void *ctx), int argc, char *argv[])