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