Package: signing-party Version: 1.1.4-1 Severity: wishlist Tags: patch Hi,
I have created a script that can: - Connect to an IMAP server - Search for messages in a mailbox - Recognize caff-generated mail - Decrypt these messages if necessary - Import contained public key (signature) data into the GPG keyring It can be seen as the counter-part to caff for getting own signatures back after a signing party. I'd be happy to see it included in Debian. Feedback welcome. -nik -- System Information: Debian Release: 7.0 APT prefers unstable APT policy: (500, 'unstable'), (1, 'experimental') Architecture: amd64 (x86_64) Foreign Architectures: i386 Kernel: Linux 3.7-trunk-amd64 (SMP w/2 CPU cores) Locale: LANG=de_DE.UTF-8, LC_CTYPE=en_US.UTF-8 (charmap=UTF-8) Shell: /bin/sh linked to /bin/mksh Versions of packages signing-party depends on: ii gnupg 1.4.12-7 ii libc6 2.17-0experimental2 ii libclass-methodmaker-perl 2.18-1+b1 ii libgnupg-interface-perl 0.45-1 ii libmailtools-perl 2.09-1 ii libmime-tools-perl 5.503-1 ii libterm-readkey-perl 2.30-4+b2 ii libtext-template-perl 1.45-2 ii perl 5.14.2-18 ii qprint 1.0.dfsg.2-2 Versions of packages signing-party recommends: ii dialog 1.1-20120215-3 ii libgd-gd2-perl 1:2.46-3+b1 ii libpaper-utils 1.1.24+nmu2 ii libtext-iconv-perl 1.7-5 ii postfix [mail-transport-agent] 2.9.6-1 ii recode 3.6-20 ii whiptail 0.52.14-11.1 Versions of packages signing-party suggests: ii imagemagick 8:6.7.7.10-5 ii mutt 1.5.21-6.2 pn texlive-latex-recommended <none> pn wipe <none> -- no debconf information
#!/usr/bin/env python # ~*~ coding: utf-8 ~*~ #- # Copyright © 2013 # Dominik George <[email protected]> # # Provided that these terms and disclaimer and all copyright notices # are retained or reproduced in an accompanying document, permission # is granted to deal in this work without restriction, including un‐ # limited rights to use, publicly perform, distribute, sell, modify, # merge, give away, or sublicence. # # This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to # the utmost extent permitted by applicable law, neither express nor # implied; without malicious intent or gross negligence. In no event # may a licensor, author or contributor be held liable for indirect, # direct, other damage, loss, or other issues arising in any way out # of dealing in the work, even if advised of the possibility of such # damage or existence of a defect, except proven that it results out # of said person’s immediate fault when using the work as intended. """Import GPG key signatures from IMAP mailbox. """ import argparse, email, getpass, imaplib, logging, os, subprocess from ConfigParser import ConfigParser from gnupg import GPG # Set up output through logger log = logging.getLogger() ch = logging.StreamHandler() ch.setFormatter(logging.Formatter("%(asctime)s %(levelname)s - %(message)s")) log.addHandler(ch) # Set up argument parser argp = argparse.ArgumentParser( description=__doc__.split("\n")[0], epilog="""WARNING: If you let this script manipulate your INBOX, it's your own bloody fault. Arguments have reasonable default arguments and can also be given in a config file, normally ~/.gpg-import-imap.conf.""" ) argp.add_argument('-c', '--config', help='Path to a different configuration file', dest='config') argp.add_argument('-s', '--server', help='Hostname or address of the IMAP server, defaults to localhost', dest='server') argp.add_argument('-p', '--port', help='Port to connect to on the server, defaults to 143 or 993 (SSL)', dest='port', type=int) argp.add_argument('-m', '--mailbox', help='Name of the mailbox that holds the messages, defaults to INBOX', dest='mailbox') argp.add_argument('-u', '--user', help='Username for connecting to the IMAP server, defaults to your system user name', dest='user') argp.add_argument('--nossl', help='Do not use a secure connection, defaults to no', dest='nossl', action='store_true') argp.add_argument('--seen', help='Also look at already seen messages (may be slow!), defaults to no', dest='seen', action='store_true') argp.add_argument('--mark', help='Mark messages as seen, defaults to no. See warning at bottom.', dest='mark', action='store_true') argp.add_argument('--delete', help='Mark messages as deleted, defaults to no', dest='delete', action='store_true') argp.add_argument('--expunge', help='Expunge mailbox before exit, defaults to no', dest='expunge', action='store_true') argp.add_argument('--subject', help='Subject match for messages containing signatures, defaults to "Your signed PGP key "', dest='subject') argp.add_argument('--gpg', help='Path to the GPG binary, defaults to gpg from PATH', dest='gpg') argp.add_argument('--gpg-home', help='Path to the GPG home, defaults to your system default, normally ~/.gnupg', dest='gpghome') argp.add_argument('-a', '--all', help='Auto-import all signatures, defaults to no', dest='all', action='store_true') argp.add_argument('-d', '--debug', help='Produce debug output, defaults to no', dest='debug', action='store_true') # Parse arguments and, if it exists, config file args = argp.parse_args() config = ConfigParser() cp = args.config if args.config else os.path.join(os.path.expanduser("~"), ".gpg-import-imap.conf") if os.path.exists(cp): cf = open(cp, "rb") config.read(cf) cf.close() # Set config variables from command-line, or from config, if not given, or set defaults m_host = args.server if args.server else config.get("imap", "server") if config.has_option("imap", "server") else "localhost" m_ssl = not bool(args.nossl if args.nossl else config.get("imap", "nossl") if config.has_option("imap", "nossl") else True) m_port = int(args.port if args.port else config.get("imap", "port") if config.has_option("imap", "port") else 993 if m_ssl else 143) m_user = args.user if args.user else config.get("imap", "user") if config.has_option("imap", "user") else getpass.getuser() m_mbox = args.mailbox if args.mailbox else config.get("imap", "mailbox") if config.has_option("imap", "mailbox") else "INBOX" m_mark = bool(args.mark if args.mark else config.get("imap", "mark") if config.has_option("imap", "mark") else False) m_del = bool(args.delete if args.delete else config.get("imap", "delete") if config.has_option("imap", "delete") else False) m_exp = bool(args.expunge if args.expunge else config.get("imap", "expunge") if config.has_option("imap", "expunge") else False) f_seen = bool(args.seen if args.seen else config.get("filter", "seen") if config.has_option("filter", "seen") else False) f_subj = args.subject if args.subject else config.get("filter", "subject") if config.has_option("filter", "subject") else "Your signed PGP key " g_path = args.gpg if args.gpg else config.get("gpg", "path") if config.has_option("gpg", "path") else "gpg" g_home = args.gpghome if args.gpghome else config.get("gpg", "home") if config.has_option("gpg", "home") else None g_all = bool(args.all if args.all else config.get("gpg", "all") if config.has_option("gpg", "all") else False) p_dbg = bool(args.debug if args.debug else config.get("general", "debug") if config.has_option("general", "debug") else False) # Set debug log level if desired if p_dbg: log.setLevel(logging.DEBUG) # Get login information log.info("Logging in as %s to %s:%d for mailbox %s, %s SSL." % (m_user, m_host, m_port, m_mbox, "using" if m_ssl else "not using")) m_pass = getpass.getpass() # Use SSL or not if m_ssl: log.debug("Using SSL IMAP4 object.") m = imaplib.IMAP4_SSL(m_host, m_port) else: log.debug("Using default IMAP4 object.") m = imaplib.IMAP4L(m_host, m_port) # Do login with PLAIN or LOGIN log.info("Logging in to IMAP server.") m.login(m_user, m_pass) # Select mailbox log.info("Selecting mailbox %s %s." % (m_mbox, "read-write" if m_mark or m_del else "read-only")) status, messages = m.select(m_mbox, readonly=(not (m_mark or m_del))) if not status == "OK": log.error("Mailbox %s not found." % m_mbox) exit(1) # Search for messages qualified for analysis criteria = 'UNSEEN ' if not f_seen else 'ALL ' criteria += 'SUBJECT "%s"' % f_subj log.debug("Searching for messages meeting criteria %s." % criteria) status, messages = m.search(None, "(%s)" % criteria) if not status == "OK": log.error("Searching for %s failed." % criteria) exit(1) # Set up GnuPG log.debug("Starting up GnuPG as %s in %s." % (g_path, g_home)) gpg = GPG(gpgbinary=g_path, gnupghome=g_home, use_agent=True) # Find key blocks pkeys = [] for num in messages[0].split(): # Fetch and parse a message log.debug("Fetching message %s." % num) status, messages = m.fetch(num, '(RFC822)') log.debug("Parsing message.") msg = email.message_from_string(messages[0][1]) # Iterate through MIME parts for part in msg.walk(): if part.is_multipart(): log.debug("Skipping multi-part content.") continue elif part.get_payload().strip().startswith("-----BEGIN PGP MESSAGE-----"): # A PGP-encrypted part might contian a key block log.debug("Decrypting pgp-encrypted message part.") decrypted = gpg.decrypt(part.get_payload()) dmsg = email.message_from_string(decrypted.data) # Iterate through MIME parts of the encrypted parent part for dpart in dmsg.walk(): if dpart.is_multipart(): log.debug("Skipping inner multi-part content.") continue elif dpart.get_payload().strip().startswith("-----BEGIN PGP PUBLIC KEY BLOCK-----"): # A key block was found log.info("Found a public key block in message %s." % num) pkeys.append((num, dpart.get_payload())) elif part.get_payload().strip().startswith("-----BEGIN PGP PUBLIC KEY BLOCK-----"): # An unencrypted key block was found log.info("Found a public key block in message %s." % num) pkeys.append((num, part.get_payload())) # Iterate over key blocks and try to import count = 0 for num, key in pkeys: if not g_all: # Make gpg output key info from file pipe = subprocess.Popen([gpg.gpgbinary, '--verbose'], stdin=subprocess.PIPE) pipe.communicate(input=key) res = raw_input("\nImport this signature? [Y/n] ") else: log.warn("Not asking any questions as --all was specified!") res = "y" if res.strip() == "" or res.strip().lower().startswith("y"): # Really do the import log.info("Importing key data.") res = gpg.import_keys(key) if not res and not res.count > 0: log.error("Importing key from message %s failed." % num) else: if m_del: # Mark message deleted log.info("Marking message %s deleted." % num) status, messages = m.store(num, '+FLAGS', '\\Deleted') if not status == "OK": log.error("Could not mark message %s deleted." % num) count += res.count else: if m_del: # Message was not imported, but user may still want to delete it res = raw_input("Still delete message? [y/N] ") if res.strip().lower().startswith("y"): log.info("Marking message %s deleted." % num) status, messages = m.store(num, '+FLAGS', '\\Deleted') if not status == "OK": log.error("Could not mark message %s deleted." % num) log.info("Imported %d signatures." % count) if m_exp: # Expunge mailbox if desired log.info("Expunging mailbox.") status, messages = m.expunge() if status == "OK": log.info("Mailbox expunged.") else: log.error("Error expunging mailbox.") exit(0)

