2016-04-21 20:10:25 +02:00
|
|
|
|
#!/usr/bin/env perl
|
2012-03-26 10:45:58 +02:00
|
|
|
|
#
|
|
|
|
|
# notmuch-mutt - notmuch (of a) helper for Mutt
|
|
|
|
|
#
|
2015-02-15 13:39:06 +01:00
|
|
|
|
# Copyright: <20> 2011-2015 Stefano Zacchiroli <zack@upsilon.cc>
|
2012-03-26 10:45:58 +02:00
|
|
|
|
# License: GNU General Public License (GPL), version 3 or above
|
|
|
|
|
#
|
|
|
|
|
# See the bottom of this file for more documentation.
|
|
|
|
|
# A manpage can be obtained by running "pod2man notmuch-mutt > notmuch-mutt.1"
|
|
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
|
|
use File::Path;
|
2020-07-27 21:38:33 +02:00
|
|
|
|
use File::Basename;
|
2012-03-26 10:45:58 +02:00
|
|
|
|
use Getopt::Long qw(:config no_getopt_compat);
|
2015-02-15 13:39:08 +01:00
|
|
|
|
use Mail::Header;
|
2012-03-26 10:45:58 +02:00
|
|
|
|
use Mail::Box::Maildir;
|
|
|
|
|
use Pod::Usage;
|
|
|
|
|
use String::ShellQuote;
|
|
|
|
|
use Term::ReadLine;
|
2015-02-15 13:39:08 +01:00
|
|
|
|
use Digest::SHA;
|
2012-03-26 10:45:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
my $xdg_cache_dir = "$ENV{HOME}/.cache";
|
|
|
|
|
$xdg_cache_dir = $ENV{XDG_CACHE_HOME} if $ENV{XDG_CACHE_HOME};
|
|
|
|
|
my $cache_dir = "$xdg_cache_dir/notmuch/mutt";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# create an empty maildir (if missing) or empty an existing maildir"
|
|
|
|
|
sub empty_maildir($) {
|
|
|
|
|
my ($maildir) = (@_);
|
|
|
|
|
rmtree($maildir) if (-d $maildir);
|
|
|
|
|
my $folder = new Mail::Box::Maildir(folder => $maildir,
|
|
|
|
|
create => 1);
|
|
|
|
|
$folder->close();
|
|
|
|
|
}
|
|
|
|
|
|
2012-08-01 10:09:41 +02:00
|
|
|
|
# search($maildir, $remove_dups, $query)
|
2012-03-26 10:45:58 +02:00
|
|
|
|
# search mails according to $query with notmuch; store results in $maildir
|
2012-08-01 10:09:41 +02:00
|
|
|
|
sub search($$$) {
|
|
|
|
|
my ($maildir, $remove_dups, $query) = @_;
|
2013-09-05 04:05:50 +02:00
|
|
|
|
my $dup_option = "";
|
|
|
|
|
|
2020-07-27 21:38:33 +02:00
|
|
|
|
my @args = qw/notmuch search --output=files/;
|
|
|
|
|
push @args, "--duplicate=1" if $remove_dups;
|
|
|
|
|
push @args, $query;
|
2013-09-05 04:05:50 +02:00
|
|
|
|
|
2012-03-26 10:45:58 +02:00
|
|
|
|
empty_maildir($maildir);
|
2020-07-27 21:38:33 +02:00
|
|
|
|
open my $pipe, '-|', @args or die "Running @args failed: $!\n";
|
|
|
|
|
while (<$pipe>) {
|
|
|
|
|
chomp;
|
|
|
|
|
my $ln = "$maildir/cur/" . basename $_;
|
|
|
|
|
symlink $_, "$ln" or warn "Failed to symlink '$_', '$ln': $!\n";
|
|
|
|
|
}
|
2012-03-26 10:45:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub prompt($$) {
|
|
|
|
|
my ($text, $default) = @_;
|
|
|
|
|
my $query = "";
|
|
|
|
|
my $term = Term::ReadLine->new( "notmuch-mutt" );
|
|
|
|
|
my $histfile = "$cache_dir/history";
|
|
|
|
|
|
|
|
|
|
$term->ornaments( 0 );
|
|
|
|
|
$term->unbind_key( ord( "\t" ) );
|
|
|
|
|
$term->MinLine( 3 );
|
|
|
|
|
$histfile = $ENV{MUTT_NOTMUCH_HISTFILE} if $ENV{MUTT_NOTMUCH_HISTFILE};
|
|
|
|
|
$term->ReadHistory($histfile) if (-r $histfile);
|
|
|
|
|
while (1) {
|
|
|
|
|
chomp($query = $term->readline($text, $default));
|
|
|
|
|
if ($query eq "?") {
|
2012-05-29 22:44:27 +02:00
|
|
|
|
system("man", "notmuch-search-terms");
|
2012-03-26 10:45:58 +02:00
|
|
|
|
} else {
|
|
|
|
|
$term->WriteHistory($histfile);
|
|
|
|
|
return $query;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub get_message_id() {
|
2015-02-15 13:39:08 +01:00
|
|
|
|
my $mid = undef;
|
|
|
|
|
my @headers = ();
|
|
|
|
|
|
|
|
|
|
while (<STDIN>) { # collect header lines in @headers
|
|
|
|
|
push(@headers, $_);
|
|
|
|
|
last if $_ =~ /^$/;
|
|
|
|
|
}
|
|
|
|
|
my $head = Mail::Header->new(\@headers);
|
|
|
|
|
$mid = $head->get("message-id") or undef;
|
|
|
|
|
|
|
|
|
|
if ($mid) { # Message-ID header found
|
|
|
|
|
$mid =~ /^<(.*)>$/; # extract message id
|
|
|
|
|
$mid = $1;
|
|
|
|
|
} else { # Message-ID header not found, synthesize a message id
|
|
|
|
|
# based on SHA1, as notmuch would do. See:
|
2016-06-02 18:26:14 +02:00
|
|
|
|
# https://git.notmuchmail.org/git/notmuch/blob/HEAD:/lib/sha1.c
|
2015-02-15 13:39:08 +01:00
|
|
|
|
my $sha = Digest::SHA->new(1);
|
|
|
|
|
$sha->add($_) foreach(@headers);
|
|
|
|
|
$sha->addfile(\*STDIN);
|
|
|
|
|
$mid = 'notmuch-sha1-' . $sha->hexdigest;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $mid;
|
2012-03-26 10:45:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
2012-08-01 10:09:41 +02:00
|
|
|
|
sub search_action($$$@) {
|
|
|
|
|
my ($interactive, $results_dir, $remove_dups, @params) = @_;
|
2012-03-26 10:45:58 +02:00
|
|
|
|
|
|
|
|
|
if (! $interactive) {
|
2012-08-01 10:09:41 +02:00
|
|
|
|
search($results_dir, $remove_dups, join(' ', @params));
|
2012-03-26 10:45:58 +02:00
|
|
|
|
} else {
|
|
|
|
|
my $query = prompt("search ('?' for man): ", join(' ', @params));
|
|
|
|
|
if ($query ne "") {
|
2012-08-01 10:09:41 +02:00
|
|
|
|
search($results_dir, $remove_dups, $query);
|
2012-03-26 10:45:58 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-08-01 10:09:41 +02:00
|
|
|
|
sub thread_action($$@) {
|
|
|
|
|
my ($results_dir, $remove_dups, @params) = @_;
|
2012-03-26 10:45:58 +02:00
|
|
|
|
|
|
|
|
|
my $mid = get_message_id();
|
notmuch-mutt: Use of uninitialized value.
On Thu, Feb 14, 2013 at 12:36:58AM +0100, Profpatsch wrote:
> On 13-02-13 02:35pm, Kevin J. McCarthy wrote:
> > A more likely idea is to check whether you have $pipe_decode set.
>
> BRILLIANT!
> So much for copying a basic rc from someone else.
> Of course, that was it and I’m officially an idiot.
Neat, thanks Kevin for debugging the issue down to $pipe_decode (which
I've never used, mutt never stops to amaze me :-)).
> And apparently Mail::Internet errors out if there is no Message-ID.
> (Which mentioned in the docs at CPAN…)
>
> Mystery solved.
Right, but still a more graceful failure model would be nice.
Please find attached a patch that in such cases should 1) give a
supposedly nice error message explaining what's going on and 2) empty
the results dir to avoid showing you unrelated results. It works for me.
But extra checking never hurts, in particular for the tag action, which
I don't personally use.
I guess it would also be nice to actually disable $pipe_decode in the
relevant Mutt macros, but I'm not sure about to do that without
interfering with user desired configuration. Kevin: do you know if there
is a common Mutt trick to store the value of a variable before changing
it, and restoring it a posteriori? More isolation for this kind of
things in Mutt would definitely be welcome...
Cheers.
--
Stefano Zacchiroli . . . . . . . zack@upsilon.cc . . . . o . . . o . o
Maître de conférences . . . . . http://upsilon.cc/zack . . . o . . . o o
Debian Project Leader . . . . . . @zack on identi.ca . . o o o . . . o .
« the first rule of tautology club is the first rule of tautology club »
From b67ab95855ce7d279d8c0b3ddcbc20e679afc70b Mon Sep 17 00:00:00 2001
From: Stefano Zacchiroli <zack@upsilon.cc>
Date: Thu, 14 Feb 2013 09:31:37 +0100
Subject: [PATCH] notmuch-mutt: more graceful handling of missing Message-Id
errors
in particular:
- the "thread" action would print an error and empty results dir
- the "tag action would print an error
2013-02-14 09:37:13 +01:00
|
|
|
|
if (! defined $mid) {
|
|
|
|
|
empty_maildir($results_dir);
|
|
|
|
|
die "notmuch-mutt: cannot find Message-Id, abort.\n";
|
|
|
|
|
}
|
2012-03-26 10:45:58 +02:00
|
|
|
|
my $search_cmd = 'notmuch search --output=threads ' . shell_quote("id:$mid");
|
|
|
|
|
my $tid = `$search_cmd`; # get thread id
|
|
|
|
|
chomp($tid);
|
|
|
|
|
|
2012-08-01 10:09:41 +02:00
|
|
|
|
search($results_dir, $remove_dups, $tid);
|
2012-03-26 10:45:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub tag_action(@) {
|
|
|
|
|
my $mid = get_message_id();
|
notmuch-mutt: Use of uninitialized value.
On Thu, Feb 14, 2013 at 12:36:58AM +0100, Profpatsch wrote:
> On 13-02-13 02:35pm, Kevin J. McCarthy wrote:
> > A more likely idea is to check whether you have $pipe_decode set.
>
> BRILLIANT!
> So much for copying a basic rc from someone else.
> Of course, that was it and I’m officially an idiot.
Neat, thanks Kevin for debugging the issue down to $pipe_decode (which
I've never used, mutt never stops to amaze me :-)).
> And apparently Mail::Internet errors out if there is no Message-ID.
> (Which mentioned in the docs at CPAN…)
>
> Mystery solved.
Right, but still a more graceful failure model would be nice.
Please find attached a patch that in such cases should 1) give a
supposedly nice error message explaining what's going on and 2) empty
the results dir to avoid showing you unrelated results. It works for me.
But extra checking never hurts, in particular for the tag action, which
I don't personally use.
I guess it would also be nice to actually disable $pipe_decode in the
relevant Mutt macros, but I'm not sure about to do that without
interfering with user desired configuration. Kevin: do you know if there
is a common Mutt trick to store the value of a variable before changing
it, and restoring it a posteriori? More isolation for this kind of
things in Mutt would definitely be welcome...
Cheers.
--
Stefano Zacchiroli . . . . . . . zack@upsilon.cc . . . . o . . . o . o
Maître de conférences . . . . . http://upsilon.cc/zack . . . o . . . o o
Debian Project Leader . . . . . . @zack on identi.ca . . o o o . . . o .
« the first rule of tautology club is the first rule of tautology club »
From b67ab95855ce7d279d8c0b3ddcbc20e679afc70b Mon Sep 17 00:00:00 2001
From: Stefano Zacchiroli <zack@upsilon.cc>
Date: Thu, 14 Feb 2013 09:31:37 +0100
Subject: [PATCH] notmuch-mutt: more graceful handling of missing Message-Id
errors
in particular:
- the "thread" action would print an error and empty results dir
- the "tag action would print an error
2013-02-14 09:37:13 +01:00
|
|
|
|
defined $mid or die "notmuch-mutt: cannot find Message-Id, abort.\n";
|
2012-03-26 10:45:58 +02:00
|
|
|
|
|
2013-09-07 19:07:57 +02:00
|
|
|
|
system("notmuch", "tag", @_, "--", "id:$mid");
|
2012-03-26 10:45:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub die_usage() {
|
|
|
|
|
my %podflags = ( "verbose" => 1,
|
|
|
|
|
"exitval" => 2 );
|
|
|
|
|
pod2usage(%podflags);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub main() {
|
|
|
|
|
mkpath($cache_dir) unless (-d $cache_dir);
|
|
|
|
|
|
|
|
|
|
my $results_dir = "$cache_dir/results";
|
|
|
|
|
my $interactive = 0;
|
|
|
|
|
my $help_needed = 0;
|
2012-08-01 10:09:41 +02:00
|
|
|
|
my $remove_dups = 0;
|
2012-03-26 10:45:58 +02:00
|
|
|
|
|
|
|
|
|
my $getopt = GetOptions(
|
|
|
|
|
"h|help" => \$help_needed,
|
|
|
|
|
"o|output-dir=s" => \$results_dir,
|
2012-08-01 10:09:41 +02:00
|
|
|
|
"p|prompt" => \$interactive,
|
|
|
|
|
"r|remove-dups" => \$remove_dups);
|
2012-03-26 10:45:58 +02:00
|
|
|
|
if (! $getopt || $#ARGV < 0) { die_usage() };
|
|
|
|
|
my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]);
|
|
|
|
|
|
|
|
|
|
foreach my $param (@params) {
|
|
|
|
|
$param =~ s/folder:=/folder:/g;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($help_needed) {
|
|
|
|
|
die_usage();
|
|
|
|
|
} elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) {
|
|
|
|
|
print STDERR "Error: no search term provided\n\n";
|
|
|
|
|
die_usage();
|
|
|
|
|
} elsif ($action eq "search") {
|
2012-08-01 10:09:41 +02:00
|
|
|
|
search_action($interactive, $results_dir, $remove_dups, @params);
|
2012-03-26 10:45:58 +02:00
|
|
|
|
} elsif ($action eq "thread") {
|
2012-08-01 10:09:41 +02:00
|
|
|
|
thread_action($results_dir, $remove_dups, @params);
|
2012-03-26 10:45:58 +02:00
|
|
|
|
} elsif ($action eq "tag") {
|
|
|
|
|
tag_action(@params);
|
|
|
|
|
} else {
|
|
|
|
|
die_usage();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main();
|
|
|
|
|
|
|
|
|
|
__END__
|
|
|
|
|
|
|
|
|
|
=head1 NAME
|
|
|
|
|
|
|
|
|
|
notmuch-mutt - notmuch (of a) helper for Mutt
|
|
|
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
|
|
=item B<notmuch-mutt> [I<OPTION>]... search [I<SEARCH-TERM>]...
|
|
|
|
|
|
|
|
|
|
=item B<notmuch-mutt> [I<OPTION>]... thread < I<MAIL>
|
|
|
|
|
|
|
|
|
|
=item B<notmuch-mutt> [I<OPTION>]... tag [I<TAGS>]... < I<MAIL>
|
|
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
|
|
|
|
|
notmuch-mutt is a frontend to the notmuch mail indexer capable of populating
|
2012-03-31 10:35:25 +02:00
|
|
|
|
a maildir with search results.
|
2012-03-26 10:45:58 +02:00
|
|
|
|
|
|
|
|
|
=head1 OPTIONS
|
|
|
|
|
|
|
|
|
|
=over 4
|
|
|
|
|
|
|
|
|
|
=item -o DIR
|
|
|
|
|
|
|
|
|
|
=item --output-dir DIR
|
|
|
|
|
|
|
|
|
|
Store search results as (symlink) messages under maildir DIR. Beware: DIR will
|
|
|
|
|
be overwritten. (Default: F<~/.cache/notmuch/mutt/results/>)
|
|
|
|
|
|
|
|
|
|
=item -p
|
|
|
|
|
|
|
|
|
|
=item --prompt
|
|
|
|
|
|
|
|
|
|
Instead of using command line search terms, prompt the user for them (only for
|
|
|
|
|
"search").
|
|
|
|
|
|
2012-08-01 10:09:41 +02:00
|
|
|
|
=item -r
|
|
|
|
|
|
|
|
|
|
=item --remove-dups
|
|
|
|
|
|
2013-09-05 04:05:50 +02:00
|
|
|
|
Remove emails with duplicate message-ids from search results. (Passes
|
|
|
|
|
--duplicate=1 to notmuch search command.) Note this can hide search
|
|
|
|
|
results if an email accidentally or maliciously uses the same message-id
|
|
|
|
|
as a different email.
|
2012-08-01 10:09:41 +02:00
|
|
|
|
|
2012-03-26 10:45:58 +02:00
|
|
|
|
=item -h
|
|
|
|
|
|
|
|
|
|
=item --help
|
|
|
|
|
|
|
|
|
|
Show usage information and exit.
|
|
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
|
|
=head1 INTEGRATION WITH MUTT
|
|
|
|
|
|
|
|
|
|
notmuch-mutt can be used to integrate notmuch with the Mutt mail user agent
|
|
|
|
|
(unsurprisingly, given the name). To that end, you should define macros like
|
|
|
|
|
the following in your Mutt configuration (usually one of: F<~/.muttrc>,
|
|
|
|
|
F</etc/Muttrc>, or a configuration snippet under F</etc/Muttrc.d/>):
|
|
|
|
|
|
|
|
|
|
macro index <F8> \
|
2013-02-15 01:12:47 +01:00
|
|
|
|
"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
|
|
|
|
|
<shell-escape>notmuch-mutt -r --prompt search<enter>\
|
|
|
|
|
<change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
|
|
|
|
|
<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
|
2012-03-26 10:45:58 +02:00
|
|
|
|
"notmuch: search mail"
|
2013-02-15 01:12:47 +01:00
|
|
|
|
|
2012-03-26 10:45:58 +02:00
|
|
|
|
macro index <F9> \
|
2013-02-15 01:12:47 +01:00
|
|
|
|
"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
|
|
|
|
|
<pipe-message>notmuch-mutt -r thread<enter>\
|
|
|
|
|
<change-folder-readonly>`echo ${XDG_CACHE_HOME:-$HOME/.cache}/notmuch/mutt/results`<enter>\
|
|
|
|
|
<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
|
2012-03-26 10:45:58 +02:00
|
|
|
|
"notmuch: reconstruct thread"
|
2013-02-15 01:12:47 +01:00
|
|
|
|
|
2012-03-26 10:45:58 +02:00
|
|
|
|
macro index <F6> \
|
2013-02-15 01:12:47 +01:00
|
|
|
|
"<enter-command>set my_old_pipe_decode=\$pipe_decode my_old_wait_key=\$wait_key nopipe_decode nowait_key<enter>\
|
|
|
|
|
<pipe-message>notmuch-mutt tag -- -inbox<enter>\
|
|
|
|
|
<enter-command>set pipe_decode=\$my_old_pipe_decode wait_key=\$my_old_wait_key<enter>" \
|
2012-03-26 10:45:58 +02:00
|
|
|
|
"notmuch: remove message from inbox"
|
|
|
|
|
|
|
|
|
|
The first macro (activated by <F8>) prompts the user for notmuch search terms
|
|
|
|
|
and then jump to a temporary maildir showing search results. The second macro
|
|
|
|
|
(activated by <F9>) reconstructs the thread corresponding to the current mail
|
|
|
|
|
and show it as search results. The third macro (activated by <F6>) removes the
|
|
|
|
|
tag C<inbox> from the current message; by changing C<-inbox> this macro may be
|
|
|
|
|
customised to add or remove tags appropriate to the users notmuch work-flow.
|
|
|
|
|
|
|
|
|
|
To keep notmuch index current you should then periodically run C<notmuch
|
|
|
|
|
new>. Depending on your local mail setup, you might want to do that via cron,
|
|
|
|
|
as a hook triggered by mail retrieval, etc.
|
|
|
|
|
|
|
|
|
|
=head1 SEE ALSO
|
|
|
|
|
|
|
|
|
|
mutt(1), notmuch(1)
|
|
|
|
|
|
|
|
|
|
=head1 AUTHOR
|
|
|
|
|
|
|
|
|
|
Copyright: (C) 2011-2012 Stefano Zacchiroli <zack@upsilon.cc>
|
|
|
|
|
|
|
|
|
|
License: GNU General Public License (GPL), version 3 or higher
|
|
|
|
|
|
|
|
|
|
=cut
|