| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 | #!/usr/bin/env python# -*- coding: utf-8 -*-import sysimport osimport loggingimport emailimport email.mime.multipartimport email.mime.applicationimport email.encodersimport smtplibimport copyfrom io import BytesIOtry:    from configparser import SafeConfigParser  # Python 3except ImportError:    from ConfigParser import SafeConfigParser  # Python 2import gpgme# Boiler plate to avoid dependency on six# BBB: Python 2.7 supportPY3K = sys.version_info > (3, 0)def message_from_binary(message):    if PY3K:        return email.message_from_bytes(message)    else:        return email.message_from_string(message)def as_binary_string(email):    if PY3K:        return email.as_bytes()    else:        return email.as_string()def encode_string(string):    if isinstance(string, bytes):        return string    else:        return string.encode('utf-8')__title__ = 'Zeyple'__version__ = '1.2.0'__author__ = 'Cédric Félizard'__license__ = 'AGPLv3+'__copyright__ = 'Copyright 2012-2016 Cédric Félizard'class Zeyple:    """Zeyple Encrypts Your Precious Log Emails"""    def __init__(self, config_fname='zeyple.conf'):        self.config = self.load_configuration(config_fname)        log_file = self.config.get('zeyple', 'log_file')        logging.basicConfig(            filename=log_file, level=logging.DEBUG,            format='%(asctime)s %(process)s %(levelname)s %(message)s'        )        logging.info("Zeyple ready to encrypt outgoing emails")    def load_configuration(self, filename):        """Reads and parses the config file"""        config = SafeConfigParser()        config.read([            os.path.join('/etc/', filename),            filename,        ])        if not config.sections():            raise IOError('Cannot open config file.')        return config    @property    def gpg(self):        protocol = gpgme.PROTOCOL_OpenPGP        if self.config.has_option('gpg', 'executable'):            executable = self.config.get('gpg', 'executable')        else:            executable = None  # Default value        home_dir = self.config.get('gpg', 'home')        ctx = gpgme.Context()        ctx.set_engine_info(protocol, executable, home_dir)        ctx.armor = True        return ctx    def process_message(self, message_data, recipients):        """Encrypts the message with recipient keys"""        message_data = encode_string(message_data)        in_message = message_from_binary(message_data)        logging.info(            "Processing outgoing message %s", in_message['Message-id'])        if not recipients:            logging.warn("Cannot find any recipients, ignoring")        sent_messages = []        for recipient in recipients:            logging.info("Recipient: %s", recipient)            key_id = self._user_key(recipient)            logging.info("Key ID: %s", key_id)            if key_id:                out_message = self._encrypt_message(in_message, key_id)                # Delete Content-Transfer-Encoding if present to default to                # "7bit" otherwise Thunderbird seems to hang in some cases.                del out_message["Content-Transfer-Encoding"]            else:                logging.warn("No keys found, message will be sent unencrypted")                out_message = copy.copy(in_message)            self._add_zeyple_header(out_message)            self._send_message(out_message, recipient)            sent_messages.append(out_message)        return sent_messages    def _get_version_part(self):        ret = email.mime.application.MIMEApplication(            'Version: 1\n',            'pgp-encrypted',            email.encoders.encode_noop,        )        ret.add_header(            'Content-Description',            "PGP/MIME version identification",        )        return ret    def _get_encrypted_part(self, payload):        ret = email.mime.application.MIMEApplication(            payload,            'octet-stream',            email.encoders.encode_noop,            name="encrypted.asc",        )        ret.add_header('Content-Description', "OpenPGP encrypted message")        ret.add_header(            'Content-Disposition',            'inline',            filename='encrypted.asc',        )        return ret    def _encrypt_message(self, in_message, key_id):        if in_message.is_multipart():            # get the body (after the first \n\n)            payload = in_message.as_string().split("\n\n", 1)[1].strip()            # prepend the Content-Type including the boundary            content_type = "Content-Type: " + in_message["Content-Type"]            payload = content_type + "\n\n" + payload            message = email.message.Message()            message.set_payload(payload)            payload = message.get_payload()        else:            payload = in_message.get_payload()            payload = encode_string(payload)            quoted_printable = email.charset.Charset('ascii')            quoted_printable.body_encoding = email.charset.QP            message = email.mime.nonmultipart.MIMENonMultipart(                'text', 'plain', charset='utf-8'            )            message.set_payload(payload, charset=quoted_printable)            mixed = email.mime.multipart.MIMEMultipart(                'mixed',                None,                [message],            )            # remove superfluous header            del mixed['MIME-Version']            payload = as_binary_string(mixed)        encrypted_payload = self._encrypt_payload(payload, [key_id])        version = self._get_version_part()        encrypted = self._get_encrypted_part(encrypted_payload)        out_message = copy.copy(in_message)        out_message.preamble = "This is an OpenPGP/MIME encrypted " \                               "message (RFC 4880 and 3156)"        if 'Content-Type' not in out_message:            out_message['Content-Type'] = 'multipart/encrypted'        else:            out_message.replace_header(                'Content-Type',                'multipart/encrypted',            )        out_message.set_param('protocol', 'application/pgp-encrypted')        out_message.set_payload([version, encrypted])        return out_message    def _encrypt_payload(self, payload, key_ids):        """Encrypts the payload with the given keys"""        payload = encode_string(payload)        plaintext = BytesIO(payload)        ciphertext = BytesIO()        self.gpg.armor = True        recipient = [self.gpg.get_key(key_id) for key_id in key_ids]        self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST,                         plaintext, ciphertext)        return ciphertext.getvalue()    def _user_key(self, email):        """Returns the GPG key for the given email address"""        logging.info("Trying to encrypt for %s", email)        keys = [key for key in self.gpg.keylist(email)]        if keys:            key = keys.pop()  # NOTE: looks like keys[0] is the master key            key_id = key.subkeys[0].keyid            return key_id        return None    def _add_zeyple_header(self, message):        if self.config.has_option('zeyple', 'add_header') and \           self.config.getboolean('zeyple', 'add_header'):            message.add_header(                'X-Zeyple',                "processed by {0} v{1}".format(__title__, __version__)            )    def _send_message(self, message, recipient):        """Sends the given message through the SMTP relay"""        logging.info("Sending message %s", message['Message-id'])        smtp = smtplib.SMTP(self.config.get('relay', 'host'),                            self.config.get('relay', 'port'))        smtp.sendmail(message['From'], recipient, message.as_string())        smtp.quit()        logging.info("Message %s sent", message['Message-id'])if __name__ == '__main__':    recipients = sys.argv[1:]    # BBB: Python 2.7 support    binary_stdin = sys.stdin.buffer if PY3K else sys.stdin    message = binary_stdin.read()    zeyple = Zeyple()    zeyple.process_message(message, recipients)
 |