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)

Reply via email to