Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing keys - rules and notifications - 2nd try #67

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 155 additions & 43 deletions tests/test_zeyple.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
import unittest
from mock import Mock
import os
import sys
import subprocess
import shutil
import re
from six.moves.configparser import ConfigParser
import tempfile
from textwrap import dedent
from zeyple import zeyple
from io import StringIO
from zeyple.zeyple import Zeyple, get_config_from_file_handle

legacy_gpg = False
try:
Expand All @@ -30,6 +32,27 @@
TEST_EXPIRED_ID = 'ED97E21F1C7F1AC6'
TEST_EXPIRED_EMAIL = '[email protected]'


DEFAULT_CONFIG_TEMPLATE = """
[gpg]
home = {0}

[relay]
host = example.net
port = 2525

[zeyple]
log_file = {1}
add_header = true
"""


def get_test_email():
filename = os.path.join(os.path.dirname(__file__), 'test.eml')
with open(filename, 'r') as test_file:
return test_file.read()


class ZeypleTest(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
Expand All @@ -38,30 +61,21 @@ def setUp(self):
self.homedir = os.path.join(self.tmpdir, 'gpg')
self.logfile = os.path.join(self.tmpdir, 'zeyple.log')

config = ConfigParser()

config.add_section('zeyple')
config.set('zeyple', 'log_file', self.logfile)
config.set('zeyple', 'add_header', 'true')

config.add_section('gpg')
config.set('gpg', 'home', self.homedir)

config.add_section('relay')
config.set('relay', 'host', 'example.net')
config.set('relay', 'port', '2525')

with open(self.conffile, 'w') as fp:
config.write(fp)

os.mkdir(self.homedir, 0o700)
subprocess.check_call(
['gpg', '--homedir', self.homedir, '--import', KEYS_FNAME],
stderr=open('/dev/null'),
)

self.zeyple = zeyple.Zeyple(self.conffile)
self.zeyple._send_message = Mock() # don't try to send emails
def get_zeyple(self, config_template=None):
if config_template is None:
config_template = DEFAULT_CONFIG_TEMPLATE
config_text = config_template.format(self.homedir, self.logfile)
handle = StringIO(config_text)
config = get_config_from_file_handle(handle)
zeyple = Zeyple(config)
zeyple._send_message = Mock() # don't try to send emails
return zeyple

def tearDown(self):
shutil.rmtree(self.tmpdir)
Expand Down Expand Up @@ -100,31 +114,34 @@ def assertValidMimeMessage(self, cipher_message, mime_message):
def test_user_key(self):
"""Returns the right ID for the given email address"""

assert self.zeyple._user_key('[email protected]') is None
zeyple = self.get_zeyple()
assert zeyple._user_key('[email protected]') is None

user_key = self.zeyple._user_key(TEST1_EMAIL)
user_key = zeyple._user_key(TEST1_EMAIL)
assert user_key == TEST1_ID

def test_encrypt_with_plain_text(self):
"""Encrypts plain text"""
content = 'The key is under the carpet.'.encode('ascii')
encrypted = self.zeyple._encrypt_payload(content, [TEST1_ID])
zeyple = self.get_zeyple()
encrypted = zeyple._encrypt_payload(content, [TEST1_ID])
assert self.decrypt(encrypted) == content

def test_expired_key(self):
"""Encrypts with expired key"""
content = 'The key is under the carpet.'.encode('ascii')
successful = None
zeyple = self.get_zeyple()

if legacy_gpg:
try:
self.zeyple._encrypt_payload(content, [TEST_EXPIRED_ID])
zeyple._encrypt_payload(content, [TEST_EXPIRED_ID])
successful = True
except gpgme.GpgmeError as error:
assert str(error) == 'Key with user email %s is expired!'.format(TEST_EXPIRED_EMAIL)
else:
try:
self.zeyple._encrypt_payload(content, [TEST_EXPIRED_ID])
zeyple._encrypt_payload(content, [TEST_EXPIRED_ID])
successful = True
except gpg.errors.GPGMEError as error:
assert error.error == 'Key with user email %s is expired!'.format(TEST_EXPIRED_EMAIL)
Expand All @@ -134,7 +151,8 @@ def test_expired_key(self):
def test_encrypt_binary_data(self):
"""Encrypts utf-8 characters"""
content = b'\xc3\xa4 \xc3\xb6 \xc3\xbc'
encrypted = self.zeyple._encrypt_payload(content, [TEST1_ID])
zeyple = self.get_zeyple()
encrypted = zeyple._encrypt_payload(content, [TEST1_ID])
assert self.decrypt(encrypted) == content

def test_process_message_with_simple_message(self):
Expand All @@ -147,7 +165,7 @@ def test_process_message_with_simple_message(self):
test
--BOUNDARY--""")

email = self.zeyple.process_message(dedent("""\
email = self.get_zeyple().process_message(dedent("""\
Received: by example.org (Postfix, from userid 0)
id DD3B67981178; Thu, 6 Sep 2012 23:35:37 +0000 (UTC)
To: """ + TEST1_EMAIL + """
Expand All @@ -171,7 +189,7 @@ def test_process_message_with_unicode_message(self):
ä ö ü
--BOUNDARY--""")

email = self.zeyple.process_message(dedent("""\
email = self.get_zeyple().process_message(dedent("""\
Received: by example.org (Postfix, from userid 0)
id DD3B67981178; Thu, 6 Sep 2012 23:35:37 +0000 (UTC)
To: """ + TEST1_EMAIL + """
Expand Down Expand Up @@ -209,7 +227,7 @@ def test_process_message_with_multipart_message(self):
Yy90ZXN0JyB3d3ctZGF0YQo=
--BOUNDARY--""")

email = self.zeyple.process_message((dedent("""\
email = self.get_zeyple().process_message((dedent("""\
Return-Path: <[email protected]>
Received: by example.org (Postfix, from userid 0)
id CE9876C78258; Sat, 8 Sep 2012 13:00:18 +0000 (UTC)
Expand All @@ -229,7 +247,7 @@ def test_process_message_with_multipart_message(self):
def test_process_message_with_multiple_recipients(self):
"""Encrypt a message with multiple recipients"""

emails = self.zeyple.process_message(dedent("""\
emails = self.get_zeyple().process_message(dedent("""\
Received: by example.org (Postfix, from userid 0)
id DD3B67981178; Thu, 6 Sep 2012 23:35:37 +0000 (UTC)
To: """ + ', '.join([TEST1_EMAIL, TEST2_EMAIL]) + """
Expand All @@ -244,26 +262,120 @@ def test_process_message_with_multiple_recipients(self):

def test_process_message_with_complex_message(self):
"""Encrypts complex messages"""
contents = get_test_email()
self.get_zeyple().process_message(contents, [TEST1_EMAIL]) # should not raise

filename = os.path.join(os.path.dirname(__file__), 'test.eml')
with open(filename, 'r') as test_file:
contents = test_file.read()
def test_force_encryption_deprecated(self):
"""Tries to encrypt without key"""
contents = get_test_email()
zeyple = self.get_zeyple(DEFAULT_CONFIG_TEMPLATE + '\nforce_encrypt = 1\n')

self.zeyple.process_message(contents, [TEST1_EMAIL]) # should not raise
sent_messages = zeyple.process_message(contents, ['[email protected]'])
assert len(sent_messages) == 0

def test_force_encryption(self):
"""Tries to encrypt without key"""
filename = os.path.join(os.path.dirname(__file__), 'test.eml')
with open(filename, 'r') as test_file:
contents = test_file.read()
sent_messages = zeyple.process_message(contents, [TEST1_EMAIL])
assert len(sent_messages) == 1

def test_missing_key_notify(self):
contents = get_test_email()
zeyple = self.get_zeyple(
DEFAULT_CONFIG_TEMPLATE + dedent("""\
[missing_key_rules]
. = notify
""")
)

sent_messages = zeyple.process_message(contents, ['[email protected]'])
assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == 'Missing PGP key'

sent_messages = zeyple.process_message(contents, [TEST1_EMAIL])
assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == 'Verify Email'

def test_missing_key_drop(self):
contents = get_test_email()
zeyple = self.get_zeyple(
DEFAULT_CONFIG_TEMPLATE + dedent("""\
[missing_key_rules]
. = drop
""")
)

sent_messages = zeyple.process_message(contents, ['[email protected]'])
assert len(sent_messages) == 0

sent_messages = zeyple.process_message(contents, [TEST1_EMAIL])
assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == 'Verify Email'

def test_missing_key_drop(self):
contents = get_test_email()
zeyple = self.get_zeyple(
DEFAULT_CONFIG_TEMPLATE + dedent("""\
[missing_key_rules]
. = cleartext
""")
)

# set force_encrypt
self.zeyple.config.set('zeyple', 'force_encrypt', '1')
sent_messages = zeyple.process_message(contents, ['[email protected]'])
assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == 'Verify Email'

sent_messages = self.zeyple.process_message(contents, ['[email protected]'])
sent_messages = zeyple.process_message(contents, [TEST1_EMAIL])
assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == 'Verify Email'

def test_missing_key_complex_config(self):
contents = get_test_email()
zeyple = self.get_zeyple(
DEFAULT_CONFIG_TEMPLATE + dedent("""\
[missing_key_rules]
erno\\.testibus\\@example\\.com = cleartext
frida\\.testibus\\@example\\.com = notify
.*\\@example\\.com = drop
. = cleartext
""")
)

sent_messages = zeyple.process_message(contents, ['[email protected]'])
assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == 'Verify Email'

sent_messages = zeyple.process_message(contents, ['[email protected]'])
assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == 'Missing PGP key'

sent_messages = zeyple.process_message(contents, ['[email protected]'])
assert len(sent_messages) == 0

sent_messages = self.zeyple.process_message(contents, [TEST1_EMAIL])
sent_messages = zeyple.process_message(contents, ['[email protected]'])
assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == 'Verify Email'

def test_custom_missing_key_message(self):
contents = get_test_email()
missing_key_message_file = os.path.join(self.tmpdir, 'missing_key_message')
subject = 'No key dude!'
body = 'xxxYYYzzzäöü'

self.zeyple.config.remove_option('zeyple', 'force_encrypt')
if sys.version_info >= (3, 0):
with open(missing_key_message_file, 'w', encoding='utf-8') as out:
out.write(body + '\n')
else:
with open(missing_key_message_file, 'w') as out:
out.write((body + '\n').encode('utf-8'))
zeyple = self.get_zeyple(
DEFAULT_CONFIG_TEMPLATE + dedent("""\
missing_key_notification_file = {0}
missing_key_notification_subject = {1}
""").format(missing_key_message_file, subject)
)

sent_messages = zeyple.process_message(contents, ['[email protected]'])

assert len(sent_messages) == 1
assert sent_messages[0]['Subject'] == subject
assert sent_messages[0]['Content-Type'] == 'text/plain; charset="utf-8"'
payload = sent_messages[0].get_payload(decode=True)
assert body in payload.decode('utf-8')
45 changes: 44 additions & 1 deletion zeyple/zeyple.conf.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
[zeyple]
log_file = /var/log/zeyple.log
force_encrypt = 1

### File name containing the body a notification email to be sent
### when no key is found. There is a nice default, however you
### may wish to give the recipient more specific instruction
### how to get his key on your system. The file must be encoded
### in UTF-8:

# missing_key_notification_file = /etc/zeyple.notify

### Subject for this email:

# missing_key_notification_subject = Fix it!

[gpg]
home = /var/lib/zeyple/keys
Expand All @@ -9,3 +20,35 @@ home = /var/lib/zeyple/keys
host = localhost
port = 10026

[missing_key_rules]
### This rules define, what to do if the recipient has no key. The rules
### have the format:
###
### <regexp> = drop|notify|cleartext
###
### The recipient address will be matched against <regexp> using the Python
### module 're'. The actions have this effect:
###
### - drop: Discards the message silently.
### - notify: Send a notification to the recipient
###
### Examples:
###
### The toystory people never get important stuff and don't know how to use PGP:
###
### .*\@toystory\.com$ = cleartext
###
### The new default - always notify:
###
### . = notify
###
### The old behavior with "force_encrypt = 1" - in doubt drop the message:
###
### . = drop
###
### The old behavior with "force_encrypt = 0" - sent messages without encryption
### if we lack the key:
###
### . = cleartext
###
### Order matters! First match wins - at least with Python 2.7+.
Loading