notmuch_status_t notmuch_database_add_stub_message (notmuch_database_t *notmuch, const char *message_id, const char **tags) { const char **tag; notmuch_status_t ret; notmuch_private_status_t private_status; notmuch_message_t *message; ret = _notmuch_database_ensure_writable (notmuch); if (ret) return ret; message = _notmuch_message_create_for_message_id (notmuch, message_id, &private_status); if (message == NULL) { return COERCE_STATUS (private_status, "Unexpected status value from _notmuch_message_create_for_message_id"); } if (private_status != NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) return NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID; _notmuch_message_add_term (message, "type", "mail"); if (tags) { ret = notmuch_message_freeze (message); if (ret) return ret; for (tag = tags; *tag; tag++) { ret = notmuch_message_add_tag (message, *tag); if (ret) return ret; } ret = notmuch_message_thaw (message); if (ret) return ret; } return NOTMUCH_STATUS_SUCCESS; }
static int update_tags(notmuch_message_t *msg, const char *tags) { char *tag = NULL, *end = NULL, *p; char *buf = safe_strdup(tags); if (!buf) return -1; notmuch_message_freeze(msg); for (p = buf; p && *p; p++) { if (!tag && isspace(*p)) continue; if (!tag) tag = p; /* begin of the tag */ if (*p == ',' || *p == ' ') end = p; /* terminate the tag */ else if (*(p + 1) == '\0') end = p + 1; /* end of optstr */ if (!tag || !end) continue; if (tag >= end) break; *end = '\0'; if (*tag == '-') { dprint(1, (debugfile, "nm: remove tag: '%s'\n", tag + 1)); notmuch_message_remove_tag(msg, tag + 1); } else { dprint(1, (debugfile, "nm: add tag: '%s'\n", *tag == '+' ? tag + 1 : tag)); notmuch_message_add_tag(msg, *tag == '+' ? tag + 1 : tag); } end = tag = NULL; } notmuch_message_thaw(msg); FREE(&buf); return 0; }
int notmuch_tag_command (void *ctx, unused (int argc), unused (char *argv[])) { int *add_tags, *remove_tags; int add_tags_count = 0; int remove_tags_count = 0; char *query_string; notmuch_config_t *config; notmuch_database_t *notmuch; notmuch_query_t *query; notmuch_messages_t *messages; notmuch_message_t *message; struct sigaction action; int i; /* Setup our handler for SIGINT */ memset (&action, 0, sizeof (struct sigaction)); action.sa_handler = handle_sigint; sigemptyset (&action.sa_mask); action.sa_flags = SA_RESTART; sigaction (SIGINT, &action, NULL); add_tags = talloc_size (ctx, argc * sizeof (int)); if (add_tags == NULL) { fprintf (stderr, "Out of memory.\n"); return 1; } remove_tags = talloc_size (ctx, argc * sizeof (int)); if (remove_tags == NULL) { fprintf (stderr, "Out of memory.\n"); return 1; } 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"); return 1; } query_string = query_string_from_args (ctx, argc - i, &argv[i]); if (*query_string == '\0') { fprintf (stderr, "Error: notmuch tag requires at least one search term.\n"); return 1; } config = notmuch_config_open (ctx, NULL, NULL); if (config == NULL) return 1; notmuch = notmuch_database_open (notmuch_config_get_database_path (config), NOTMUCH_DATABASE_MODE_READ_WRITE); if (notmuch == NULL) return 1; query = notmuch_query_create (notmuch, query_string); if (query == NULL) { fprintf (stderr, "Out of memory.\n"); return 1; } /* tagging is not interested in any special sort order */ notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED); for (messages = notmuch_query_search_messages (query); notmuch_messages_valid (messages) && !interrupted; notmuch_messages_move_to_next (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); notmuch_database_close (notmuch); return interrupted; }
/* Examine 'path' recursively as follows: * * o Ask the filesystem for the mtime of 'path' (fs_mtime) * o Ask the database for its timestamp of 'path' (db_mtime) * * o Ask the filesystem for files and directories within 'path' * (via scandir and stored in fs_entries) * * o Pass 1: For each directory in fs_entries, recursively call into * this same function. * * o Compare fs_mtime to db_mtime. If they are equivalent, terminate * the algorithm at this point, (this directory has not been * updated in the filesystem since the last database scan of PASS * 2). * * o Ask the database for files and directories within 'path' * (db_files and db_subdirs) * * o Pass 2: Walk fs_entries simultaneously with db_files and * db_subdirs. Look for one of three interesting cases: * * 1. Regular file in fs_entries and not in db_files * This is a new file to add_message into the database. * * 2. Filename in db_files not in fs_entries. * This is a file that has been removed from the mail store. * * 3. Directory in db_subdirs not in fs_entries * This is a directory that has been removed from the mail store. * * Note that the addition of a directory is not interesting here, * since that will have been taken care of in pass 1. Also, we * don't immediately act on file/directory removal since we must * ensure that in the case of a rename that the new filename is * added before the old filename is removed, (so that no * information is lost from the database). * * o Tell the database to update its time of 'path' to 'fs_mtime' * if fs_mtime isn't the current wall-clock time. */ static notmuch_status_t add_files_recursive (notmuch_database_t *notmuch, const char *path, add_files_state_t *state) { DIR *dir = NULL; struct dirent *entry = NULL; char *next = NULL; time_t fs_mtime, db_mtime; notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS; notmuch_message_t *message = NULL; struct dirent **fs_entries = NULL; int i, num_fs_entries; notmuch_directory_t *directory; notmuch_filenames_t *db_files = NULL; notmuch_filenames_t *db_subdirs = NULL; time_t stat_time; struct stat st; notmuch_bool_t is_maildir, new_directory; const char **tag; if (stat (path, &st)) { fprintf (stderr, "Error reading directory %s: %s\n", path, strerror (errno)); return NOTMUCH_STATUS_FILE_ERROR; } stat_time = time (NULL); /* This is not an error since we may have recursed based on a * symlink to a regular file, not a directory, and we don't know * that until this stat. */ if (! S_ISDIR (st.st_mode)) return NOTMUCH_STATUS_SUCCESS; fs_mtime = st.st_mtime; directory = notmuch_database_get_directory (notmuch, path); db_mtime = notmuch_directory_get_mtime (directory); new_directory = db_mtime ? FALSE : TRUE; /* XXX This is a temporary workaround. If we don't update the * database mtime until after processing messages in this * directory, then a 0 mtime is *not* sufficient to indicate that * this directory has no messages or subdirs in the database (for * example, if an earlier run skipped the mtime update because * fs_mtime == stat_time, or was interrupted before updating the * mtime at the end). To address this, we record a (bogus) * non-zero value before processing any child messages so that a * later run won't mistake this for a new directory (and, for * example, fail to detect removed files and subdirs). * * A better solution would be for notmuch_database_get_directory * to indicate if it really created a new directory or not, either * by a new out-argument, or by recording this information and * providing an accessor. */ if (new_directory) notmuch_directory_set_mtime (directory, -1); /* If the database knows about this directory, then we sort based * on strcmp to match the database sorting. Otherwise, we can do * inode-based sorting for faster filesystem operation. */ num_fs_entries = scandir (path, &fs_entries, 0, new_directory ? dirent_sort_inode : dirent_sort_strcmp_name); if (num_fs_entries == -1) { fprintf (stderr, "Error opening directory %s: %s\n", path, strerror (errno)); ret = NOTMUCH_STATUS_FILE_ERROR; goto DONE; } /* Pass 1: Recurse into all sub-directories. */ is_maildir = _entries_resemble_maildir (fs_entries, num_fs_entries); for (i = 0; i < num_fs_entries; i++) { if (interrupted) break; entry = fs_entries[i]; /* We only want to descend into directories. * But symlinks can be to directories too, of course. * * And if the filesystem doesn't tell us the file type in the * scandir results, then it might be a directory (and if not, * then we'll stat and return immediately in the next level of * recursion). */ if (entry->d_type != DT_DIR && entry->d_type != DT_LNK && entry->d_type != DT_UNKNOWN) { continue; } /* Ignore special directories to avoid infinite recursion. * Also ignore the .notmuch directory and any "tmp" directory * that appears within a maildir. */ /* XXX: Eventually we'll want more sophistication to let the * user specify files to be ignored. */ if (strcmp (entry->d_name, ".") == 0 || strcmp (entry->d_name, "..") == 0 || (is_maildir && strcmp (entry->d_name, "tmp") == 0) || strcmp (entry->d_name, ".notmuch") ==0) { continue; } next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name); status = add_files_recursive (notmuch, next, state); if (status && ret == NOTMUCH_STATUS_SUCCESS) ret = status; talloc_free (next); next = NULL; } /* If the directory's modification time in the filesystem is the * same as what we recorded in the database the last time we * scanned it, then we can skip the second pass entirely. * * We test for strict equality here to avoid a bug that can happen * if the system clock jumps backward, (preventing new mail from * being discovered until the clock catches up and the directory * is modified again). */ if (fs_mtime == db_mtime) goto DONE; /* new_directory means a directory that the database has never * seen before. In that case, we can simply leave db_files and * db_subdirs NULL. */ if (!new_directory) { db_files = notmuch_directory_get_child_files (directory); db_subdirs = notmuch_directory_get_child_directories (directory); } /* Pass 2: Scan for new files, removed files, and removed directories. */ for (i = 0; i < num_fs_entries; i++) { if (interrupted) break; entry = fs_entries[i]; /* Check if we've walked past any names in db_files or * db_subdirs. If so, these have been deleted. */ while (notmuch_filenames_valid (db_files) && strcmp (notmuch_filenames_get (db_files), entry->d_name) < 0) { char *absolute = talloc_asprintf (state->removed_files, "%s/%s", path, notmuch_filenames_get (db_files)); _filename_list_add (state->removed_files, absolute); notmuch_filenames_move_to_next (db_files); } while (notmuch_filenames_valid (db_subdirs) && strcmp (notmuch_filenames_get (db_subdirs), entry->d_name) <= 0) { const char *filename = notmuch_filenames_get (db_subdirs); if (strcmp (filename, entry->d_name) < 0) { char *absolute = talloc_asprintf (state->removed_directories, "%s/%s", path, filename); _filename_list_add (state->removed_directories, absolute); } notmuch_filenames_move_to_next (db_subdirs); } /* If we're looking at a symlink, we only want to add it if it * links to a regular file, (and not to a directory, say). * * Similarly, if the file is of unknown type (due to filesystem * limitations), then we also need to look closer. * * In either case, a stat does the trick. */ if (entry->d_type == DT_LNK || entry->d_type == DT_UNKNOWN) { int err; next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name); err = stat (next, &st); talloc_free (next); next = NULL; /* Don't emit an error for a link pointing nowhere, since * the directory-traversal pass will have already done * that. */ if (err) continue; if (! S_ISREG (st.st_mode)) continue; } else if (entry->d_type != DT_REG) { continue; } /* Don't add a file that we've added before. */ if (notmuch_filenames_valid (db_files) && strcmp (notmuch_filenames_get (db_files), entry->d_name) == 0) { notmuch_filenames_move_to_next (db_files); continue; } /* We're now looking at a regular file that doesn't yet exist * in the database, so add it. */ next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name); state->processed_files++; if (state->verbose) { if (state->output_is_a_tty) printf("\r\033[K"); printf ("%i/%i: %s", state->processed_files, state->total_files, next); putchar((state->output_is_a_tty) ? '\r' : '\n'); fflush (stdout); } status = notmuch_database_add_message (notmuch, next, &message); switch (status) { /* success */ case NOTMUCH_STATUS_SUCCESS: state->added_messages++; notmuch_message_freeze (message); for (tag=state->new_tags; *tag != NULL; tag++) notmuch_message_add_tag (message, *tag); if (state->synchronize_flags == TRUE) notmuch_message_maildir_flags_to_tags (message); notmuch_message_thaw (message); break; /* Non-fatal issues (go on to next file) */ case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: /* Defer sync of maildir flags until after old filenames * are removed in the case of a rename. */ if (state->synchronize_flags == TRUE) _filename_list_add (state->message_ids_to_sync, notmuch_message_get_message_id (message)); 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_READ_ONLY_DATABASE: case NOTMUCH_STATUS_XAPIAN_EXCEPTION: case NOTMUCH_STATUS_OUT_OF_MEMORY: fprintf (stderr, "Error: %s. Halting processing.\n", notmuch_status_to_string (status)); ret = status; goto DONE; default: case NOTMUCH_STATUS_FILE_ERROR: case NOTMUCH_STATUS_NULL_POINTER: case NOTMUCH_STATUS_TAG_TOO_LONG: case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: case NOTMUCH_STATUS_LAST_STATUS: INTERNAL_ERROR ("add_message returned unexpected value: %d", status); goto DONE; } if (message) { notmuch_message_destroy (message); message = NULL; } if (do_print_progress) { do_print_progress = 0; generic_print_progress ("Processed", "files", state->tv_start, state->processed_files, state->total_files); } talloc_free (next); next = NULL; } if (interrupted) goto DONE; /* Now that we've walked the whole filesystem list, anything left * over in the database lists has been deleted. */ while (notmuch_filenames_valid (db_files)) { char *absolute = talloc_asprintf (state->removed_files, "%s/%s", path, notmuch_filenames_get (db_files)); _filename_list_add (state->removed_files, absolute); notmuch_filenames_move_to_next (db_files); } while (notmuch_filenames_valid (db_subdirs)) { char *absolute = talloc_asprintf (state->removed_directories, "%s/%s", path, notmuch_filenames_get (db_subdirs)); _filename_list_add (state->removed_directories, absolute); notmuch_filenames_move_to_next (db_subdirs); } /* If the directory's mtime is the same as the wall-clock time * when we stat'ed the directory, we skip updating the mtime in * the database because a message could be delivered later in this * same second. This may lead to unnecessary re-scans, but it * avoids overlooking messages. */ if (! interrupted && fs_mtime != stat_time) { status = notmuch_directory_set_mtime (directory, fs_mtime); if (status && ret == NOTMUCH_STATUS_SUCCESS) ret = status; } DONE: if (next) talloc_free (next); if (entry) free (entry); if (dir) closedir (dir); if (fs_entries) free (fs_entries); if (db_subdirs) notmuch_filenames_destroy (db_subdirs); if (db_files) notmuch_filenames_destroy (db_files); if (directory) notmuch_directory_destroy (directory); return ret; }
static int tag_message (notmuch_database_t *notmuch, const char *message_id, char *file_tags, notmuch_bool_t remove_all, notmuch_bool_t synchronize_flags) { notmuch_status_t status; notmuch_tags_t *db_tags; char *db_tags_str; notmuch_message_t *message = NULL; const char *tag; char *next; int ret = 0; status = notmuch_database_find_message (notmuch, message_id, &message); if (status || message == NULL) { fprintf (stderr, "Warning: Cannot apply tags to %smessage: %s\n", message ? "" : "missing ", message_id); if (status) fprintf (stderr, "%s\n", notmuch_status_to_string(status)); return 1; } /* In order to detect missing messages, this check/optimization is * intentionally done *after* first finding the message. */ if (!remove_all && (file_tags == NULL || *file_tags == '\0')) goto DONE; db_tags_str = NULL; for (db_tags = notmuch_message_get_tags (message); notmuch_tags_valid (db_tags); notmuch_tags_move_to_next (db_tags)) { tag = notmuch_tags_get (db_tags); if (db_tags_str) db_tags_str = talloc_asprintf_append (db_tags_str, " %s", tag); else db_tags_str = talloc_strdup (message, tag); } if (((file_tags == NULL || *file_tags == '\0') && (db_tags_str == NULL || *db_tags_str == '\0')) || (file_tags && db_tags_str && strcmp (file_tags, db_tags_str) == 0)) goto DONE; notmuch_message_freeze (message); if (remove_all) notmuch_message_remove_all_tags (message); next = file_tags; while (next) { tag = strsep (&next, " "); if (*tag == '\0') continue; 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)); ret = 1; } } notmuch_message_thaw (message); if (synchronize_flags) notmuch_message_tags_to_maildir_flags (message); DONE: if (message) notmuch_message_destroy (message); return ret; }
notmuch_status_t tag_op_list_apply (notmuch_message_t *message, tag_op_list_t *list, tag_op_flag_t flags) { size_t i; notmuch_status_t status = 0; tag_operation_t *tag_ops = list->ops; if (! (flags & TAG_FLAG_PRE_OPTIMIZED) && ! makes_changes (message, list, flags)) return NOTMUCH_STATUS_SUCCESS; status = notmuch_message_freeze (message); if (status) { message_error (message, status, "freezing message"); return status; } if (flags & TAG_FLAG_REMOVE_ALL) { status = notmuch_message_remove_all_tags (message); if (status) { message_error (message, status, "removing all tags"); return status; } } for (i = 0; i < list->count; i++) { if (tag_ops[i].remove) { status = notmuch_message_remove_tag (message, tag_ops[i].tag); if (status) { message_error (message, status, "removing tag %s", tag_ops[i].tag); return status; } } else { status = notmuch_message_add_tag (message, tag_ops[i].tag); if (status) { message_error (message, status, "adding tag %s", tag_ops[i].tag); return status; } } } status = notmuch_message_thaw (message); if (status) { message_error (message, status, "thawing message"); return status; } if (flags & TAG_FLAG_MAILDIR_SYNC) { status = notmuch_message_tags_to_maildir_flags (message); if (status) { message_error (message, status, "synching tags to maildir"); return status; } } return NOTMUCH_STATUS_SUCCESS; }