#!/usr/bin/python3
"""
This tool filters a list of key ids from your gpg keyring as input to gpgparticipants.

It sorts the alphabetically list by the first uid of each key. Per default it skips over revoked and expired keys.
The output can be piped into gpgparticipants or stored in a file.
"""

__version__ = "1.0"
__author__ = "Max Harmathy"
__email__ = "max.harmathy@web.de"
__copyright__ = "Copyright 2019, Max Harmathy"
__license__ = "MIT"

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

import argparse
import subprocess
import sys


class RawDataSet:
    """
    Hold raw output of gpg for one key.
    """

    def __init__(self, data):
        """
        :param data: list of lines of raw output of gpg --with-colons
        """
        self.data = data

    def has_line_with(self, prefix):
        return any(line.startswith(prefix.encode()) for line in self.data)

    def is_key_set(self):
        """sanity check for a data set of a key"""
        return self.has_line_with("pub") and self.has_line_with("fpr") and self.has_line_with("uid")

    def data_field_from_lines(self, field_index, line_prefix):
        """Select all lines which start with line_prefix and return the columns denoted by field_index."""
        return [line.split(b':')[field_index].decode() for line in self.data if line.startswith(line_prefix)]


class KeyDataSet:
    """
    Hold processed key data.
    """

    def __init__(self, key_id, uids, valid_flag):
        self.key_id = key_id
        self.uids = uids
        self.valid_flag = valid_flag

    def __repr__(self):
        return self.key_id

    def is_valid(self):
        """check that the key isn't revoked or expired"""
        return self.valid_flag not in ['e', 'r']


def filter_to_gpg(filter_string, gpg_bin, set_homedir, homedir):
    """
    Call gpg and return output.
    """

    command = [
        gpg_bin,
        "--with-colons",
        "--fingerprint",
    ]

    if set_homedir:
        command.append("--homedir")
        command.append(homedir)

    command.append(filter_string)

    with subprocess.Popen(command, stdout=subprocess.PIPE) as process:
        return process.stdout.readlines()


def slpit_raw_key_data(gpg_output):
    """
    Split the whole output of gpg into fragments, each belonging to one key.
    """
    acc = []
    for line in gpg_output:
        if line.startswith(b'pub'):
            yield RawDataSet(acc)
            acc = []
        acc.append(line)
    yield RawDataSet(acc)


def key_data_from(raw_data_set):
    """process a raw data set into a key data set"""
    return KeyDataSet(
        raw_data_set.data_field_from_lines(9, b'fpr')[0],
        raw_data_set.data_field_from_lines(9, b'uid'),
        raw_data_set.data_field_from_lines(9, b'pub')[0],
    )


def key_data_sets_from(gpg_output):
    for raw_data_set in slpit_raw_key_data(gpg_output):
        if raw_data_set.is_key_set():
            yield key_data_from(raw_data_set)


def sorted_by_name(key_list):
    return sorted(key_list, key=lambda key_data_set: key_data_set.uids[0])


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)

    parser.add_argument('-o', '--output', metavar="FILE", default=sys.stdout, type=argparse.FileType('w'),
                        help='specifies the file where the output should be written to (per default writes to stdout)'
                        )
    parser.add_argument('-b', '--gpg-binary', metavar="GPG_BIN", default="gpg",
                        help='custom path to gpg (default: "gpg")'
                        )
    parser.add_argument('-d', '--homedir', metavar="GPG_HOME",
                        help='custom homedir for gpg (see --homedir in gpg manual)'
                        )
    parser.add_argument('-a', '--all-keys', action='store_true',
                        help='show also revoked and expired keys'
                        )
    parser.add_argument('--version', action='version',
                        help='output version information and exit',
                        version="%(prog)s {version}".format(version=__version__)
                        )
    parser.add_argument('filter', type=str, nargs='+', metavar="FILTER",
                        help='input query, which will be passed to gpg (see gpg manual)')

    args = parser.parse_args()
    key_data = []
    for input_string in args.filter:
        gpg_output = filter_to_gpg(
            filter_string=input_string,
            gpg_bin=args.gpg_binary,
            set_homedir=args.homedir is not None,
            homedir=args.homedir
        )
        key_data += key_data_sets_from(gpg_output)

    for key in sorted_by_name(key_data):
        if key.is_valid() or args.all_keys:
            print(key, file=args.output)
    args.output.close()
