]> git.notmuchmail.org Git - notmuch/blobdiff - notmuch.c
libify: Move library sources down into lib directory.
[notmuch] / notmuch.c
index 4ac5531ae42a14c8216e3977cfc7dc8a495fbb29..923a7cee53eb366cd91ee822ffb8c7678e157621 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -23,6 +23,8 @@
 #endif
 #include <stdio.h>
 
+#include <gmime/gmime.h>
+
 #include "notmuch.h"
 
 /* This is separate from notmuch-private.h because we're trying to
@@ -60,7 +62,7 @@
 
 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
 
-typedef int (*command_function_t) (int argc, char *argv[]);
+typedef int (*command_function_t) (void *ctx, int argc, char *argv[]);
 
 typedef struct command {
     const char *name;
@@ -471,7 +473,7 @@ count_files (const char *path, int *count)
 }
 
 static int
-setup_command (unused (int argc), unused (char *argv[]))
+setup_command (unused (void *ctx), unused (int argc), unused (char *argv[]))
 {
     notmuch_database_t *notmuch = NULL;
     char *default_path, *mail_directory = NULL;
@@ -531,7 +533,7 @@ setup_command (unused (int argc), unused (char *argv[]))
        free (default_path);
     }
 
-    /* Coerce th directory into an absolute directory name. */
+    /* Coerce the directory into an absolute directory name. */
     if (*mail_directory != '/') {
        char *cwd, *absolute_mail_directory;
 
@@ -551,8 +553,6 @@ setup_command (unused (int argc), unused (char *argv[]))
        free (cwd);
        free (mail_directory);
        mail_directory = absolute_mail_directory;
-
-       printf ("Abs: %s\n", mail_directory);
     }
 
     notmuch = notmuch_database_create (mail_directory);
@@ -629,7 +629,7 @@ tag_inbox_and_unread (notmuch_message_t *message)
 }
 
 static int
-new_command (unused (int argc), unused (char *argv[]))
+new_command (unused (void *ctx), unused (int argc), unused (char *argv[]))
 {
     notmuch_database_t *notmuch;
     const char *mail_directory;
@@ -733,16 +733,93 @@ query_string_from_args (void *ctx, int argc, char *argv[])
     return query_string;
 }
 
+/* Format a nice representation of 'time' relative to the current time.
+ *
+ * Examples include:
+ *
+ *     5 mins. ago     (For times less than 60 minutes ago)
+ *     Today 12:30     (For times >60 minutes but still today)
+ *     Yest. 12:30
+ *     Mon.  12:30     (Before yesterday but fewer than 7 days ago)
+ *     October 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 mins. 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,
+                     "Today %R", &tm_then); /* Today 12:30 */
+           return result;
+       } else if ((tm_now.tm_wday + 7 - tm_then.tm_wday) % 7 == 1) {
+           strftime (result, RELATIVE_DATE_MAX,
+                     "Yest. %R", &tm_then); /* Yest. 12:30 */
+           return result;
+       } else {
+           if (tm_then.tm_wday != tm_now.tm_wday) {
+               strftime (result, RELATIVE_DATE_MAX,
+                         "%a. %R", &tm_then); /* Mon. 12:30 */
+               return result;
+           }
+       }
+    }
+
+    strftime (result, RELATIVE_DATE_MAX,
+             "%B %d", &tm_then); /* October 12 */
+    return result;
+}
+#undef MINUTE
+#undef HOUR
+#undef DAY
+
 static int
-search_command (int argc, char *argv[])
+search_command (void *ctx, int argc, char *argv[])
 {
-    void *local = talloc_new (NULL);
+    void *local = talloc_new (ctx);
     notmuch_database_t *notmuch = NULL;
     notmuch_query_t *query;
-    notmuch_thread_results_t *results;
+    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);
@@ -760,16 +837,20 @@ search_command (int argc, char *argv[])
        goto DONE;
     }
 
