]> git.notmuchmail.org Git - notmuch/blobdiff - notmuch.c
Fix relative date formatting to not split one day into two formats.
[notmuch] / notmuch.c
index 4b16a71eb7352321f657fb08f8de7e6d88f0ff3a..7ee1d6bc0d4aa3258a203fe56d5b720a7f752d25 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -38,6 +38,7 @@
 #include <unistd.h>
 #include <dirent.h>
 #include <errno.h>
+#include <signal.h>
 
 #include <talloc.h>
 
@@ -123,6 +124,14 @@ print_formatted_seconds (double seconds)
     printf ("%ds", (int) seconds);
 }
 
+static volatile sig_atomic_t do_add_files_print_progress = 0;
+
+static void
+handle_sigalrm (unused (int signal))
+{
+    do_add_files_print_progress = 1;
+}
+
 static void
 add_files_print_progress (add_files_state_t *state)
 {
@@ -298,8 +307,10 @@ add_files_recursive (notmuch_database_t *notmuch,
                    message = NULL;
                }
 
-               if (state->processed_files % 1000 == 0)
+               if (do_add_files_print_progress) {
+                   do_add_files_print_progress = 0;
                    add_files_print_progress (state);
+               }
            }
        } else if (S_ISDIR (st->st_mode)) {
            status = add_files_recursive (notmuch, next, st, state);
@@ -327,16 +338,17 @@ add_files_recursive (notmuch_database_t *notmuch,
 }
 
 /* This is the top-level entry point for add_files. It does a couple
- * of error checks, and then calls into the recursive function,
- * (avoiding the repeating of these error checks at every
- * level---which would be useless becaues we already do a stat() at
- * the level above). */
+ * of error checks, sets up the progress-printing timer and then calls
+ * into the recursive function. */
 static notmuch_status_t
 add_files (notmuch_database_t *notmuch,
           const char *path,
           add_files_state_t *state)
 {
     struct stat st;
+    notmuch_status_t status;
+    struct sigaction action;
+    struct itimerval timerval;
 
     if (stat (path, &st)) {
        fprintf (stderr, "Error reading directory %s: %s\n",
@@ -349,7 +361,34 @@ add_files (notmuch_database_t *notmuch,
        return NOTMUCH_STATUS_FILE_ERROR;
     }
 
-    return add_files_recursive (notmuch, path, &st, state);
+    /* Setup our handler for SIGALRM */
+    memset (&action, 0, sizeof (struct sigaction));
+    action.sa_handler = handle_sigalrm;
+    sigemptyset (&action.sa_mask);
+    action.sa_flags = SA_RESTART;
+    sigaction (SIGALRM, &action, NULL);
+
+    /* Then start a timer to send SIGALRM once per second. */
+    timerval.it_interval.tv_sec = 1;
+    timerval.it_interval.tv_usec = 0;
+    timerval.it_value.tv_sec = 1;
+    timerval.it_value.tv_usec = 0;
+    setitimer (ITIMER_REAL, &timerval, NULL);
+
+    status = add_files_recursive (notmuch, path, &st, state);
+
+    /* Now stop the timer. */
+    timerval.it_interval.tv_sec = 0;
+    timerval.it_interval.tv_usec = 0;
+    timerval.it_value.tv_sec = 0;
+    timerval.it_value.tv_usec = 0;
+    setitimer (ITIMER_REAL, &timerval, NULL);
+
+    /* And disable the signal handler. */
+    action.sa_handler = SIG_IGN;
+    sigaction (SIGALRM, &action, NULL);
+
+    return status;
 }
 
 /* Recursively count all regular files in path and all sub-direcotries
@@ -492,6 +531,30 @@ setup_command (unused (int argc), unused (char *argv[]))
        free (default_path);
     }
 
+    /* Coerce th directory into an absolute directory name. */
+    if (*mail_directory != '/') {
+       char *cwd, *absolute_mail_directory;
+
+       cwd = getcwd (NULL, 0);
+       if (cwd == NULL) {
+           fprintf (stderr, "Out of memory.\n");
+           exit (1);
+       }
+
+       if (asprintf (&absolute_mail_directory, "%s/%s",
+                     cwd, mail_directory) < 0)
+       {
+           fprintf (stderr, "Out of memory.\n");
+           exit (1);
+       }
+
+       free (cwd);
+       free (mail_directory);
+       mail_directory = absolute_mail_directory;
+
+       printf ("Abs: %s\n", mail_directory);
+    }
+
     notmuch = notmuch_database_create (mail_directory);
     if (notmuch == NULL) {
        fprintf (stderr, "Failed to create new notmuch database at %s\n",
@@ -670,6 +733,79 @@ 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 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 (int argc, char *argv[])
 {
@@ -680,6 +816,8 @@ search_command (int argc, char *argv[])
     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);
@@ -705,8 +843,12 @@ search_command (int argc, char *argv[])
 
        thread = notmuch_thread_results_get (results);
 
-       printf ("%s %s",
+       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 (" (");
@@ -734,11 +876,124 @@ search_command (int argc, char *argv[])
     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 int
 show_command (unused (int argc), unused (char *argv[]))
 {
-    fprintf (stderr, "Error: show is not implemented yet.\n");
-    return 1;
+    void *local = talloc_new (NULL);
+    char *query_string;
+    notmuch_database_t *notmuch = NULL;
+    notmuch_query_t *query = NULL;
+    notmuch_message_results_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"
+    };
+    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_message_results_has_more (messages);
+        notmuch_message_results_advance (messages))
+    {
+       message = notmuch_message_results_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");
+
+       filename = 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 ("%%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
@@ -1037,12 +1292,16 @@ help_command (int argc, char *argv[]);
 command_t commands[] = {
     { "setup", setup_command,
       "Interactively setup notmuch for first use.",
+      "\t\tThe setup command is the first command you will run in order\n"
+      "\t\tto start using notmuch. It will prompt you for the directory\n"
+      "\t\tcontaining your email archives, and will then proceed to build\n"
+      "\t\ta database to allow fast searching of that mail.\n\n"
       "\t\tInvoking notmuch with no command argument will run setup if\n"
       "\t\tthe setup command has not previously been completed." },
     { "new", new_command,
       "Find and import any new messages.",
       "\t\tScans all sub-directories of the database, adding new messages\n"
-      "\t\tthat are found. Each new message will be tagges as both\n"
+      "\t\tthat are found. Each new message will be tagged as both\n"
       "\t\t\"inbox\" and \"unread\".\n"
       "\n"
       "\t\tNote: \"notmuch new\" will skip any read-only directories,\n"
@@ -1051,16 +1310,41 @@ command_t commands[] = {
     { "search", search_command,
       "<search-term> [...]\n\n"
       "\t\tSearch for threads matching the given search terms.",
-      "\t\tOnce we actually implement search we'll document the\n"
-      "\t\tsyntax here." },
+      "\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\t\ttag:<tag>\n"
+      "\t\t\tid:<message-id>\n"
+      "\t\t\tthread:<thread-id>\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"
+      "\t\tMessage ID values are the literal contents of the Message-ID:\n"
+      "\t\theader of email messages, but without the '<','>' delimiters.\n\n"
+      "\t\tThread ID values are generated internally by notmuch but can\n"
+      "\t\tbe seen in the output of \"notmuch search\" for example.\n\n"
+      "\t\tIn addition to individual terms, multiple terms can be\n"
+      "\t\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n"
+      "\t\tEach term in the query will be implicitly connected by a\n"
+      "\t\tlogical AND if no explicit operator is provided, (except\n"
+      "\t\tthat terms with a common prefix will be implicitly combined\n"
+      "\t\twith OR until we get Xapian defect #402 fixed).\n\n"
+      "\t\tParentheses can also be used to control the combination of\n"
+      "\t\tthe Boolean operators, but will have to be protected from\n"
+      "\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." },
     { "tag", tag_command,
       "+<tag>|-<tag> [...] [--] <search-term> [...]\n\n"
       "\t\tAdd/remove tags for all messages matching the search terms.",
-      "\t\tThe search terms are handled texactly as in 'search' so one\n"
+      "\t\tThe search terms are handled exactly as in 'search' so one\n"
       "\t\tcan use that command first to see what will be modified.\n\n"
       "\t\tTags prefixed by '+' are added while those prefixed by '-' are\n"
       "\t\tremoved. For each message, tag removal is before tag addition.\n\n"
@@ -1068,7 +1352,13 @@ command_t commands[] = {
       "\t\targument that begins with neither '+' nor '-'. Support for\n"
       "\t\tan initial search term beginning with '+' or '-' is provided\n"
       "\t\tby allowing the user to specify a \"--\" argument to separate\n"
-      "\t\tthe tags from the search terms." },
+      "\t\tthe tags from the search terms.\n\n"
+      "\t\tNote: If you run \"notmuch new\" between reading a thread with\n"
+      "\t\t\"notmuch show\" and removing the \"inbox\" tag for that thread\n"
+      "\t\twith \"notmuch tag\" then you create the possibility of moving\n"
+      "\t\tsome messages from that thread out of your inbox without ever\n"
+      "\t\treading them. The easiest way to avoid this problem is to not\n"
+      "\t\trun \"notmuch new\" between reading and removing tags." },
     { "dump", dump_command,
       "[<filename>]\n\n"
       "\t\tCreate a plain-text dump of the tags for each message.",
@@ -1077,15 +1367,20 @@ command_t commands[] = {
       "\t\tthat can't be recreated from the messages themselves.\n"
       "\t\tThe output of notmuch dump is therefore the only\n"
       "\t\tcritical thing to backup (and much more friendly to\n"
-      "\t\tincremental backup than the native database files." },
+      "\t\tincremental backup than the native database files.)" },
     { "restore", restore_command,
       "<filename>\n\n"
       "\t\tRestore the tags from the given dump file (see 'dump').",
-      "" },
+      "\t\tNote: The dump file format is specifically chosen to be\n"
+      "\t\tcompatible with the format of files produced by sup-dump.\n"
+      "\t\tSo if you've previously been using sup for mail, then the\n"
+      "\t\t\"notmuch restore\" command provides you a way to import\n"
+      "\t\tall of your tags (or labels as sup calls them)." },
     { "help", help_command,
       "[<command>]\n\n"
       "\t\tThis message, or more detailed help for the named command.",
-      "" }
+      "\t\tExcept in this case, where there's not much more detailed\n"
+      "\t\thelp available." }
 };
 
 static void