summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Whitton <spwhitton@spwhitton.name>2019-11-20 22:19:15 -0700
committerSean Whitton <spwhitton@spwhitton.name>2019-11-20 22:19:15 -0700
commit33afb1e3af7ce05c8f2ce4af354c8908a14573f5 (patch)
tree3c0eae4836e7f20c054c193ac3e76da5e0462dfa
parent914b13d9c578aab098f427aef88b7cbd5eb3f5a2 (diff)
parent693117551a0e21359ac6dbadba443516c56b04df (diff)
downloadmailscripts-33afb1e3af7ce05c8f2ce4af354c8908a14573f5.tar.gz
Merge tag 'debian/0.14-1' into buster-bpo
mailscripts release 0.14-1 for unstable (sid) [dgit] [dgit distro=debian no-split --quilt=linear] # gpg: Signature made Fri 15 Nov 2019 06:20:35 PM MST # gpg: using RSA key 9B917007AE030E36E4FC248B695B7AE4BF066240 # gpg: Good signature from "Sean Whitton <spwhitton@spwhitton.name>" [ultimate] # Primary key fingerprint: 8DC2 487E 51AB DD90 B5C4 753F 0F56 D055 3B6D 411B # Subkey fingerprint: 9B91 7007 AE03 0E36 E4FC 248B 695B 7AE4 BF06 6240
-rw-r--r--Makefile9
-rw-r--r--debian/changelog47
-rw-r--r--debian/control9
-rw-r--r--debian/mailscripts.bash-completion1
-rw-r--r--debian/mailscripts.install10
-rw-r--r--debian/mailscripts.manpages10
-rwxr-xr-xdebian/rules2
-rw-r--r--email-extract-openpgp-certs.1.pod2
-rwxr-xr-xemail-print-mime-structure189
-rw-r--r--email-print-mime-structure.1.pod47
-rw-r--r--mailscripts.el88
-rw-r--r--notmuch-extract-patch.1.pod31
-rwxr-xr-xnotmuch-extract-patch/notmuch-extract-patch24
13 files changed, 385 insertions, 84 deletions
diff --git a/Makefile b/Makefile
index 352f6f0..cd8f592 100644
--- a/Makefile
+++ b/Makefile
@@ -3,14 +3,21 @@ MANPAGES=mdmv.1 mbox2maildir.1 \
email-extract-openpgp-certs.1 \
email-print-mime-structure.1 \
notmuch-import-patch.1
+COMPLETIONS=completions/bash/email-print-mime-structure
-all: $(MANPAGES)
+all: $(MANPAGES) $(COMPLETIONS)
clean:
rm -f $(MANPAGES)
+ rm -rf completions
%.1: %.1.pod
pod2man --section=1 --date="Debian Project" --center="User Commands" \
--utf8 \
--name=$(subst .1,,$@) \
$^ $@
+
+completions/bash/%:
+ mkdir -p completions/bash
+ register-python-argcomplete3 $(notdir $@) >$@.tmp
+ mv $@.tmp $@
diff --git a/debian/changelog b/debian/changelog
index 407759f..61d1a11 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,48 @@
+mailscripts (0.14-1) unstable; urgency=medium
+
+ * email-print-mime-structure: add bash completion (Closes: #944434).
+ Thanks to Daniel Kahn Gillmor for the patch.
+ - Build-depend on bash-completion, python3-argcomplete.
+ - Recommend python3-argcomplete.
+ * email-print-mime-structure: replace --use-gpg-agent=true with
+ --use-gpg-agent, and add --no-use-gpg-agent (Closes: #944475).
+ This is due to limitations in Python's argparse library.
+ Thanks to Daniel Kahn Gillmor for the report and a patch.
+ * Declare compliance with Debian Policy 4.4.1.
+ Thanks to Daniel Kahn Gillmor for taking the time to verify that no
+ changes are required.
+
+ -- Sean Whitton <spwhitton@spwhitton.name> Fri, 15 Nov 2019 18:19:04 -0700
+
+mailscripts (0.13-1) unstable; urgency=medium
+
+ * notmuch-extract-patch: add -v/--reroll-count option (Closes: #944418).
+ * mailscripts.el: prefix arg to pass -v/--reroll-count to
+ notmuch-extract-patch.
+ * email-print-mime-structure: add --use-gpg-agent option (Closes: #944340).
+ Thanks to Daniel Kahn Gillmor for the patch series.
+ - Suggest gpg & gpg-agent.
+
+ -- Sean Whitton <spwhitton@spwhitton.name> Sun, 10 Nov 2019 01:12:04 -0700
+
+mailscripts (0.12-1) unstable; urgency=medium
+
+ * email-print-mime-structure: make typesafe.
+ Thanks to Daniel Kahn Gillmor for the patch.
+ * email-print-mime-structure: add capability to decrypt message parts
+ (Closes: #943959).
+ Thanks to Daniel Kahn Gillmor for the patch series.
+
+ * mailscripts.el:
+ - new defcustom: mailscripts-extract-patches-branch-prefix
+ - new commands:
+ + notmuch-extract-thread-patches-projectile
+ + notmuch-extract-message-patches{,-projectile}
+ - if user does not enter a branch name, use current HEAD.
+ * elpa-mailscripts now depends on elpa-projectile.
+
+ -- Sean Whitton <spwhitton@spwhitton.name> Wed, 06 Nov 2019 20:54:56 -0700
+
mailscripts (0.11-1~bpo10+1) buster-backports; urgency=medium
* Rebuild for buster-backports.
@@ -7,7 +52,7 @@ mailscripts (0.11-1~bpo10+1) buster-backports; urgency=medium
mailscripts (0.11-1) unstable; urgency=medium
* New script: email-print-mime-structure (Closes: #939993).
- Imported from the notmuch project, which never shipped it in releases.
+ Imported from the notmuch project, which never installed it.
Thanks to Daniel Kahn Gillmor for the patches.
* Generate nroff output in UTF-8.
Thanks to Daniel Kahn Gillmor for the patch.
diff --git a/debian/control b/debian/control
index 6d3a54f..72b57c3 100644
--- a/debian/control
+++ b/debian/control
@@ -2,11 +2,13 @@ Source: mailscripts
Section: mail
Priority: optional
Maintainer: Sean Whitton <spwhitton@spwhitton.name>
-Standards-Version: 4.1.5
+Standards-Version: 4.4.1
Build-Depends:
+ bash-completion,
debhelper (>= 10),
dh-elpa,
perl,
+ python3-argcomplete,
Vcs-Git: https://git.spwhitton.name/mailscripts
Vcs-Browser: https://git.spwhitton.name/mailscripts
@@ -39,6 +41,11 @@ Recommends:
devscripts,
git,
notmuch,
+ python3-argcomplete,
+ python3-pgpy,
+Suggests:
+ gpg,
+ gpg-agent,
Architecture: all
Description: collection of scripts for manipulating e-mail on Debian
This package provides a collection of scripts for manipulating e-mail
diff --git a/debian/mailscripts.bash-completion b/debian/mailscripts.bash-completion
new file mode 100644
index 0000000..435576f
--- /dev/null
+++ b/debian/mailscripts.bash-completion
@@ -0,0 +1 @@
+completions/bash/email-print-mime-structure
diff --git a/debian/mailscripts.install b/debian/mailscripts.install
index 99216c1..2c060df 100644
--- a/debian/mailscripts.install
+++ b/debian/mailscripts.install
@@ -1,8 +1,8 @@
+email-extract-openpgp-certs /usr/bin
+email-print-mime-structure /usr/bin
+maildir-import-patch /usr/bin
mbox2maildir /usr/bin
mdmv /usr/bin
-notmuch-slurp-debbug /usr/bin
-maildir-import-patch /usr/bin
-notmuch-import-patch /usr/bin
notmuch-extract-patch/notmuch-extract-patch /usr/bin
-email-extract-openpgp-certs /usr/bin
-email-print-mime-structure /usr/bin
+notmuch-import-patch /usr/bin
+notmuch-slurp-debbug /usr/bin
diff --git a/debian/mailscripts.manpages b/debian/mailscripts.manpages
index 6d7cb30..1de088f 100644
--- a/debian/mailscripts.manpages
+++ b/debian/mailscripts.manpages
@@ -1,8 +1,8 @@
+email-extract-openpgp-certs.1
+email-print-mime-structure.1
+maildir-import-patch.1
mbox2maildir.1
mdmv.1
-notmuch-slurp-debbug.1
-maildir-import-patch.1
-notmuch-import-patch.1
notmuch-extract-patch.1
-email-extract-openpgp-certs.1
-email-print-mime-structure.1
+notmuch-import-patch.1
+notmuch-slurp-debbug.1
diff --git a/debian/rules b/debian/rules
index e8e22ba..6d50bf4 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,4 +1,4 @@
#!/usr/bin/make -f
%:
- dh $@ --with elpa
+ dh $@ --with elpa --with bash-completion
diff --git a/email-extract-openpgp-certs.1.pod b/email-extract-openpgp-certs.1.pod
index 9983de0..d1d641a 100644
--- a/email-extract-openpgp-certs.1.pod
+++ b/email-extract-openpgp-certs.1.pod
@@ -38,7 +38,7 @@ message's cryptographic envelope.
B<email-extract-openpgp-certs> does not attempt to validate the
certificates it finds in any way. It does not ensure that they are
valid OpenPGP certificates, or even that they are of a sane size. It
-doeds not try to establish any relationship between the extracted
+does not try to establish any relationship between the extracted
certificates and the messages in which they are sent. For example, it
does not check the Autocrypt addr= attribute against the message's From:
header.
diff --git a/email-print-mime-structure b/email-print-mime-structure
index 7adeb2b..4f165b1 100755
--- a/email-print-mime-structure
+++ b/email-print-mime-structure
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
+# PYTHON_ARGCOMPLETE_OK
# -*- coding: utf-8 -*-
# Copyright (C) 2019 Daniel Kahn Gillmor
@@ -29,49 +30,153 @@ Example:
If you want to number the parts, i suggest piping the output through
something like "cat -n"
'''
-import email
+import os
import sys
+import email
+import logging
+import subprocess
-def print_part(z, prefix):
- fname = '' if z.get_filename() is None else ' [' + z.get_filename() + ']'
- cset = '' if z.get_charset() is None else ' (' + z.get_charset() + ')'
- disp = z.get_params(None, header='Content-Disposition')
- if (disp is None):
- disposition = ''
- else:
- disposition = ''
- for d in disp:
- if d[0] in [ 'attachment', 'inline' ]:
- disposition = ' ' + d[0]
- if z.is_multipart():
- nbytes = len(z.as_string())
- else:
- nbytes = len(z.get_payload())
-
- print('{}{}{}{}{} {:d} bytes'.format(
- prefix,
- z.get_content_type(),
- cset,
- disposition,
- fname,
- nbytes,
- ))
-
-def test(z, prefix=''):
- if (z.is_multipart()):
- print_part(z, prefix+'┬╴')
- if prefix.endswith('└'):
- prefix = prefix.rpartition('└')[0] + ' '
- if prefix.endswith('├'):
- prefix = prefix.rpartition('├')[0] + '│'
- parts = z.get_payload()
- i = 0
- while (i < parts.__len__()-1):
- test(parts[i], prefix + '├')
- i += 1
- test(parts[i], prefix + '└')
- # FIXME: show epilogue?
+from argparse import ArgumentParser, Namespace
+from typing import Optional, Union, List, Tuple, Any
+from email.charset import Charset
+from email.message import Message
+
+try:
+ import pgpy #type: ignore
+except ImportError:
+ pgpy = None
+
+try:
+ import argcomplete #type: ignore
+except ImportError:
+ argcomplete = None
+
+class MimePrinter(object):
+ def __init__(self, args:Namespace):
+ self.args = args
+
+ def print_part(self, z:Message, prefix:str, parent:Optional[Message], num:int) -> None:
+ ofname:Optional[str] = z.get_filename()
+ fname:str = '' if ofname is None else f' [{ofname}]'
+ ocharset:Union[Charset, str, None] = z.get_charset()
+ cset:str = '' if ocharset is None else f' ({ocharset})'
+ disp:Union[List[Tuple[str,str]], List[str], None] = z.get_params(None, header='Content-Disposition')
+ disposition:str = ''
+ if (disp is not None):
+ for d in disp:
+ if d[0] in [ 'attachment', 'inline' ]:
+ disposition = ' ' + d[0]
+ nbytes:int
+ if z.is_multipart():
+ # FIXME: it looks like we are counting chars here, not bytes:
+ nbytes = len(z.as_string())
+ else:
+ payload:Union[List[Message], str, bytes, None] = z.get_payload()
+ if not isinstance(payload, (str,bytes)):
+ raise TypeError(f'expected payload to be either str or bytes, got {type(payload)}')
+ # FIXME: it looks like we are counting chars here, not bytes:
+ nbytes = len(payload)
+
+ print(f'{prefix}{z.get_content_type()}{cset}{disposition}{fname} {nbytes:d} bytes')
+ try_decrypt:bool = self.args.pgpkey or self.args.use_gpg_agent
+
+ if try_decrypt and \
+ (parent is not None) and \
+ (parent.get_content_type().lower() == 'multipart/encrypted') and \
+ (str(parent.get_param('protocol')).lower() == 'application/pgp-encrypted') and \
+ (num == 2):
+ cryptopayload:Optional[Message] = None
+ ciphertext:Union[List[Message],str,bytes,None] = z.get_payload()
+ if not isinstance(ciphertext, str):
+ logging.warning('encrypted part was not a leaf mime part somehow')
+ return
+ if self.args.pgpkey:
+ cryptopayload = self.pgpy_decrypt(self.args.pgpkey, ciphertext)
+ if cryptopayload is None and self.args.use_gpg_agent:
+ cryptopayload = self.gpg_decrypt(ciphertext)
+ if cryptopayload is None:
+ logging.warning(f'Unable to decrypt')
+ return
+ newprefix = prefix[:-3] + ' '
+ print(f'{newprefix}↧ (decrypts to)')
+ self.print_tree(cryptopayload, newprefix + '└', z, 0)
+
+ def pgpy_decrypt(self, keys:List[str], ciphertext:str) -> Optional[Message]:
+ if pgpy is None:
+ logging.warning(f'Python module pgpy is not available, not decrypting (try "apt install python3-pgpy")')
+ return None
+ keyname:str
+ ret:Optional[Message] = None
+ for keyname in keys:
+ try:
+ key:pgpy.PGPKey
+ key, _ = pgpy.PGPKey.from_file(keyname)
+ msg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(ciphertext)
+ msg = key.decrypt(msg)
+ return email.message_from_bytes(msg.message)
+ except:
+ pass
+ return None
+
+ def gpg_decrypt(self, ciphertext:str) -> Optional[Message]:
+ inp:int
+ outp:int
+ inp, outp = os.pipe()
+ with open(outp, 'w') as outf:
+ outf.write(ciphertext)
+ out:subprocess.CompletedProcess[bytes] = subprocess.run(['gpg', '--batch', '--decrypt'],
+ stdin=inp,
+ capture_output=True)
+ if out.returncode == 0:
+ return email.message_from_bytes(out.stdout)
+ return None
+
+ def print_tree(self, z:Message, prefix:str, parent:Optional[Message], num:int) -> None:
+ if (z.is_multipart()):
+ self.print_part(z, prefix+'┬╴', parent, num)
+ if prefix.endswith('└'):
+ prefix = prefix.rpartition('└')[0] + ' '
+ if prefix.endswith('├'):
+ prefix = prefix.rpartition('├')[0] + '│'
+ parts:Union[List[Message], str, bytes, None] = z.get_payload()
+ if not isinstance(parts, list):
+ raise TypeError(f'parts was {type(parts)}, expected List[Message]')
+ i = 0
+ while (i < len(parts)-1):
+ self.print_tree(parts[i], prefix + '├', z, i+1)
+ i += 1
+ self.print_tree(parts[i], prefix + '└', z, i+1)
+ # FIXME: show epilogue?
+ else:
+ self.print_part(z, prefix+'─╴', parent, num)
+
+def main() -> None:
+ parser:ArgumentParser = ArgumentParser(description='Read RFC2822 MIME message from stdin and emit a tree diagram to stdout.',
+ epilog="Example: email-print-mime-structure <message.eml")
+ parser.add_argument('--pgpkey', metavar='KEYFILE', action='append',
+ help='OpenPGP Transferable Secret Key for decrypting')
+ parser.add_argument('--use-gpg-agent', action='store_true',
+ help='Ask local GnuPG installation for decryption')
+ parser.add_argument('--no-use-gpg-agent', action='store_false',
+ help='Don\'t ask local GnuPG installation for decryption')
+ parser.set_defaults(use_gpg_agent=False)
+
+ if argcomplete:
+ argcomplete.autocomplete(parser)
+ elif '_ARGCOMPLETE' in os.environ:
+ logging.error('Argument completion requested but the "argcomplete" '
+ 'module is not installed. '
+ 'Maybe you want to "apt install python3-argcomplete"')
+ sys.exit(1)
+
+ args:Namespace = parser.parse_args()
+ msg:Union[Message, str, int, Any] = email.message_from_file(sys.stdin)
+
+ if isinstance(msg, Message):
+ printer:MimePrinter = MimePrinter(args)
+ printer.print_tree(msg, '└', None, 0)
else:
- print_part(z, prefix+'─╴')
+ logging.error('Input was not an e-mail message')
-test(email.message_from_file(sys.stdin), '└')
+if __name__ == '__main__':
+ main()
diff --git a/email-print-mime-structure.1.pod b/email-print-mime-structure.1.pod
index ab1ec05..d8545ad 100644
--- a/email-print-mime-structure.1.pod
+++ b/email-print-mime-structure.1.pod
@@ -19,7 +19,45 @@ something like "cat -n".
=head1 OPTIONS
-None.
+=over 4
+
+=item B<--pgpkey=>I<KEYFILE>
+
+I<KEYFILE> should name an OpenPGP transferable secret key that is not
+password-protected. If a PGP/MIME-encrypted message is found on
+standard input, this key will be tried for decryption. May be used
+multiple times if you want to try decrypting with more than one secret
+key.
+
+OpenPGP secret keys listed in B<--pgpkey=> are used ephemerally, and
+do not interact with any local GnuPG keyring.
+
+=item B<--use-gpg-agent>
+
+If this flag is present, and B<email-print-mime-structure> encounters
+a PGP/MIME-encrypted part, it will try to decrypt the part using the
+secret keys found in the local installation of GnuPG.
+
+If both B<--pgpkey=>I<KEYFILE> and B<--use-gpg-agent> are
+supplied, I<KEYFILE> arguments will be tried before falling back to
+GnuPG.
+
+If B<email-print-mime-structure> has been asked to decrypt parts with
+either B<--pgpkey=>I<KEYFILE> or with B<--use-gpg-agent>, and it
+is unable to decrypt an encrypted part, it will emit a warning to
+stderr.
+
+=item B<--no-use-gpg-agent>
+
+Don't try to decrypt PGP/MIME-encrypted parts using secret keys found
+in the local installation of GnuPG. This is the default.
+
+=item B<--help>, B<-h>
+
+Show usage instructions.
+
+=back
+
=head1 EXAMPLE
@@ -34,10 +72,6 @@ None.
=head1 LIMITATIONS
-B<email-print-mime-structure> currently does not try to decrypt
-encrypted e-mails, so it cannot display the MIME structure that is
-inside the message's cryptographic envelope.
-
B<email-print-mime-structure>'s output is not stable, and is not
intended to be interpreted by machines, so please do not depend on it
in scripts!
@@ -52,7 +86,8 @@ environment.
=head1 SEE ALSO
-https://tools.ietf.org/html/rfc2045, https://tools.ietf.org/html/rfc2049
+https://tools.ietf.org/html/rfc2045, https://tools.ietf.org/html/rfc2049,
+https://tools.ietf.org/html/rfc3156
=head1 AUTHOR
diff --git a/mailscripts.el b/mailscripts.el
index f0002fc..916aec8 100644
--- a/mailscripts.el
+++ b/mailscripts.el
@@ -1,10 +1,10 @@
;;; mailscripts.el --- functions to access tools in the mailscripts package
;; Author: Sean Whitton <spwhitton@spwhitton.name>
-;; Version: 0.11
-;; Package-Requires: (notmuch)
+;; Version: 0.13
+;; Package-Requires: (notmuch projectile)
-;; Copyright (C) 2018 Sean Whitton
+;; Copyright (C) 2018, 2019 Sean Whitton
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
@@ -22,6 +22,17 @@
;;; Code:
(require 'notmuch)
+(require 'projectile)
+
+(defgroup mailscripts nil
+ "Customisation of functions in the mailscripts package.")
+
+(defcustom mailscripts-extract-patches-branch-prefix nil
+ "Prefix for git branches created by functions which extract patch series.
+
+E.g. `email/'."
+ :type 'string
+ :group 'mailscripts)
;;;###autoload
(defun notmuch-slurp-debbug (bug &optional no-open)
@@ -43,25 +54,84 @@ If NO-OPEN, don't open the thread."
(notmuch-refresh-this-buffer)))
;;;###autoload
-(defun notmuch-extract-thread-patches (repo branch)
+(defun notmuch-extract-thread-patches (repo branch &optional reroll-count)
"Extract patch series in current thread to branch BRANCH in repo REPO.
The target branch may or may not already exist.
+With an optional prefix numeric argument REROLL-COUNT, try to
+extract the nth revision of a series. See the --reroll-count
+option detailed in notmuch-extract-patch(1).
+
See notmuch-extract-patch(1) manpage for limitations: in
particular, this Emacs Lisp function supports passing only entire
threads to the notmuch-extract-patch(1) command."
- (interactive "Dgit repo: \nsnew branch name: ")
+ (interactive
+ "Dgit repo: \nsbranch name (or leave blank to apply to current HEAD): \np")
(let ((thread-id notmuch-show-thread-id)
(default-directory (expand-file-name repo)))
- (call-process-shell-command
- (format "git checkout -b %s"
- (shell-quote-argument branch)))
+ (mailscripts--check-out-branch branch)
(shell-command
- (format "notmuch-extract-patch %s | git am"
+ (format "notmuch-extract-patch -v%d %s | git am"
+ (if reroll-count reroll-count 1)
(shell-quote-argument thread-id))
"*notmuch-apply-thread-series*")))
+;;;###autoload
+(defun notmuch-extract-thread-patches-projectile ()
+ "Like `notmuch-extract-thread-patches', but use projectile to choose the repo."
+ (interactive)
+ (mailscripts--projectile-repo-and-branch
+ 'notmuch-extract-thread-patches (prefix-numeric-value current-prefix-arg)))
+
+;;;###autoload
+(defun notmuch-extract-message-patches (repo branch)
+ "Extract patches attached to current message to branch BRANCH in repo REPO.
+
+The target branch may or may not already exist.
+
+Patches are applied using git-am(1), so we only consider
+attachments with filenames which look like they were generated by
+git-format-patch(1)."
+ (interactive
+ "Dgit repo: \nsbranch name (or leave blank to apply to current HEAD): ")
+ (with-current-notmuch-show-message
+ (let ((default-directory (expand-file-name repo))
+ (mm-handle (mm-dissect-buffer)))
+ (mailscripts--check-out-branch branch)
+ (notmuch-foreach-mime-part
+ (lambda (p)
+ (let* ((disposition (mm-handle-disposition p))
+ (filename (cdr (assq 'filename disposition))))
+ (and filename
+ (string-match
+ "^\\(v[0-9]+-\\)?[0-9]+-.+\.\\(patch\\|diff\\|txt\\)$" filename)
+ (mm-pipe-part p "git am"))))
+ mm-handle))))
+
+;;;###autoload
+(defun notmuch-extract-message-patches-projectile ()
+ "Like `notmuch-extract-message-patches', but use projectile to choose the repo."
+ (interactive)
+ (mailscripts--projectile-repo-and-branch 'notmuch-extract-message-patches))
+
+(defun mailscripts--check-out-branch (branch)
+ (unless (string= branch "")
+ (call-process-shell-command
+ (format "git checkout -b %s"
+ (shell-quote-argument
+ (if mailscripts-extract-patches-branch-prefix
+ (concat mailscripts-extract-patches-branch-prefix branch)
+ branch))))))
+
+(defun mailscripts--projectile-repo-and-branch (f &rest args)
+ (let ((repo (projectile-completing-read
+ "Select projectile project: " projectile-known-projects))
+ (branch (completing-read
+ "Branch name (or leave blank to apply to current HEAD): "
+ nil)))
+ (apply f repo branch args)))
+
(provide 'mailscripts)
;;; mailscripts.el ends here
diff --git a/notmuch-extract-patch.1.pod b/notmuch-extract-patch.1.pod
index 21095bc..a18cc22 100644
--- a/notmuch-extract-patch.1.pod
+++ b/notmuch-extract-patch.1.pod
@@ -4,7 +4,7 @@ notmuch-extract-patch - extract a git patch series from notmuch
=head1 SYNOPSIS
-B<notmuch-extract-patch> I<QUERY>
+B<notmuch-extract-patch> [B<-v>|B<--reroll-count=>I<N>] I<QUERY>
=head1 DESCRIPTION
@@ -15,7 +15,23 @@ replies/reviews.
=head1 OPTIONS
-None.
+=over 4
+
+=item B<-v>|B<--reroll-count=>I<N>
+
+Try to extract the I<N>th version of a patch series, where these
+patches are identified by subject prefixes like "[PATCH vI<N> 1/3]".
+
+If this option is not specified, default to extracting the first
+version of the patch series.
+
+Note that this option should not usually be needed, because best
+practices when sharing patches with git-send-email(1) include starting
+a new thread when posting a revised series. The I<--in-reply-to>
+option to git-format-patch(1) is used mainly for posting a patch
+series in reply to a bug report.
+
+=back
=head1 EXAMPLE
@@ -28,17 +44,12 @@ None.
=head1 LIMITATIONS
-B<notmuch-extract-patch> assumes one patch series per query. So if
-there is more than one patch series in a thread, you will need to
+B<notmuch-extract-patch> can select patches to extract based on the
+reroll count, but otherwise assumes that there is only one patch
+series in a thread. If this assumption is violated, you would need to
construct a notmuch query that includes only the patches you want to
extract, which somewhat defeats the purpose of this script.
-This should not happen often because best practices when sharing
-patches with git-send-email(1) include starting a new thread when
-posting a revised series. The I<--in-reply-to> option to
-B<notmuch-extract-patch> is used mainly for posting a patch series in
-reply to a bug report.
-
=head1 SEE ALSO
notmuch(1), git-send-email(1)
diff --git a/notmuch-extract-patch/notmuch-extract-patch b/notmuch-extract-patch/notmuch-extract-patch
index cfd4464..4cfda4c 100755
--- a/notmuch-extract-patch/notmuch-extract-patch
+++ b/notmuch-extract-patch/notmuch-extract-patch
@@ -22,6 +22,7 @@ import sys
import tempfile
import subprocess
import re
+import getopt
def get_body(message):
body = None
@@ -48,8 +49,27 @@ def is_git_patch(msg):
# return ("git-send-email" in msg['x-mailer'] and match)
return match
+def has_reroll_count(msg, v):
+ subject_prefix = get_subject_prefix(msg['subject'])
+ if subject_prefix is not None:
+ return "v"+str(v) in subject_prefix \
+ or (v == 1 and not any(entry[0] == 'v' for entry in subject_prefix))
+
+def get_subject_prefix(s):
+ match = re.search(r'''^\[(.*PATCH.*)\]''', s)
+ if match:
+ return match.group(1).split()
+
def main():
- query = sys.argv[1:]
+ try:
+ opts, query = getopt.getopt(sys.argv[1:], "v:", ["reroll-count="])
+ except getopt.GetoptError as err:
+ sys.stderr.write(str(err)+"\n")
+ sys.exit(2)
+ reroll_count = 1
+ for o, a in opts:
+ if o in ("-v", "--reroll-count"):
+ reroll_count = int(a)
with tempfile.NamedTemporaryFile() as in_mb_file:
out = subprocess.check_output(['notmuch', 'show', '--format=mbox']+query)
in_mb_file.write(out)
@@ -59,7 +79,7 @@ def main():
with tempfile.NamedTemporaryFile() as out_mb_file:
out_mb = mailbox.mbox(out_mb_file.name)
for m in in_mb:
- if is_git_patch(m):
+ if is_git_patch(m) and has_reroll_count(m, reroll_count):
sys.stderr.write(m['subject']+"\n")
out_mb.add(m)
out_mb.flush()