]> git.notmuchmail.org Git - notmuch/blobdiff - notmuch.c
Print a better message than "0s" for zero seconds.
[notmuch] / notmuch.c
index 966901f04b1c42b8affcfe23496b61eab3e4f6bc..d4e0745369b57b82069260c8ffd2a45b088a476b 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
  * Author: Carl Worth <cworth@cworth.org>
  */
 
-#include "notmuch.h"
-
 #ifndef _GNU_SOURCE
 #define _GNU_SOURCE /* for getline */
 #endif
-
 #include <stdio.h>
-#include <stdlib.h>
+
+#include "notmuch.h"
+
+/* This is separate from notmuch-private.h because we're trying to
+ * keep notmuch.c from looking into any internals, (which helps us
+ * develop notmuch.h into a plausible library interface).
+ */
+#include "xutil.h"
+
 #include <stddef.h>
 #include <string.h>
-#include <sys/types.h>
 #include <sys/stat.h>
 #include <sys/time.h>
 #include <unistd.h>
 #include <dirent.h>
 #include <errno.h>
 
+#include <talloc.h>
+
 #include <glib.h> /* g_strdup_printf */
 
 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
@@ -48,11 +54,19 @@ typedef struct command {
 } command_t;
 
 typedef struct {
-    int total_messages;
-    int count;
+    int total_files;
+    int processed_files;
+    int added_messages;
     struct timeval tv_start;
 } add_files_state_t;
 
+static void
+chomp_newline (char *str)
+{
+    if (str && str[strlen(str)-1] == '\n')
+       str[strlen(str)-1] = '\0';
+}
+
 /* Compute the number of seconds elapsed from start to end. */
 double
 tv_elapsed (struct timeval start, struct timeval end)
@@ -67,6 +81,11 @@ print_formatted_seconds (double seconds)
     int hours;
     int minutes;
 
+    if (seconds < 1) {
+       printf ("almost no time");
+       return;
+    }
+
     if (seconds > 3600) {
        hours = (int) seconds / 3600;
        printf ("%dh ", hours);
@@ -79,7 +98,7 @@ print_formatted_seconds (double seconds)
        seconds -= minutes * 60;
     }
 
-    printf ("%02ds", (int) seconds);
+    printf ("%ds", (int) seconds);
 }
 
 void
@@ -91,37 +110,73 @@ add_files_print_progress (add_files_state_t *state)
     gettimeofday (&tv_now, NULL);
 
     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
-    rate_overall = (state->count) / elapsed_overall;
+    rate_overall = (state->processed_files) / elapsed_overall;
 
-    printf ("Added %d of %d messages (",
-           state->count, state->total_messages);
-    print_formatted_seconds ((state->total_messages - state->count) /
-                            rate_overall);
-    printf (" remaining).\r");
+    printf ("Processed %d", state->processed_files);
+
+    if (state->total_files) {
+       printf (" of %d files (", state->total_files);
+       print_formatted_seconds ((state->total_files - state->processed_files) /
+                                rate_overall);
+       printf (" remaining).      \r");
+    } else {
+       printf (" files (%d files/sec.)    \r", (int) rate_overall);
+    }
 
     fflush (stdout);
 }
 
