275 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			275 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env python
 | 
						|
# -*- coding: utf-8 -*-
 | 
						|
 | 
						|
import sys
 | 
						|
import os
 | 
						|
import logging
 | 
						|
import email
 | 
						|
import email.mime.multipart
 | 
						|
import email.mime.application
 | 
						|
import email.encoders
 | 
						|
import smtplib
 | 
						|
import copy
 | 
						|
from io import BytesIO
 | 
						|
 | 
						|
try:
 | 
						|
    from configparser import SafeConfigParser  # Python 3
 | 
						|
except ImportError:
 | 
						|
    from ConfigParser import SafeConfigParser  # Python 2
 | 
						|
 | 
						|
import gpgme
 | 
						|
 | 
						|
# Boiler plate to avoid dependency on six
 | 
						|
# BBB: Python 2.7 support
 | 
						|
PY3K = 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)
 |