3 # notmuch-mutt - notmuch (of a) helper for Mutt
5 # Copyright: © 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
6 # License: GNU General Public License (GPL), version 3 or above
8 # See the bottom of this file for more documentation.
9 # A manpage can be obtained by running "pod2man notmuch-mutt > notmuch-mutt.1"
15 use Getopt::Long qw(:config no_getopt_compat);
17 use Mail::Box::Maildir;
19 use String::ShellQuote;
25 my $xdg_cache_dir = "$ENV{HOME}/.cache";
26 $xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME};
27 my $cache_dir = "$xdg_cache_dir/notmuch/mutt";
30 # create an empty maildir (if missing) or empty an existing maildir"
31 sub empty_maildir($) {
33 rmtree($maildir) if (-d $maildir);
34 my $folder = new Mail::Box::Maildir(folder => $maildir,
39 # Match files by size and SHA-256; then delete duplicates
40 sub builtin_remove_dups($) {
42 my (%size_to_files, %sha_to_files);
44 # Group files by matching sizes
45 foreach my $file (glob("$maildir/cur/*")) {
47 push(@{$size_to_files{$size}}, $file) if $size;
50 foreach my $same_size_files (values %size_to_files) {
51 # Don't run sha unless there is another file of the same size
52 next if scalar(@$same_size_files) < 2;
55 # Group files with matching sizes by SHA-256
56 foreach my $file (@$same_size_files) {
57 open(my $fh, '<', $file) or next;
59 my $sha256hash = Digest::SHA->new(256)->addfile($fh)->hexdigest;
62 push(@{$sha_to_files{$sha256hash}}, $file);
66 foreach my $same_sha_files (values %sha_to_files) {
67 next if scalar(@$same_sha_files) < 2;
68 unlink(@{$same_sha_files}[1..$#$same_sha_files]);
73 # Use either fdupes or the built-in scanner to detect and remove duplicate
74 # search results in the maildir
75 sub remove_duplicates($) {
78 my $fdupes = which("fdupes");
80 system("$fdupes --hardlinks --symlinks --delete --noprompt"
81 . " --quiet $maildir/cur/ > /dev/null");
83 builtin_remove_dups($maildir);
87 # search($maildir, $remove_dups, $query)
88 # search mails according to $query with notmuch; store results in $maildir
90 my ($maildir, $remove_dups, $query) = @_;
91 $query = shell_quote($query);
93 empty_maildir($maildir);
94 system("notmuch search --output=files $query"
95 . " | sed -e 's: :\\\\ :g'"
96 . " | xargs --no-run-if-empty ln -s -t $maildir/cur/");
97 remove_duplicates($maildir) if ($remove_dups);
101 my ($text, $default) = @_;
103 my $term = Term::ReadLine->new( "notmuch-mutt" );
104 my $histfile = "$cache_dir/history";
106 $term->ornaments( 0 );
107 $term->unbind_key( ord( "\t" ) );
109 $histfile = $ENV{MUTT_NOTMUCH_HISTFILE} if $ENV{MUTT_NOTMUCH_HISTFILE};
110 $term->ReadHistory($histfile) if (-r $histfile);
112 chomp($query = $term->readline($text, $default));
114 system("man", "notmuch-search-terms");
116 $term->WriteHistory($histfile);
122 sub get_message_id() {
123 my $mail = Mail::Internet->new(\*STDIN);
124 my $mid = $mail->head->get("message-id") or return undef;
125 $mid =~ /^<(.*)>$/; # get message-id value
129 sub search_action($$$@) {
130 my ($interactive, $results_dir, $remove_dups, @params) = @_;
132 if (! $interactive) {
133 search($results_dir, $remove_dups, join(' ', @params));
135 my $query = prompt("search ('?' for man): ", join(' ', @params));
137 search($results_dir, $remove_dups, $query);
142 sub thread_action($$@) {
143 my ($results_dir, $remove_dups, @params) = @_;
145 my $mid = get_message_id();
146 if (! defined $mid) {
147 empty_maildir($results_dir);
148 die "notmuch-mutt: cannot find Message-Id, abort.\n";
150 my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid");
151 my $tid = `$search_cmd`; # get thread id
154 search($results_dir, $remove_dups, $tid);
158 my $mid = get_message_id();
159 defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n";
161 system("notmuch tag "
162 . shell_quote(join(' ', @_))
167 my %podflags = ( "verbose" => 1,
169 pod2usage(%podflags);
173 mkpath($cache_dir) unless (-d $cache_dir);
175 my $results_dir = "$cache_dir/results";
180 my $getopt = GetOptions(
181 "h|help" => \$help_needed,
182 "o|output-dir=s" => \$results_dir,
183 "p|prompt" => \$interactive,
184 "r|remove-dups" => \$remove_dups);
185 if (! $getopt || $#ARGV < 0) { die_usage() };
186 my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
188 foreach my $param (@params) {
189 $param =~ s/folder:=/folder:/g;
194 } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
195 print STDERR "Error: no search term provided\n\n";
197 } elsif ($action eq "search") {
198 search_action($interactive, $results_dir, $remove_dups, @params);
199 } elsif ($action eq "thread") {
200 thread_action($results_dir, $remove_dups, @params);
201 } elsif ($action eq "tag") {
214 notmuch-mutt - notmuch (of a) helper for Mutt
220 =item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]...
222 =item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL>
224 =item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL>
230 notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
231 a maildir with search results.
239 =item --output-dir DIR
241 Store search results as (symlink) messages under maildir DIR. Beware: DIR will
242 be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
248 Instead of using command line search terms, prompt the user for them (only for
255 Remove duplicates from search results.
261 Show usage information and exit.
265 =head1 INTEGRATION WITH MUTT
267 notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent
268 (unsurprisingly, given the name). To that end, you should define macros like
269 the following in your Mutt configuration (usually one of: F<~/.muttrc>,
270 F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
273 "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
274 <shell-escape>notmuch-mutt -r --prompt search<enter>\
275 <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
276 <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
277 "notmuch: search mail"
280 "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
281 <pipe-message>notmuch-mutt -r thread<enter>\
282 <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
283 <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
284 "notmuch: reconstruct thread"
287 "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
288 <pipe-message>notmuch-mutt tag -- -inbox<enter>\
289 <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
290 "notmuch: remove message from inbox"
292 The first macro (activated by <F8>) prompts the user for notmuch search terms
293 and then jump to a temporary maildir showing search results. The second macro
294 (activated by <F9>) reconstructs the thread corresponding to the current mail
295 and show it as search results. The third macro (activated by <F6>) removes the
296 tag C<inbox> from the current message; by changing C<-inbox> this macro may be
297 customised to add or remove tags appropriate to the users notmuch work-flow.
299 To keep notmuch index current you should then periodically run C<notmuch
300 new>. Depending on your local mail setup, you might want to do that via cron,
301 as a hook triggered by mail retrieval, etc.
309 Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
311 License: GNU General Public License (GPL), version 3 or higher