notmuch-mutt (8396B)
1 #!/usr/bin/env perl 2 # 3 # notmuch-mutt - notmuch (of a) helper for Mutt 4 # 5 # Copyright: © 2011-2015 Stefano Zacchiroli <zack@upsilon.cc> 6 # License: GNU General Public License (GPL), version 3 or above 7 # 8 # See the bottom of this file for more documentation. 9 # A manpage can be obtained by running "pod2man notmuch-mutt > notmuch-mutt.1" 10 11 use strict; 12 use warnings; 13 14 use File::Path; 15 use Getopt::Long qw(:config no_getopt_compat); 16 use Mail::Header; 17 use Mail::Box::Maildir; 18 use Pod::Usage; 19 use String::ShellQuote; 20 use Term::ReadLine; 21 use Digest::SHA; 22 23 24 my $xdg_cache_dir = "$ENV{HOME}/.cache"; 25 $xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME}; 26 my $cache_dir = "$xdg_cache_dir/notmuch/mutt"; 27 28 29 # create an empty maildir (if missing) or empty an existing maildir" 30 sub empty_maildir($) { 31 my ($maildir) = (@_); 32 rmtree($maildir) if (-d $maildir); 33 my $folder = new Mail::Box::Maildir(folder => $maildir, 34 create => 1); 35 $folder->close(); 36 } 37 38 # search($maildir, $remove_dups, $query) 39 # search mails according to $query with notmuch; store results in $maildir 40 sub search($$$) { 41 my ($maildir, $remove_dups, $query) = @_; 42 my $dup_option = ""; 43 44 $query = shell_quote($query); 45 46 if ($remove_dups) { 47 $dup_option = "--duplicate=1"; 48 } 49 50 empty_maildir($maildir); 51 #system("notmuch search --output=files $dup_option $query" 52 #. " | sed -e 's: :\\\\ :g'" 53 #. " | xargs -r -I searchoutput ln -s searchoutput $maildir/cur/"); 54 system("notmuch search --output=files $dup_option $query" 55 . " | sed -e 's: :\\\\ :g'" 56 . " | xargs -I searchoutput ln -s searchoutput $maildir/cur/"); 57 } 58 59 sub prompt($$) { 60 my ($text, $default) = @_; 61 my $query = ""; 62 my $term = Term::ReadLine->new( "notmuch-mutt" ); 63 my $histfile = "$cache_dir/history"; 64 65 $term->ornaments( 0 ); 66 $term->unbind_key( ord( "\t" ) ); 67 $term->MinLine( 3 ); 68 $histfile = $ENV{MUTT_NOTMUCH_HISTFILE} if $ENV{MUTT_NOTMUCH_HISTFILE}; 69 $term->ReadHistory($histfile) if (-r $histfile); 70 while (1) { 71 chomp($query = $term->readline($text, $default)); 72 if ($query eq "?") { 73 system("man", "notmuch-search-terms"); 74 } else { 75 $term->WriteHistory($histfile); 76 return $query; 77 } 78 } 79 } 80 81 sub get_message_id() { 82 my $mid = undef; 83 my @headers = (); 84 85 while (<STDIN>) { # collect header lines in @headers 86 push(@headers, $_); 87 last if $_ =~ /^$/; 88 } 89 my $head = Mail::Header->new(\@headers); 90 $mid = $head->get("message-id") or undef; 91 92 if ($mid) { # Message-ID header found 93 $mid =~ /^<(.*)>$/; # extract message id 94 $mid = $1; 95 } else { # Message-ID header not found, synthesize a message id 96 # based on SHA1, as notmuch would do. See: 97 # https://git.notmuchmail.org/git/notmuch/blob/HEAD:/lib/sha1.c 98 my $sha = Digest::SHA->new(1); 99 $sha->add($_) foreach(@headers); 100 $sha->addfile(\*STDIN); 101 $mid = 'notmuch-sha1-' . $sha->hexdigest; 102 } 103 104 return $mid; 105 } 106 107 sub search_action($$$@) { 108 my ($interactive, $results_dir, $remove_dups, @params) = @_; 109 110 if (! $interactive) { 111 search($results_dir, $remove_dups, join(' ', @params)); 112 } else { 113 my $query = prompt("search ('?' for man): ", join(' ', @params)); 114 if ($query ne "") { 115 search($results_dir, $remove_dups, $query); 116 } 117 } 118 } 119 120 sub thread_action($$@) { 121 my ($results_dir, $remove_dups, @params) = @_; 122 123 my $mid = get_message_id(); 124 if (! defined $mid) { 125 empty_maildir($results_dir); 126 die "notmuch-mutt: cannot find Message-Id, abort.\n"; 127 } 128 my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid"); 129 my $tid = `$search_cmd`; # get thread id 130 chomp($tid); 131 132 search($results_dir, $remove_dups, $tid); 133 } 134 135 sub tag_action(@) { 136 my $mid = get_message_id(); 137 defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n"; 138 139 system("notmuch", "tag", @_, "--", "id:$mid"); 140 } 141 142 sub die_usage() { 143 my %podflags = ( "verbose" => 1, 144 "exitval" => 2 ); 145 pod2usage(%podflags); 146 } 147 148 sub main() { 149 mkpath($cache_dir) unless (-d $cache_dir); 150 151 my $results_dir = "$cache_dir/results"; 152 my $interactive = 0; 153 my $help_needed = 0; 154 my $remove_dups = 0; 155 156 my $getopt = GetOptions( 157 "h|help" => \$help_needed, 158 "o|output-dir=s" => \$results_dir, 159 "p|prompt" => \$interactive, 160 "r|remove-dups" => \$remove_dups); 161 if (! $getopt || $#ARGV < 0) { die_usage() }; 162 my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]); 163 164 foreach my $param (@params) { 165 $param =~ s/folder:=/folder:/g; 166 } 167 168 if ($help_needed) { 169 die_usage(); 170 } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) { 171 print STDERR "Error: no search term provided\n\n"; 172 die_usage(); 173 } elsif ($action eq "search") { 174 search_action($interactive, $results_dir, $remove_dups, @params); 175 } elsif ($action eq "thread") { 176 thread_action($results_dir, $remove_dups, @params); 177 } elsif ($action eq "tag") { 178 tag_action(@params); 179 } else { 180 die_usage(); 181 } 182 } 183 184 main(); 185 186 __END__ 187 188 =head1 NAME 189 190 notmuch-mutt - notmuch (of a) helper for Mutt 191 192 =head1 SYNOPSIS 193 194 =over 195 196 =item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]... 197 198 =item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL> 199 200 =item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL> 201 202 =back 203 204 =head1 DESCRIPTION 205 206 notmuch-mutt is a frontend to the notmuch mail indexer capable of populating 207 a maildir with search results. 208 209 =head1 OPTIONS 210 211 =over 4 212 213 =item -o DIR 214 215 =item --output-dir DIR 216 217 Store search results as (symlink) messages under maildir DIR. Beware: DIR will 218 be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>) 219 220 =item -p 221 222 =item --prompt 223 224 Instead of using command line search terms, prompt the user for them (only for 225 "search"). 226 227 =item -r 228 229 =item --remove-dups 230 231 Remove emails with duplicate message-ids from search results. (Passes 232 --duplicate=1 to notmuch search command.) Note this can hide search 233 results if an email accidentally or maliciously uses the same message-id 234 as a different email. 235 236 =item -h 237 238 =item --help 239 240 Show usage information and exit. 241 242 =back 243 244 =head1 INTEGRATION WITH MUTT 245 246 notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent 247 (unsurprisingly, given the name). To that end, you should define macros like 248 the following in your Mutt configuration (usually one of: F<~/.muttrc>, 249 F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>): 250 251 macro index <F8> \ 252 "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ 253 <shell-escape>notmuch-mutt -r --prompt search<enter>\ 254 <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\ 255 <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ 256 "notmuch: search mail" 257 258 macro index <F9> \ 259 "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ 260 <pipe-message>notmuch-mutt -r thread<enter>\ 261 <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\ 262 <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ 263 "notmuch: reconstruct thread" 264 265 macro index <F6> \ 266 "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\ 267 <pipe-message>notmuch-mutt tag -- -inbox<enter>\ 268 <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \ 269 "notmuch: remove message from inbox" 270 271 The first macro (activated by <F8>) prompts the user for notmuch search terms 272 and then jump to a temporary maildir showing search results. The second macro 273 (activated by <F9>) reconstructs the thread corresponding to the current mail 274 and show it as search results. The third macro (activated by <F6>) removes the 275 tag C<inbox> from the current message; by changing C<-inbox> this macro may be 276 customised to add or remove tags appropriate to the users notmuch work-flow. 277 278 To keep notmuch index current you should then periodically run C<notmuch 279 new>. Depending on your local mail setup, you might want to do that via cron, 280 as a hook triggered by mail retrieval, etc. 281 282 =head1 SEE ALSO 283 284 mutt(1), notmuch(1) 285 286 =head1 AUTHOR 287 288 Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc> 289 290 License: GNU General Public License (GPL), version 3 or higher 291 292 =cut