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 $mail->head->get("message-id") =~ /^<(.*)>$/; # get message-id
128 sub search_action($$$@) {
129 my ($interactive, $results_dir, $remove_dups, @params) = @_;
131 if (! $interactive) {
132 search($results_dir, $remove_dups, join(' ', @params));
134 my $query = prompt("search ('?' for man): ", join(' ', @params));
136 search($results_dir, $remove_dups, $query);
141 sub thread_action($$@) {
142 my ($results_dir, $remove_dups, @params) = @_;
144 my $mid = get_message_id();
145 my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid");
146 my $tid = `$search_cmd`; # get thread id
149 search($results_dir, $remove_dups, $tid);
153 my $mid = get_message_id();
155 system("notmuch tag "
156 . shell_quote(join(' ', @_))
161 my %podflags = ( "verbose" => 1,
163 pod2usage(%podflags);
167 mkpath($cache_dir) unless (-d $cache_dir);
169 my $results_dir = "$cache_dir/results";
174 my $getopt = GetOptions(
175 "h|help" => \$help_needed,
176 "o|output-dir=s" => \$results_dir,
177 "p|prompt" => \$interactive,
178 "r|remove-dups" => \$remove_dups);
179 if (! $getopt || $#ARGV < 0) { die_usage() };
180 my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
182 foreach my $param (@params) {
183 $param =~ s/folder:=/folder:/g;
188 } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
189 print STDERR "Error: no search term provided\n\n";
191 } elsif ($action eq "search") {
192 search_action($interactive, $results_dir, $remove_dups, @params);
193 } elsif ($action eq "thread") {
194 thread_action($results_dir, $remove_dups, @params);
195 } elsif ($action eq "tag") {
208 notmuch-mutt - notmuch (of a) helper for Mutt
214 =item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]...
216 =item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL>
218 =item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL>
224 notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
225 a maildir with search results.
233 =item --output-dir DIR
235 Store search results as (symlink) messages under maildir DIR. Beware: DIR will
236 be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
242 Instead of using command line search terms, prompt the user for them (only for
249 Remove duplicates from search results.
255 Show usage information and exit.
259 =head1 INTEGRATION WITH MUTT
261 notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent
262 (unsurprisingly, given the name). To that end, you should define macros like
263 the following in your Mutt configuration (usually one of: F<~/.muttrc>,
264 F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
267 "<enter-command>unset wait_key<enter><shell-escape>notmuch-mutt -r --prompt search<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter>" \
268 "notmuch: search mail"
270 "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt -r thread<enter><change-folder-readonly>~/.cache/notmuch/mutt/results<enter><enter-command>set wait_key<enter>" \
271 "notmuch: reconstruct thread"
273 "<enter-command>unset wait_key<enter><pipe-message>notmuch-mutt tag -- -inbox<enter>" \
274 "notmuch: remove message from inbox"
276 The first macro (activated by <F8>) prompts the user for notmuch search terms
277 and then jump to a temporary maildir showing search results. The second macro
278 (activated by <F9>) reconstructs the thread corresponding to the current mail
279 and show it as search results. The third macro (activated by <F6>) removes the
280 tag C<inbox> from the current message; by changing C<-inbox> this macro may be
281 customised to add or remove tags appropriate to the users notmuch work-flow.
283 To keep notmuch index current you should then periodically run C<notmuch
284 new>. Depending on your local mail setup, you might want to do that via cron,
285 as a hook triggered by mail retrieval, etc.
293 Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
295 License: GNU General Public License (GPL), version 3 or higher