-/* Recursively find all regular files in 'path' and add them to the
- * database. */
-void
+/* Examine 'path' recursively as follows:
+ *
+ *   o Ask the filesystem for the mtime of 'path' (path_mtime)
+ *
+ *   o Ask the database for its timestamp of 'path' (path_dbtime)
+ *
+ *   o If 'path_mtime' > 'path_dbtime'
+ *
+ *       o For each regular file in 'path' with mtime newer than the
+ *         'path_dbtime' call add_message to add the file to the
+ *         database.
+ *
+ *       o For each sub-directory of path, recursively call into this
+ *         same function.
+ *
+ *   o Tell the database to update its time of 'path' to 'path_mtime'
+ */
+notmuch_status_t
 add_files (notmuch_database_t *notmuch, const char *path,
           add_files_state_t *state)
 {
-    DIR *dir;
-    struct dirent *entry, *e;
+    DIR *dir = NULL;
+    struct dirent *e, *entry = NULL;
     int entry_length;
     int err;
-    char *next;
+    char *next = NULL;
     struct stat st;
-    notmuch_status_t status;
+    time_t path_mtime, path_dbtime;
+    notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
 
-    dir = opendir (path);
+    if (stat (path, &st)) {
+       fprintf (stderr, "Error reading directory %s: %s\n",
+                path, strerror (errno));
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+
+    if (! S_ISDIR (st.st_mode)) {
+       fprintf (stderr, "Error: %s is not a directory.\n", path);
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+
+    path_mtime = st.st_mtime;
 
+    path_dbtime = notmuch_database_get_timestamp (notmuch, path);
+
+    dir = opendir (path);
     if (dir == NULL) {
-       fprintf (stderr, "Warning: failed to open directory %s: %s\n",
+       fprintf (stderr, "Error opening directory %s: %s\n",
                 path, strerror (errno));
-       return;
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
     }
 
     entry_length = offsetof (struct dirent, d_name) +
@@ -133,13 +188,19 @@ add_files (notmuch_database_t *notmuch, const char *path,
        if (err) {
            fprintf (stderr, "Error reading directory: %s\n",
                     strerror (errno));
-           free (entry);
-           return;
+           ret = NOTMUCH_STATUS_FILE_ERROR;
+           goto DONE;
        }
 
        if (e == NULL)
            break;
 
+       /* If this directory hasn't been modified since the last
+        * add_files, then we only need to look further for
+        * sub-directories. */
+       if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
+           continue;
+
        /* Ignore special directories to avoid infinite recursion.
         * Also ignore the .notmuch directory.
         */
@@ -154,28 +215,67 @@ add_files (notmuch_database_t *notmuch, const char *path,
 
        next = g_strdup_printf ("%s/%s", path, entry->d_name);
 
-       stat (next, &st);
+       if (stat (next, &st)) {
+           fprintf (stderr, "Error reading %s: %s\n",
+                    next, strerror (errno));
+           ret = NOTMUCH_STATUS_FILE_ERROR;
+           continue;
+       }
 
        if (S_ISREG (st.st_mode)) {
-           status = notmuch_database_add_message (notmuch, next);
-           if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
-               fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
-                        next);
-           } else {
-               state->count++;
+           /* If the file hasn't been modified since the last
+            * add_files, then we need not look at it. */
+           if (st.st_mtime > path_dbtime) {
+               state->processed_files++;
+
+               status = notmuch_database_add_message (notmuch, next);
+               switch (status) {
+                   /* success */
+                   case NOTMUCH_STATUS_SUCCESS:
+                       state->added_messages++;
+                       break;
+                   /* Non-fatal issues (go on to next file) */
+                   case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+                       /* Stay silent on this one. */
+                       break;
+                   case NOTMUCH_STATUS_FILE_NOT_EMAIL:
+                       fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
+                                next);
+                       break;
+                   /* Fatal issues. Don't process anymore. */
+                   case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+                       fprintf (stderr, "A Xapian error was encountered. Halting processing.\n");
+                       ret = status;
+                       goto DONE;
+                   default:
+                       fprintf (stderr, "Internal error: add_message returned unexpected value: %d\n",  status);
+                       ret = status;
+                       goto DONE;
+               }
+               if (state->processed_files % 1000 == 0)
+                   add_files_print_progress (state);
            }
-           if (state->count % 1000 == 0)
-               add_files_print_progress (state);
        } else if (S_ISDIR (st.st_mode)) {
-           add_files (notmuch, next, state);
+           status = add_files (notmuch, next, state);
+           if (status && ret == NOTMUCH_STATUS_SUCCESS)
+               ret = status;
        }
 
        free (next);
+       next = NULL;
     }
 
-    free (entry);
+    notmuch_database_set_timestamp (notmuch, path, path_mtime);
 
-    closedir (dir);
+  DONE:
+    if (next)
+       free (next);
+    if (entry)
+       free (entry);
+    if (dir)
+       closedir (dir);
+
+    return ret;
 }
 
 /* Recursively count all regular files in path and all sub-direcotries
@@ -253,13 +353,14 @@ count_files (const char *path, int *count)
 int
 setup_command (int argc, char *argv[])
 {
-    notmuch_database_t *notmuch;
-    char *mail_directory, *default_path;
+    notmuch_database_t *notmuch = NULL;
+    char *default_path, *mail_directory = NULL;
     size_t line_size;
     int count;
     add_files_state_t add_files_state;
     double elapsed;
     struct timeval tv_now;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
 
     printf ("Welcome to notmuch!\n\n");
 
@@ -285,8 +386,9 @@ setup_command (int argc, char *argv[])
     printf ("Top-level mail directory [%s]: ", default_path);
     fflush (stdout);
 
-    mail_directory = NULL;
     getline (&mail_directory, &line_size, stdin);
+    chomp_newline (mail_directory);
+
     printf ("\n");
 
     if (mail_directory == NULL || strlen (mail_directory) == 0) {
@@ -294,8 +396,6 @@ setup_command (int argc, char *argv[])
            free (mail_directory);
        mail_directory = default_path;
     } else {
-       if (mail_directory[strlen(mail_directory)-1] == '\n')
-           mail_directory[strlen(mail_directory)-1] = '\0';
        /* XXX: Instead of telling the user to use an environment
         * variable here, we should really be writing out a configuration
         * file and loading that on the next run. */
@@ -315,8 +415,8 @@ setup_command (int argc, char *argv[])
     if (notmuch == NULL) {
        fprintf (stderr, "Failed to create new notmuch database at %s\n",
                 mail_directory);
-       free (mail_directory);
-       return 1;
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
     }
 
     printf ("OK. Let's take a look at the mail we can find in the directory\n");
@@ -329,24 +429,100 @@ setup_command (int argc, char *argv[])
 
     printf ("Next, we'll inspect the messages and create a database of threads:\n");
 
-    add_files_state.total_messages = count;
-    add_files_state.count = 0;
+    add_files_state.total_files = count;
+    add_files_state.processed_files = 0;
+    add_files_state.added_messages = 0;
     gettimeofday (&add_files_state.tv_start, NULL);
 
-    add_files (notmuch, mail_directory, &add_files_state);
+    ret = add_files (notmuch, mail_directory, &add_files_state);
 
     gettimeofday (&tv_now, NULL);
     elapsed = tv_elapsed (add_files_state.tv_start,
                          tv_now);
-    printf ("Added %d total messages in ", add_files_state.count);
+    printf ("Processed %d total files in ", add_files_state.processed_files);
     print_formatted_seconds (elapsed);
-    printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
+    if (elapsed > 1) {
+       printf (" (%d files/sec.).                 \n",
+               (int) (add_files_state.processed_files / elapsed));
+    } else {
+       printf (".                    \n");
+    }
+    printf ("Added %d unique messages to the database.\n\n",
+           add_files_state.added_messages);
 
-    notmuch_database_close (notmuch);
+    printf ("When new mail is delivered to %s in the future,\n"
+           "run \"notmuch new\" to add it to the database.\n",
+           mail_directory);
 
-    free (mail_directory);
-    
-    return 0;
+    if (ret) {
+       printf ("Note: At least one error was encountered: %s\n",
+               notmuch_status_to_string (ret));
+    }
+
+  DONE:
+    if (mail_directory)
+       free (mail_directory);
+    if (notmuch)
+       notmuch_database_close (notmuch);
+
+    return ret;
+}
+
+int
+new_command (int argc, char *argv[])
+{
+    notmuch_database_t *notmuch;
+    const char *mail_directory;
+    add_files_state_t add_files_state;
+    double elapsed;
+    struct timeval tv_now;
+    int ret = 0;
+
+    notmuch = notmuch_database_open (NULL);
+    if (notmuch == NULL) {
+       ret = 1;
+       goto DONE;
+    }
+
+    mail_directory = notmuch_database_get_path (notmuch);
+
+    add_files_state.total_files = 0;
+    add_files_state.processed_files = 0;
+    add_files_state.added_messages = 0;
+    gettimeofday (&add_files_state.tv_start, NULL);
+
+    ret = add_files (notmuch, mail_directory, &add_files_state);
+
+    gettimeofday (&tv_now, NULL);
+    elapsed = tv_elapsed (add_files_state.tv_start,
+                         tv_now);
+    if (add_files_state.processed_files) {
+       printf ("Processed %d total files in ", add_files_state.processed_files);
+       print_formatted_seconds (elapsed);
+       if (elapsed > 1) {
+           printf (" (%d files/sec.).                 \n",
+                   (int) (add_files_state.processed_files / elapsed));
+       } else {
+           printf (".                    \n");
+       }
+    }
+    if (add_files_state.added_messages) {
+       printf ("Added %d new messages to the database (not much, really).\n",
+               add_files_state.added_messages);
+    } else {
+       printf ("No new mail---and that's not much!\n");
+    }
+
+    if (ret) {
+       printf ("Note: At least one error was encountered: %s\n",
+               notmuch_status_to_string (ret));
+    }
+
+  DONE:
+    if (notmuch)
+       notmuch_database_close (notmuch);
+
+    return ret;
 }
 
 int
@@ -366,15 +542,183 @@ show_command (int argc, char *argv[])
 int
 dump_command (int argc, char *argv[])
 {
-    fprintf (stderr, "Error: dump is not implemented yet.\n");
-    return 1;
+    FILE *output;
+    notmuch_database_t *notmuch;
+    notmuch_query_t *query;
+    notmuch_results_t *results;
+    notmuch_message_t *message;
+    notmuch_tags_t *tags;
+    int ret = 0;
+
+    if (argc) {
+       output = fopen (argv[0], "w");
+       if (output == NULL) {
+           fprintf (stderr, "Error opening %s for writing: %s\n",
+                    argv[0], strerror (errno));
+           ret = 1;
+           goto DONE;
+       }
+    } else {
+       output = stdout;
+    }
+
+    notmuch = notmuch_database_open (NULL);
+    if (notmuch == NULL) {
+       ret = 1;
+       goto DONE;
+    }
+
+    query = notmuch_query_create (notmuch, "");
+    if (query == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       ret = 1;
+       goto DONE;
+    }
+
+    notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
+
+    for (results = notmuch_query_search (query);
+        notmuch_results_has_more (results);
+        notmuch_results_advance (results))
+    {
+       int first = 1;
+       message = notmuch_results_get (results);
+
+       fprintf (output,
+                "%s (", notmuch_message_get_message_id (message));
+
+       for (tags = notmuch_message_get_tags (message);
+            notmuch_tags_has_more (tags);
+            notmuch_tags_advance (tags))
+       {
+           if (! first)
+               fprintf (output, " ");
+
+           fprintf (output, "%s", notmuch_tags_get (tags));
+
+           first = 0;
+       }
+
+       fprintf (output, ")\n");
+
+       notmuch_message_destroy (message);
+    }
+
+    notmuch_query_destroy (query);
+
+  DONE:
+    if (notmuch)
+       notmuch_database_close (notmuch);
+    if (output != stdout)
+       fclose (output);
+
+    return ret;
 }
 
 int
 restore_command (int argc, char *argv[])
 {
-    fprintf (stderr, "Error: restore is not implemented yet.\n");
-    return 1;
+    FILE *input;
+    notmuch_database_t *notmuch;
+    char *line = NULL;
+    size_t line_size, line_len;
+    regex_t regex;
+    int rerr;
+    int ret = 0;
+
+    if (argc) {
+       input = fopen (argv[0], "r");
+       if (input == NULL) {
+           fprintf (stderr, "Error opening %s for reading: %s\n",
+                    argv[0], strerror (errno));
+           ret = 1;
+           goto DONE;
+       }
+    } else {
+       printf ("No filename given. Reading dump from stdin.\n");
+       input = stdin;
+    }
+
+    notmuch = notmuch_database_open (NULL);
+    if (notmuch == NULL) {
+       ret = 1;
+       goto DONE;
+    }
+
+    /* Dump output is one line per message. We match a sequence of
+     * non-space characters for the message-id, then one or more
+     * spaces, then a list of space-separated tags as a sequence of
+     * characters within literal '(' and ')'. */
+    xregcomp (&regex,
+             "^([^ ]+) \\(([^)]*)\\)$",
+             REG_EXTENDED);
+
+    while ((line_len = getline (&line, &line_size, input)) != -1) {
+       regmatch_t match[3];
+       char *message_id, *tags, *tag, *next;
+       notmuch_message_t *message;
+       notmuch_status_t status;
+
+       chomp_newline (line);
+
+       rerr = xregexec (&regex, line, 3, match, 0);
+       if (rerr == REG_NOMATCH)
+       {
+           fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
+                    line);
+           continue;
+       }
+
+       message_id = xstrndup (line + match[1].rm_so,
+                              match[1].rm_eo - match[1].rm_so);
+       tags = xstrndup (line + match[2].rm_so,
+                        match[2].rm_eo - match[2].rm_so);
+
+       if (strlen (tags)) {
+
+           message = notmuch_database_find_message (notmuch, message_id);
+           if (message == NULL) {
+               fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
+                        message_id);
+           }
+
+           next = tags;
+           while (next) {
+               tag = strsep (&next, " ");
+               if (*tag == '\0')
+                   continue;
+               if (message) {
+                   status = notmuch_message_add_tag (message, tag);
+                   if (status) {
+                       fprintf (stderr,
+                                "Error applying tag %s to message %s:\n",
+                                tag, message_id);
+                       fprintf (stderr, "%s\n",
+                                notmuch_status_to_string (status));
+                   }
+               } else {
+                   fprintf (stderr, "%s ", tag);
+               }
+           }
+
+           if (message)
+               notmuch_message_destroy (message);
+           else
+               fprintf (stderr, ")\n");
+       }
+       free (message_id);
+       free (tags);
+    }
+
+    regfree (&regex);
+
+  DONE:
+    if (line)
+       free (line);
+    if (notmuch)
+       notmuch_database_close (notmuch);
+
+    return ret;
 }
 
 command_t commands[] = {
@@ -382,6 +726,8 @@ command_t commands[] = {
       "Interactively setup notmuch for first use.\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."},
     { "search", search_command,
       "<search-term> [...]\n\n"
       "\t\tSearch for threads matching the given search terms.\n"