]> git.notmuchmail.org Git - notmuch/blobdiff - contrib/notmuch-mutt/notmuch-mutt
emacs: Add new option notmuch-search-hide-excluded
[notmuch] / contrib / notmuch-mutt / notmuch-mutt
index 9176ed514a3855bef157b3476016d048b3661bbc..b81252c809a8e3ed2fae56fd8e2bb14a6e56ed09 100755 (executable)
@@ -1,8 +1,8 @@
-#!/usr/bin/perl -w
+#!/usr/bin/env perl
 #
 # notmuch-mutt - notmuch (of a) helper for Mutt
 #
-# Copyright: © 2011-2012 Stefano Zacchiroli <zack@upsilon.cc> 
+# Copyright: © 2011-2015 Stefano Zacchiroli <zack@upsilon.cc>
 # License: GNU General Public License (GPL), version 3 or above
 #
 # See the bottom of this file for more documentation.
@@ -12,21 +12,67 @@ use strict;
 use warnings;
 
 use File::Path;
+use File::Basename;
+use File::Find;
 use Getopt::Long qw(:config no_getopt_compat);
-use Mail::Internet;
+use Mail::Header;
 use Mail::Box::Maildir;
 use Pod::Usage;
-use String::ShellQuote;
 use Term::ReadLine;
+use Digest::SHA;
 
 
 my $xdg_cache_dir = "$ENV{HOME}/.cache";
 $xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME};
 my $cache_dir = "$xdg_cache_dir/notmuch/mutt";
 
+sub die_dir($$) {
+    my ($maildir, $error) = @_;
+    die "notmuch-mutt: search cache maildir $maildir $error\n".
+        "Please ensure that the notmuch-mutt search cache Maildir\n".
+        "contains no subfolders or real mail data, only symlinks to mail\n";
+}
+
+sub die_subdir($$$) {
+    my ($maildir, $subdir, $error) = @_;
+    die_dir($maildir, "subdir $subdir $error");
+}
+
+# check that the search cache maildir is that and not a real maildir
+# otherwise there could be data loss when the search cache is emptied
+sub check_search_cache_maildir($) {
+    my ($maildir) = (@_);
+
+    return unless -e $maildir;
+
+    -d $maildir or die_dir($maildir, 'is not a directory');
+
+    opendir(my $mdh, $maildir) or die_dir($maildir, "cannot be opened: $!");
+    my @contents = grep { !/^\.\.?$/ } readdir $mdh;
+    closedir $mdh;
+
+    my @required = ('cur', 'new', 'tmp');
+    foreach my $d (@required) {
+        -l "$maildir/$d" and die_dir($maildir, "contains symlink $d");
+        -e "$maildir/$d" or die_subdir($maildir, $d, 'is missing');
+        -d "$maildir/$d" or die_subdir($maildir, $d, 'is not a directory');
+        find(sub {
+            $_ eq '.' and return;
+            $_ eq '..' and return;
+            -l $_ or die_subdir($maildir, $d, "contains non-symlink $_");
+        }, "$maildir/$d");
+    }
+
+    my %required = map { $_ => 1 } @required;
+    foreach my $d (@contents) {
+        -l "$maildir/$d" and die_dir( $maildir, "contains symlink $d");
+        -d "$maildir/$d" or die_dir( $maildir, "contains non-directory $d");
+        exists($required{$d}) or die_dir( $maildir, "contains directory $d");
+    }
+}
 
