]> git.notmuchmail.org Git - notmuch/blob - contrib/notmuch-mutt/notmuch-mutt
c69b35c98651115e0f42dea17d70641752f51ef9
[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 "
117            . shell_quote(join(' ', @_))
118            . " id:$mid");
119 }
120
121 sub die_usage() {
122     my %podflags = ( "verbose" => 1,
123                     "exitval" => 2 );
124     pod2usage(%podflags);
125 }
126
127 sub main() {
128     mkpath($cache_dir) unless (-d $cache_dir);
129
130     my $results_dir = "$cache_dir/results";
131     my $interactive = 0;
132     my $help_needed = 0;
133     my $remove_dups = 0;
134
135     my $getopt = GetOptions(
136         "h|help" => \$help_needed,
137         "o|output-dir=s" => \$results_dir,
138         "p|prompt" => \$interactive,
139         "r|remove-dups" => \$remove_dups);
140     if (! $getopt || $#ARGV < 0) { die_usage() };
141     my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
142
143     foreach my $param (@params) {
144       $param =~ s/folder:=/folder:/g;
145     }
146
147     if ($help_needed) {
148         die_usage();
149     } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
150         print STDERR "Error: no search term provided\n\n";
151         die_usage();
152     } elsif ($action eq "search") {
153         search_action($interactive, $results_dir, $remove_dups, @params);
154     } elsif ($action eq "thread") {
155         thread_action($results_dir, $remove_dups, @params);
156     } elsif ($action eq "tag") {
157         tag_action(@params);
158     } else {
159         die_usage();
160     }
161 }
162
163 main();
164
165 __END__
166
167 =head1 NAME
168
169 notmuch-mutt - notmuch (of a) helper for Mutt
170
171 =head1 SYNOPSIS
172
173 =over
174
175 =item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]...
176
177 =item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL>
178
179 =item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL>
180
181 =back
182
183 =head1 DESCRIPTION
184
185 notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
186 a maildir with search results.
187
188 =head1 OPTIONS
189
190 =over 4
191
192 =item -o DIR
193
194 =item --output-dir DIR
195
196 Store search results as (symlink) messages under maildir DIR. Beware: DIR will
197 be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
198
199 =item -p
200
201 =item --prompt
202
203 Instead of using command line search terms, prompt the user for them (only for
204 "search").
205
206 =item -r
207
208 =item --remove-dups
209
210 Remove emails with duplicate message-ids from search results.  (Passes
211 --duplicate=1 to notmuch search command.)  Note this can hide search
212 results if an email accidentally or maliciously uses the same message-id
213 as a different email.
214
215 =item -h
216
217 =item --help
218
219 Show usage information and exit.
220
221 =back
222
223 =head1 INTEGRATION WITH MUTT
224
225 notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent
226 (unsurprisingly, given the name). To that end, you should define macros like
227 the following in your Mutt configuration (usually one of: F<~/.muttrc>,
228 F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
229
230     macro index <F8> \
231     "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
232     <shell-escape>notmuch-mutt -r --prompt search<enter>\
233     <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
234     <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
235           "notmuch: search mail"
236
237     macro index <F9> \
238     "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
239     <pipe-message>notmuch-mutt -r thread<enter>\
240     <change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
241     <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
242           "notmuch: reconstruct thread"
243
244     macro index <F6> \
245     "<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
246     <pipe-message>notmuch-mutt tag -- -inbox<enter>\
247     <enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
248           "notmuch: remove message from inbox"
249
250 The first macro (activated by <F8>) prompts the user for notmuch search terms
251 and then jump to a temporary maildir showing search results. The second macro
252 (activated by <F9>) reconstructs the thread corresponding to the current mail
253 and show it as search results. The third macro (activated by <F6>) removes the
254 tag C<inbox> from the current message; by changing C<-inbox> this macro may be
255 customised to add or remove tags appropriate to the users notmuch work-flow.
256
257 To keep notmuch index current you should then periodically run C<notmuch
258 new>. Depending on your local mail setup, you might want to do that via cron,
259 as a hook triggered by mail retrieval, etc.
260
261 =head1 SEE ALSO
262
263 mutt(1), notmuch(1)
264
265 =head1 AUTHOR
266
267 Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
268
269 License: GNU General Public License (GPL), version 3 or higher
270
271 =cut