-    for (results = notmuch_query_search_threads (query);
-        notmuch_thread_results_has_more (results);
-        notmuch_thread_results_advance (results))
+    for (threads = notmuch_query_search_threads (query);
+        notmuch_threads_has_more (threads);
+        notmuch_threads_advance (threads))
     {
        int first = 1;
 
-       thread = notmuch_thread_results_get (results);
+       thread = notmuch_threads_get (threads);
+
+       date = notmuch_thread_get_oldest_date (thread);
+       relative_date = _format_relative_date (local, date);
 
-       printf ("%s %s",
+       printf ("thread:%s %12s %s",
                notmuch_thread_get_thread_id (thread),
+               relative_date,
                notmuch_thread_get_subject (thread));
 
        printf (" (");
@@ -797,39 +878,214 @@ search_command (int argc, char *argv[])
     return ret;
 }
 
+static const char *
+_get_tags_as_string (void *ctx, notmuch_message_t *message)
+{
+    notmuch_tags_t *tags;
+    int first = 1;
+    const char *tag;
+    char *result;
+
+    result = talloc_strdup (ctx, "");
+    if (result == NULL)
+       return NULL;
+
+    for (tags = notmuch_message_get_tags (message);
+        notmuch_tags_has_more (tags);
+        notmuch_tags_advance (tags))
+    {
+       tag = notmuch_tags_get (tags);
+
+       result = talloc_asprintf_append (result, "%s%s",
+                                        first ? "" : " ", tag);
+       first = 0;
+    }
+
+    return result;
+}
+
+/* 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 *tags;
+
+    from = notmuch_message_get_header (message, "from");
+
+    date = notmuch_message_get_date (message);
+    relative_date = _format_relative_date (ctx, date);
+
+    tags = _get_tags_as_string (ctx, message);
+
+    return talloc_asprintf (ctx, "%s (%s) (%s)",
+                           from, relative_date, tags);
+}
+
+static void
+show_message_part (GMimeObject *part, int *part_count)
+{
+    GMimeStream *stream;
+    GMimeDataWrapper *wrapper;
+    GMimeContentDisposition *disposition;
+    GMimeContentType *content_type;
+
+    *part_count = *part_count + 1;
+
+    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),
+                              part_count);
+       }
+       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),
+                          part_count);
+
+       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 ("\fattachment{ ID: %d, Content-type: %s\n",
+               *part_count,
+               g_mime_content_type_to_string (content_type));
+       printf ("Attachment: %s (%s)\n", filename,
+               g_mime_content_type_to_string (content_type));
+       printf ("\fattachment}\n");
+
+       return;
+    }
+
+    content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+
+    printf ("\fpart{ ID: %d, Content-type: %s\n",
+           *part_count,
+           g_mime_content_type_to_string (content_type));
+
+    if (g_mime_content_type_is_type (content_type, "text", "*") &&
+       !g_mime_content_type_is_type (content_type, "text", "html"))
+    {
+       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);
+
+       g_object_unref (stream);
+    }
+    else
+    {
+       printf ("Non-text part: %s\n",
+               g_mime_content_type_to_string (content_type));
+    }
+
+    printf ("\fpart}\n");
+}
+
+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;
+    int part_count = 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;
+    }
+
+    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),
+                      &part_count);
+
+  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 (unused (int argc), unused (char *argv[]))
+show_command (void *ctx, unused (int argc), unused (char *argv[]))
 {
-    void *local = talloc_new (NULL);
+    void *local = talloc_new (ctx);
     char *query_string;
     notmuch_database_t *notmuch = NULL;
     notmuch_query_t *query = NULL;
-    notmuch_message_results_t *messages;
+    notmuch_messages_t *messages;
     notmuch_message_t *message;
-    const char *filename;
-    FILE *file;
     int ret = 0;
-    int c;
 
     const char *headers[] = {
-       "Subject", "From", "To", "Cc", "Bcc", "Date"
+       "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]);
+    query_string = query_string_from_args (local, argc, argv);
     if (query_string == NULL) {
        fprintf (stderr, "Out of memory\n");
        ret = 1;
@@ -844,14 +1100,20 @@ show_command (unused (int argc), unused (char *argv[]))
     }
 
     for (messages = notmuch_query_search_messages (query);
-        notmuch_message_results_has_more (messages);
-        notmuch_message_results_advance (messages))
+        notmuch_messages_has_more (messages);
+        notmuch_messages_advance (messages))
     {
-       message = notmuch_message_results_get (messages);
+       message = notmuch_messages_get (messages);
 
-       printf ("%%message{\n");
+       printf ("\fmessage{ id:%s filename:%s\n",
+               notmuch_message_get_message_id (message),
+               notmuch_message_get_filename (message));
 
-       printf ("%%header{\n");
+       printf ("\fheader{\n");
+
+       printf ("%s\n", _get_one_line_summary (local, message));
+
+       printf ("%s\n", notmuch_message_get_header (message, "subject"));
 
        for (i = 0; i < ARRAY_SIZE (headers); i++) {
            name = headers[i];
@@ -860,24 +1122,14 @@ show_command (unused (int argc), unused (char *argv[]))
                printf ("%s: %s\n", name, value);
        }
 
-       printf ("%%header}\n");
+       printf ("\fheader}\n");
+       printf ("\fbody{\n");
 
-       filename = notmuch_message_get_filename (message);
+       show_message_body (notmuch_message_get_filename (message));
 
-       file = fopen (filename, "r");
-       if (file) {
-           size_t header_size = notmuch_message_get_header_size (message);
-           fseek (file, header_size + 1, SEEK_SET);
-           while (1) {
-               c = fgetc (file);
-               if (c == EOF)
-                   break;
-               putchar (c);
-           }
-       }
-       fclose (file);
+       printf ("\fbody}\n");
 
-       printf ("%%message}\n");
+       printf ("\fmessage}\n");
 
        notmuch_message_destroy (message);
     }
@@ -896,7 +1148,7 @@ show_command (unused (int argc), unused (char *argv[]))
 }
 
 static int
-tag_command (unused (int argc), unused (char *argv[]))
+tag_command (void *ctx, unused (int argc), unused (char *argv[]))
 {
     void *local;
     int *add_tags, *remove_tags;
@@ -905,12 +1157,12 @@ tag_command (unused (int argc), unused (char *argv[]))
     char *query_string;
     notmuch_database_t *notmuch = NULL;
     notmuch_query_t *query;
-    notmuch_message_results_t *results;
+    notmuch_messages_t *messages;
     notmuch_message_t *message;
     int ret = 0;
     int i;
 
-    local = talloc_new (NULL);
+    local = talloc_new (ctx);
     if (local == NULL) {
        ret = 1;
        goto DONE;
@@ -969,11 +1221,11 @@ tag_command (unused (int argc), unused (char *argv[]))
        goto DONE;
     }
 
-    for (results = notmuch_query_search_messages (query);
-        notmuch_message_results_has_more (results);
-        notmuch_message_results_advance (results))
+    for (messages = notmuch_query_search_messages (query);
+        notmuch_messages_has_more (messages);
+        notmuch_messages_advance (messages))
     {
-       message = notmuch_message_results_get (results);
+       message = notmuch_messages_get (messages);
 
        notmuch_message_freeze (message);
 
@@ -1001,12 +1253,12 @@ tag_command (unused (int argc), unused (char *argv[]))
 }
 
 static int
-dump_command (int argc, char *argv[])
+dump_command (unused (void *ctx), int argc, char *argv[])
 {
     FILE *output = NULL;
     notmuch_database_t *notmuch = NULL;
     notmuch_query_t *query;
-    notmuch_message_results_t *results;
+    notmuch_messages_t *messages;
     notmuch_message_t *message;
     notmuch_tags_t *tags;
     int ret = 0;
@@ -1038,12 +1290,12 @@ dump_command (int argc, char *argv[])
 
     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
 
-    for (results = notmuch_query_search_messages (query);
-        notmuch_message_results_has_more (results);
-        notmuch_message_results_advance (results))
+    for (messages = notmuch_query_search_messages (query);
+        notmuch_messages_has_more (messages);
+        notmuch_messages_advance (messages))
     {
        int first = 1;
-       message = notmuch_message_results_get (results);
+       message = notmuch_messages_get (messages);
 
        fprintf (output,
                 "%s (", notmuch_message_get_message_id (message));
@@ -1077,7 +1329,7 @@ dump_command (int argc, char *argv[])
 }
 
 static int
-restore_command (int argc, char *argv[])
+restore_command (unused (void *ctx), int argc, char *argv[])
 {
     FILE *input = NULL;
     notmuch_database_t *notmuch = NULL;
@@ -1186,7 +1438,7 @@ restore_command (int argc, char *argv[])
 }
 
 static int
-help_command (int argc, char *argv[]);
+help_command (void *ctx, int argc, char *argv[]);
 
 command_t commands[] = {
     { "setup", setup_command,
@@ -1204,7 +1456,7 @@ command_t commands[] = {
       "\t\t\"inbox\" and \"unread\".\n"
       "\n"
       "\t\tNote: \"notmuch new\" will skip any read-only directories,\n"
-      "\t\tso you can use that to mark tdirectories that will not\n"
+      "\t\tso you can use that to mark directories that will not\n"
       "\t\treceive any new mail (and make \"notmuch new\" faster)." },
     { "search", search_command,
       "<search-term> [...]\n\n"
@@ -1212,11 +1464,24 @@ command_t commands[] = {
       "\t\tNote that the individual mail messages will be matched\n"
       "\t\tagainst the search terms, but the results will be the\n"
       "\t\tthreads containing the matched messages.\n\n"
-      "\t\tCurrently, the supported search terms are as follows, (where\n"
-      "\t\t<brackets> indicate user-supplied values):\n\n"
+      "\t\tCurrently, in addition to free text (and quoted phrases)\n"
+      "\t\twhich match terms appearing anywhere within an email,\n"
+      "\t\tthe following prefixes can be used to search specific\n"
+      "\t\tportions of an email, (where <brackets> indicate user-\n"
+      "\t\tsupplied values):\n\n"
+      "\t\t\tfrom:<name-or-address>\n"
+      "\t\t\tto:<name-or-address>\n"
+      "\t\t\tsubject:<word-or-quoted-phrase>\n"
       "\t\t\ttag:<tag>\n"
       "\t\t\tid:<message-id>\n"
       "\t\t\tthread:<thread-id>\n\n"
+      "\t\tThe from: prefix is used to match the name or address of\n"
+      "\t\tthe sender of an email message.\n\n"
+      "\t\tThe to: prefix is used to match the names or addresses of\n"
+      "\t\tany recipient of an email message, (whether To, Cc, or Bcc).\n\n"
+      "\t\tAny term prefixed with subject: will match only text from\n"
+      "\t\tthe subject of an email. Quoted phrases are supported when\n"
+      "\t\tsearching with: subject:\"this is a phrase\".\n\n"
       "\t\tValid tag values include \"inbox\" and \"unread\" by default\n"
       "\t\tfor new messages added by \"notmuch new\" as well as any other\n"
       "\t\ttag values added manually with \"notmuch tag\".\n\n"
@@ -1235,11 +1500,22 @@ command_t commands[] = {
       "\t\tinterpretation by the shell, (such as by putting quotation\n"
       "\t\tmarks around any parenthesized expression)." },
     { "show", show_command,
-      "<thread-id>\n\n"
-      "\t\tShow the thread with the given thread ID (see 'search').",
-      "\t\tThread ID values are given as the first column in the\n"
-      "\t\toutput of the \"notmuch search\" command. These are the\n"
-      "\t\trandom-looking strings of 32 characters." },
+      "<search-terms> [...]\n\n"
+      "\t\tShows all messages matching the search terms.",
+      "\t\tSee the documentation of \"notmuch search\" for details\n"
+      "\t\tof the supported syntax of search terms.\n\n"
+      "\t\tA common use of \"notmuch show\" is to display a single\n"
+      "\t\tthread of email messages. For this, use a search term of\n"
+      "\t\t\"thread:<thread-id>\" as can be seen in the first column\n"
+      "\t\tof output from the \"notmuch search\" command.\n\n"
+      "\t\tAll messages will be displayed in date order. The output\n"
+      "\t\tformat is plain-text, with all text-content MIME parts\n"
+      "\t\tdecoded. Various components in the output, ('message',\n"
+      "\t\t'header', 'body', 'attachment', and MIME 'part') will be\n"
+      "\t\tdelimited by easily-parsed markers. Each marker consists\n"
+      "\t\tof a Control-L character (ASCII decimal 12), the name of\n"
+      "\t\tthe marker, and then either an opening or closing brace,\n"
+      "\t\t'{' or '}' to either open or close the component."},
     { "tag", tag_command,
       "+<tag>|-<tag> [...] [--] <search-term> [...]\n\n"
       "\t\tAdd/remove tags for all messages matching the search terms.",
@@ -1303,7 +1579,7 @@ usage (void)
 }
 
 static int
-help_command (int argc, char *argv[])
+help_command (unused (void *ctx), int argc, char *argv[])
 {
     command_t *command;
     unsigned int i;
@@ -1334,23 +1610,26 @@ help_command (int argc, char *argv[])
 int
 main (int argc, char *argv[])
 {
+    void *local = talloc_new (NULL);
     command_t *command;
     unsigned int i;
 
     if (argc == 1)
-       return setup_command (0, NULL);
+       return setup_command (local, 0, NULL);
 
     for (i = 0; i < ARRAY_SIZE (commands); i++) {
        command = &commands[i];
 
        if (strcmp (argv[1], command->name) == 0)
-           return (command->function) (argc - 2, &argv[2]);
+           return (command->function) (local, argc - 2, &argv[2]);
     }
 
     /* Don't complain about "help" being an unknown command when we're
        about to provide exactly what's wanted anyway. */
-    fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
-    usage ();
+    fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
+            argv[1]);
+
+    talloc_free (local);
 
     return 1;
 }