-# create an empty maildir (if missing) or empty an existing maildir"
-sub empty_maildir($) {
+# create an empty search cache maildir (if missing) or empty existing one
+sub empty_search_cache_maildir($) {
     my ($maildir) = (@_);
     rmtree($maildir) if (-d $maildir);
     my $folder = new Mail::Box::Maildir(folder => $maildir,
@@ -34,16 +80,24 @@ sub empty_maildir($) {
     $folder->close();
 }
 
-# search($maildir, $query)
+# search($maildir, $remove_dups, $query)
 # search mails according to $query with notmuch; store results in $maildir
-sub search($$) {
-    my ($maildir, $query) = @_;
-    $query = shell_quote($query);
-
-    empty_maildir($maildir);
-    system("notmuch search --output=files $query"
-          . " | sed -e 's: :\\\\ :g'"
-          . " | xargs --no-run-if-empty ln -s -t $maildir/cur/");
+sub search($$$) {
+    my ($maildir, $remove_dups, $query) = @_;
+    my $dup_option = "";
+
+    my @args = qw/notmuch search --output=files/;
+    push @args, "--duplicate=1" if $remove_dups;
+    push @args, $query;
+
+    check_search_cache_maildir($maildir);
+    empty_search_cache_maildir($maildir);
+    open my $pipe, '-|', @args or die "Running @args failed: $!\n";
+    while (<$pipe>) {
+       chomp;
+       my $ln = "$maildir/cur/" . basename $_;
+       symlink $_, "$ln" or warn "Failed to symlink '$_', '$ln': $!\n";
+    }
 }
 
 sub prompt($$) {
@@ -60,7 +114,7 @@ sub prompt($$) {
     while (1) {
        chomp($query = $term->readline($text, $default));
        if ($query eq "?") {
-           system("man", "notmuch");
+           system("man", "notmuch-search-terms");
        } else {
            $term->WriteHistory($histfile);
            return $query;
@@ -69,41 +123,66 @@ sub prompt($$) {
 }
 
 sub get_message_id() {
-    my $mail = Mail::Internet->new(\*STDIN);
-    $mail->head->get("message-id") =~ /^<(.*)>$/;      # get message-id
-    return $1;
+    my $mid = undef;
+    my @headers = ();
+
+    while (<STDIN>) {  # collect header lines in @headers
+       push(@headers, $_);
+       last if $_ =~ /^$/;
+    }
+    my $head = Mail::Header->new(\@headers);
+    $mid = $head->get("message-id") or undef;
+
+    if ($mid) {  # Message-ID header found
+       $mid =~ /^<(.*)>$/;  # extract message id
+       $mid = $1;
+    } else {  # Message-ID header not found, synthesize a message id
+             # based on SHA1, as notmuch would do.  See:
+             # https://git.notmuchmail.org/git/notmuch/blob/HEAD:/lib/sha1.c
+       my $sha = Digest::SHA->new(1);
+       $sha->add($_) foreach(@headers);
+       $sha->addfile(\*STDIN);
+       $mid = 'notmuch-sha1-' . $sha->hexdigest;
+    }
+
+    return $mid;
 }
 
-sub search_action($$@) {
-    my ($interactive, $results_dir, @params) = @_;
+sub search_action($$$@) {
+    my ($interactive, $results_dir, $remove_dups, @params) = @_;
 
     if (! $interactive) {
-       search($results_dir, join(' ', @params));
+       search($results_dir, $remove_dups, join(' ', @params));
     } else {
        my $query = prompt("search ('?' for man): ", join(' ', @params));
        if ($query ne "") {
-           search($results_dir,$query);
+           search($results_dir, $remove_dups, $query);
        }
     }
 }
 
-sub thread_action(@) {
-    my ($results_dir, @params) = @_;
+sub thread_action($$@) {
+    my ($results_dir, $remove_dups, @params) = @_;
 
     my $mid = get_message_id();
-    my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid");
-    my $tid = `$search_cmd`;   # get thread id
-    chomp($tid);
+    if (! defined $mid) {
+       die "notmuch-mutt: cannot find Message-Id, abort.\n";
+    }
+
+    $mid =~ s/ //g; # notmuch strips spaces before storing Message-Id
+    $mid =~ s/"/""""/g; # escape all double quote characters twice
 
-    search($results_dir, $tid);
+    search($results_dir, $remove_dups, qq{thread:"{id:""$mid""}"});
 }
 
 sub tag_action(@) {
     my $mid = get_message_id();
+    defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n";
 
-    system("notmuch tag "
-          . shell_quote(join(' ', @_))
-          . " id:$mid");
+    $mid =~ s/ //g; # notmuch strips spaces before storing Message-Id
+    $mid =~ s/"/""/g; # escape all double quote characters
+
+    system("notmuch", "tag", @_, "--", qq{id:"$mid"});
 }
 
 sub die_usage() {
@@ -118,11 +197,13 @@ sub main() {
     my $results_dir = "$cache_dir/results";
     my $interactive = 0;
     my $help_needed = 0;
+    my $remove_dups = 0;
 
     my $getopt = GetOptions(
        "h|help" => \$help_needed,
        "o|output-dir=s" => \$results_dir,
-       "p|prompt" => \$interactive);
+       "p|prompt" => \$interactive,
+       "r|remove-dups" => \$remove_dups);
     if (! $getopt || $#ARGV < 0) { die_usage() };
     my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
 
@@ -136,9 +217,9 @@ sub main() {
        print STDERR "Error: no search term provided\n\n";
        die_usage();
     } elsif ($action eq "search") {
-       search_action($interactive, $results_dir, @params);
+       search_action($interactive, $results_dir, $remove_dups, @params);
     } elsif ($action eq "thread") {
-       thread_action($results_dir, @params);
+       thread_action($results_dir, $remove_dups, @params);
     } elsif ($action eq "tag") {
        tag_action(@params);
     } else {
@@ -169,7 +250,7 @@ notmuch-mutt - notmuch (of a) helper for Mutt
 =head1 DESCRIPTION
 
 notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
-maildir with search results.
+maildir with search results.
 
 =head1 OPTIONS
 
@@ -189,6 +270,15 @@ be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
 Instead of using command line search terms, prompt the user for them (only for
 "search").
 
+=item -r
+
+=item --remove-dups
+
+Remove emails with duplicate message-ids from search results.  (Passes
+--duplicate=1 to notmuch search command.)  Note this can hide search
+results if an email accidentally or maliciously uses the same message-id
+as a different email.
+
 =item -h
 
 =item --help
@@ -205,13 +295,23 @@ the following in your Mutt configuration (usually one of: F<~/.muttrc>,
 F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
 
     macro index <F8> \
-          "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt --prompt search<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter>" \
+    "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+    <shell-escape>notmuch-mutt -r --prompt search<enter>\
+    <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
+    <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
           "notmuch: search mail"
+
     macro index <F9> \
-          "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt thread<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter><enter-command>set wait_key<enter>" \
+    "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+    <pipe-message>notmuch-mutt -r thread<enter>\
+    <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
+    <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
           "notmuch: reconstruct thread"
+
     macro index <F6> \
-          "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -inbox<enter>" \
+    "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
+    <pipe-message>notmuch-mutt tag -- -inbox<enter>\
+    <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
           "notmuch: remove message from inbox"
 
 The first macro (activated by <F8>) prompts the user for notmuch search terms