Browse Source

Add additional email parsing and tests

Added additional attribute parsing and corresponding unit-tests.
E-mail attachment and url extraction added in this commit. This includes
unpacking zipfiles and simple password cracking of encrypted zipfiles.
pull/62/head
seamus tuohy 5 years ago
parent
commit
1a7973bc06
  1. 123
      misp_modules/modules/import_mod/email_import.py
  2. 1
      tests/EICAR.com
  3. BIN
      tests/EICAR.com.zip
  4. BIN
      tests/infected.zip
  5. BIN
      tests/longer_password.zip
  6. BIN
      tests/short_password.zip
  7. 481
      tests/test.py
  8. 167
      tests/test_attachment.eml
  9. 144
      tests/test_no_attach.eml

123
misp_modules/modules/import_mod/email_import.py

@ -21,12 +21,10 @@ moduleinfo = {'version': '0.1',
'description': 'Email import module for MISP',
'module-type': ['import']}
# treat_attachments_as_malware : This treats all attachments as malware. This will zip all attachments and password protect using the password 'infected'
# unzip_attachments : Unzip all zip files that are not password protected
# guess_zip_attachment_passwords : This attempts to unzip all password protected zip files using all the strings found in the email body and subject
# extract_urls : This attempts to extract all URL's from text/html parts of the email
moduleconfig = ["treat_attachments_as_malware",
"unzip_attachments",
moduleconfig = ["unzip_attachments",
"guess_zip_attachment_passwords",
"extract_urls"]
@ -45,7 +43,7 @@ def handler(q=False):
# Extract all header information
all_headers = ""
for k, v in message.items():
all_headers += "\n{0}: {1}".format(k, v)
all_headers += "{0}: {1}\n".format(k, v)
results.append({"values": all_headers,
"types": ['email-header']})
@ -77,14 +75,10 @@ def handler(q=False):
from_addr = message.get('From')
results.append({"values": parseaddr(from_addr)[1],
"types": ['email-src'],
"comment": "From: {0}".format(re.sub('["\']',
'',
from_addr))})
results.append({"values": parseaddr(from_addr)[1],
"comment": "From: {0}".format(from_addr)})
results.append({"values": parseaddr(from_addr)[0],
"types": ['email-src-display-name'],
"comment": "From: {0}".format(re.sub('["\']',
'',
from_addr))})
"comment": "From: {0}".format(from_addr)})
# Return Path
return_path = message.get('Return-Path')
@ -111,15 +105,11 @@ def handler(q=False):
results.append({"values": parsed_addr[1],
"types": ["email-dst"],
"comment": "{0}: {1}".format(hdr_val,
re.sub('["\']',
'',
addr))})
addr)})
results.append({"values": parsed_addr[0],
"types": ["email-dst-display-name"],
"comment": "{0}: {1}".format(hdr_val,
re.sub('["\']',
'',
addr))})
addr)})
except AttributeError:
continue
@ -127,45 +117,45 @@ def handler(q=False):
# Get E-Mail Targets
# Get the addresses that received the email.
# As pulled from the Received header
received = message.get_all('received')
received = message.get_all('Received')
email_targets = set()
for rec in received:
try:
email_check = re.search("for\s(.*@.*);", rec).group(1)
email_check = email_check.strip(' <>')
email_targets.add(parseaddr(email_check)[1])
except (AttributeError):
continue
for tar in email_targets:
results.append({"values": tar,
"types": ["target-email"],
"comment": "Extracted from email 'Received' header"})
try:
for rec in received:
try:
email_check = re.search("for\s(.*@.*);", rec).group(1)
email_check = email_check.strip(' <>')
email_targets.add(parseaddr(email_check)[1])
except (AttributeError):
continue
for tar in email_targets:
results.append({"values": tar,
"types": ["target-email"],
"comment": "Extracted from email 'Received' header"})
except TypeError:
pass # If received header is missing we can't iterate over NoneType
# Check if we were given a configuration
config = request.get("config", {})
# Don't be picky about how the user chooses to say yes to these
acceptable_config_yes = ['y', 'yes', 'true', 't']
# Do we treat all attachments as malware
treat_attachments_as_malware = config.get("treat_attachments_as_malware",
"false")
if treat_attachments_as_malware.lower() in acceptable_config_yes:
treat_attachments_as_malware = True
# Do we unzip attachments we find?
unzip = config.get("unzip_attachments", "false")
if unzip.lower() in acceptable_config_yes:
unzip = config.get("unzip_attachments", None)
if (unzip is not None and
unzip.lower() in acceptable_config_yes):
unzip = True
# Do we try to find passwords for protected zip files?
zip_pass_crack = config.get("guess_zip_attachment_passwords", "false")
if zip_pass_crack.lower() in acceptable_config_yes:
zip_pass_crack = config.get("guess_zip_attachment_passwords", None)
if (zip_pass_crack is not None and
zip_pass_crack.lower() in acceptable_config_yes):
zip_pass_crack = True
password_list = None # Only want to collect password list once
# Do we extract URL's from the email.
extract_urls = config.get("extract_urls", "false")
if extract_urls.lower() in acceptable_config_yes:
extract_urls = config.get("extract_urls", None)
if (extract_urls is not None and
extract_urls.lower() in acceptable_config_yes):
extract_urls = True
# Get Attachments
@ -174,41 +164,35 @@ def handler(q=False):
filename = part.get_filename()
if filename is not None:
attachment_data = part.get_payload(decode=True)
# Base attachment data is default
attachment_files = [{"values": filename,
"data" : base64.b64encode(attachment_data).decode()}]
if unzip is True: # Attempt to unzip the attachment and return its files
try:
attachment_files = get_zipped_contents(filename,
attachment_files += get_zipped_contents(filename,
attachment_data)
except RuntimeError: # File is encrypted with a password
if zip_pass_crack is True:
if password_list is None:
password_list = get_zip_passwords(message)
password = test_zip_passwords(attachment_data, password_list)
# If we don't guess the password just use the zip
if password is None:
attachment_files = [{"values": filename,
"data" : base64.b64encode(attachment_data),
"comment":"Password could not be cracked from message"}]
if password is None: # Inform the analyst that we could not crack password
attachment_files[0]['comment'] = "Encrypted Zip: Password could not be cracked from message"
else:
attachment_files = get_zipped_contents(filename,
attachment_files[0]['comment'] = """Original Zipped Attachment with Password {0}""".format(password)
attachment_files += get_zipped_contents(filename,
attachment_data,
password=password)
except zipfile.BadZipFile: # Attachment is not a zipfile
attachment_files = [{"values": filename,
"data" : base64.b64encode(attachment_data)}]
else:
attachment_files = [{"values": filename,
"data" : base64.b64encode(attachment_data)}]
attachment_files += [{"values": filename,
"data" : base64.b64encode(attachment_data).decode()}]
for attch_item in attachment_files:
if treat_attachments_as_malware is True: # Malware-samples are encrypted by server
attch_item["types"] = ['malware-sample']
else:
attch_item["types"] = ['attachment']
attch_item["types"] = ['attachment']
results.append(attch_item)
else: # Check email body part for urls
if (extract_urls is True and part.get_content_type() == 'text/html'):
url_parser = HTMLURLParser()
charset = get_charset(i, get_charset(message))
charset = get_charset(part, get_charset(message))
url_parser.feed(part.get_payload(decode=True).decode(charset))
urls = url_parser.urls
for url in urls:
@ -235,11 +219,11 @@ def get_zipped_contents(filename, data, password=None):
unzipped_files = []
if password is not None:
password = str.encode(password) # Byte encoded password required
for zip_file_name in zf: # Get all files in the zip file
for zip_file_name in zf.namelist(): # Get all files in the zip file
with zf.open(zip_file_name, mode='rU', pwd=password) as fp:
file_data = fp.read()
unzipped_files.append({"values": zip_file_name,
"data" : base64.b64encode(zf.open(zip_file_name,
mode='rU',
pwd=password)), # Any password works when not encrypted
"data" : base64.b64encode(file_data).decode(), # Any password works when not encrypted
"comment": "Extracted from {0}".format(filename)})
return unzipped_files
@ -256,11 +240,12 @@ def test_zip_passwords(data, test_passwords):
"""
with zipfile.ZipFile(io.BytesIO(data), "r") as zf:
firstfile = zf.namelist()[0]
for pw_test in test_passwords:
byte_pwd = str.encode(pw_test)
try:
zf.testzip()
return byte_pwd
zf.open(firstfile, pwd=byte_pwd)
return pw_test
except RuntimeError: # Incorrect Password
continue
return None
@ -315,10 +300,10 @@ def get_zip_passwords(message):
raw_text += subject
# Grab any strings that are marked off by special chars
marking_chars = [["'", "'"], ['"', '"'], ['[', ']'], ['(', ')']]
marking_chars = [["\'", "\'"], ['"', '"'], ['[', ']'], ['(', ')']]
for char_set in marking_chars:
regex = re.compile("'{0}([^{1}]*){1}'".format(char_set[0],
char_set[1]))
regex = re.compile("""\{0}([^\{1}]*)\{1}""".format(char_set[0],
char_set[1]))
marked_off = re.findall(regex, raw_text)
possible_passwords += marked_off
@ -350,7 +335,7 @@ class HTMLURLParser(HTMLParser):
if urls is None:
self.urls = []
else:
self.urls = output_list
self.urls = urls
def handle_starttag(self, tag, attrs):
if tag == 'a':
self.urls.append(dict(attrs).get('href'))

1
tests/EICAR.com

@ -0,0 +1 @@
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-

BIN
tests/EICAR.com.zip

Binary file not shown.

BIN
tests/infected.zip

Binary file not shown.

BIN
tests/longer_password.zip

Binary file not shown.

BIN
tests/short_password.zip

Binary file not shown.

481
tests/test.py

@ -5,8 +5,11 @@ import unittest
import requests
import base64
import json
import os
import urllib
import io
import zipfile
from email.mime.application import MIMEApplication
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
class TestModules(unittest.TestCase):
@ -53,164 +56,298 @@ class TestModules(unittest.TestCase):
assert("eu-society.com" in values)
def test_email_headers(self):
with open("tests/test_no_attach.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(bytes(f.read(), 'utf8')),
'utf8')}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": None,
"guess_zip_attachment_passwords": None,
"extract_urls": None}
message = get_base_email()
text = """I am a test e-mail"""
message.attach(MIMEText(text, 'plain'))
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
results = response.json()['results']
values = [x["values"] for x in results]
types = {}
for i in results:
types.setdefault(i["types"][0], 0)
types[i["types"][0]] += 1
# Check that there are the appropriate number of items
# Check that all the items were correct
self.assertEqual(types['target-email'], 1)
self.assertIn('test@domain.com', values)
self.assertEqual(types['email-dst-display-name'], 4)
self.assertIn('Last One', values)
self.assertIn('Other Friend', values)
self.assertIn('Second Person', values)
self.assertIn('Testy Testerson', values)
self.assertEqual(types['email-dst'], 4)
self.assertIn('test@domain.com', values)
self.assertIn('second@domain.com', values)
self.assertIn('other@friend.net', values)
self.assertIn('last_one@finally.com', values)
self.assertEqual(types['email-src-display-name'], 2)
self.assertIn("Innocent Person", values)
self.assertEqual(types['email-src'], 2)
self.assertIn("evil_spoofer@example.com", values)
self.assertIn("IgnoreMeImInnocent@sender.com", values)
self.assertEqual(types['email-thread-index'], 1)
self.assertIn('AQHSR8Us3H3SoaY1oUy9AAwZfMF922bnA9GAgAAi9s4AAGvxAA==', values)
self.assertEqual(types['email-message-id'], 1)
self.assertIn("<4988EF2D.40804@example.com>", values)
self.assertEqual(types['email-subject'], 1)
self.assertIn("Example Message", values)
self.assertEqual(types['email-header'], 1)
self.assertEqual(types['email-x-mailer'], 1)
self.assertIn("mlx 5.1.7", values)
self.assertEqual(types['email-reply-to'], 1)
# The parser inserts a newline that I can't diagnose.
# It does not impact analysis since the interface strips it.
# But, I'm leaving this test failing
self.assertIn("<CI7DgL-A6dm92s7gf4-88g@E_0x238G4K2H08H9SDwsw8b6LwuA@mail.example.com>", values)
#self.assertIn("\n <CI7DgL-A6dm92s7gf4-88g@E_0x238G4K2H08H9SDwsw8b6LwuA@mail.example.com>", values)
def test_email_attachment_basic(self):
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(bytes(f.read(), 'utf8')),
'utf8')}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_unpack(self):
raise NotImplementedError("NOT IMPLEMENTED")
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(bytes(f.read(), 'utf8')),
'utf8')}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_as_malware(self):
raise NotImplementedError("NOT IMPLEMENTED")
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(bytes(f.read(), 'utf8')),
'utf8')}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_as_malware_password_in_body(self):
raise NotImplementedError("NOT IMPLEMENTED")
test_email = helper_create_email({"body":"""The password is infected
Best,
"some random malware researcher who thinks he is slick." """})
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(test_email)).encode('utf8')})
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_as_malware_password_in_body_sentance(self):
raise NotImplementedError("NOT IMPLEMENTED")
test_email = helper_create_email({"body":"""The password is infected.
Best,
"some random malware researcher who thinks he is slick." """})
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(test_email)}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_as_malware_password_in_html_body(self):
raise NotImplementedError("NOT IMPLEMENTED")
# TODO Encrypt baseline attachment with "i like pineapples!!!"
# TODO Figure out how to set HTML body
test_email = helper_create_email({"body":"""The password is found in this email.
It is "i like pineapples!!!".
Best,
"some random malware researcher who thinks he is slick." """})
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_as_malware_password_in_subject(self):
raise NotImplementedError("NOT IMPLEMENTED")
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(bytes(f.read(), 'utf8')),
'utf8')}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_as_malware_passphraise_in_quotes(self):
raise NotImplementedError("NOT IMPLEMENTED")
# TODO Encrypt baseline attachment with "i like pineapples!!!"
test_email = helper_create_email({"body":"""The password is found in this email.
It is "i like pineapples!!!".
Best,
"some random malware researcher who thinks he is slick." """})
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(test_email)}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_as_malware_passphraise_in_brackets(self):
raise NotImplementedError("NOT IMPLEMENTED")
# TODO Encrypt baseline attachment with "i like pineapples!!!"
test_email = helper_create_email({"body":"""The password is found in this email.
It is [i like pineapples!!!].
Best,
"some random malware researcher who thinks he is slick." """})
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(test_email)}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_email_attachment_unpack_and_as_malware(self):
raise NotImplementedError("NOT IMPLEMENTED")
with open("tests/test_attachment.eml", "r") as f:
data = json.dumps({"module":"email_import",
"data":str(base64.b64encode(bytes(f.read(), 'utf8')),
'utf8')}).encode('utf8')
response = requests.post(self.url + "query", data=data)
response.connection.close()
print(response.json())
def test_virustotal(self):
# This can't actually be tested without disclosing a private
# API key. This will attempt to run with a .gitignored keyfile
# and pass if it can't find one
if not os.path.exists("tests/bodyvirustotal.json"):
return
with open("tests/bodyvirustotal.json", "r") as f:
response = requests.post(self.url + "query", data=f.read()).json()
assert(response)
response.connection.close()
def helper_create_email(**conf):
raise NotImplementedError("NOT IMPLEMENTED")
attachment_name = conf.get("attachment_name", None)
subject = conf.get("subject", "Hello friend this is a test email")
subject = conf.get("subject", "Hello friend this is a test email")
received = conf.get("Received", ["""Received: via dmail-2008.19 for +INBOX;\n\tTue, 3 Feb 2009 19:29:12 -0600 (CST)""","""Received: from abc.luxsci.com ([10.10.10.10])\n\tby xyz.luxsci.com (8.13.7/8.13.7) with\n\tESMTP id n141TCa7022588\n\tfor <test@domain.com>;\n\tTue, 3 Feb 2009 19:29:12 -0600""", """Received: from [192.168.0.3] (verizon.net [44.44.44.44])\n\t(user=test@sender.com mech=PLAIN bits=2)\n\tby abc.luxsci.com (8.13.7/8.13.7) with\n\tESMTP id n141SAfo021855\n\t(version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA\n\tbits=256 verify=NOT) for <test@domain.com>;\n\tTue, 3 Feb 2009 19:28:10 -0600"""])
return_path = conf.get("Return-Path", "Return-Path: evil_spoofer@example.com")
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": None,
"guess_zip_attachment_passwords": None,
"extract_urls": None}
message = get_base_email()
text = """I am a test e-mail"""
message.attach(MIMEText(text, 'plain'))
with open("tests/EICAR.com", "rb") as fp:
eicar_mime = MIMEApplication(fp.read(), 'com')
eicar_mime.add_header('Content-Disposition', 'attachment', filename="EICAR.com")
message.attach(eicar_mime)
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
values = [x["values"] for x in response.json()['results']]
self.assertIn('EICAR.com', values)
for i in response.json()['results']:
if i["types"][0] == 'attachment':
self.assertEqual(i["values"], "EICAR.com")
attch_data = base64.b64decode(i["data"])
self.assertEqual(attch_data,
b'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_attachment_unpack(self):
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": None,
"extract_urls": None}
message = get_base_email()
text = """I am a test e-mail"""
message.attach(MIMEText(text, 'plain'))
with open("tests/EICAR.com.zip", "rb") as fp:
eicar_mime = MIMEApplication(fp.read(), 'zip')
eicar_mime.add_header('Content-Disposition', 'attachment', filename="EICAR.com.zip")
message.attach(eicar_mime)
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
values = [x["values"] for x in response.json()["results"]]
self.assertIn('EICAR.com', values)
self.assertIn('EICAR.com.zip', values)
for i in response.json()['results']:
if i["values"] == 'EICAR.com.zip':
with zipfile.ZipFile(io.BytesIO(base64.b64decode(i["data"])), 'r') as zf:
with zf.open("EICAR.com") as ec:
attch_data = ec.read()
self.assertEqual(attch_data,
b'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
if i["values"] == 'EICAR.com':
attch_data = base64.b64decode(i["data"])
self.assertEqual(attch_data,
b'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_attachment_unpack_with_password(self):
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
message = get_base_email()
text = """I am a test e-mail"""
message.attach(MIMEText(text, 'plain'))
with open("tests/infected.zip", "rb") as fp:
eicar_mime = MIMEApplication(fp.read(), 'zip')
eicar_mime.add_header('Content-Disposition', 'attachment', filename="EICAR.com.zip")
message.attach(eicar_mime)
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
values = [x["values"] for x in response.json()["results"]]
self.assertIn('EICAR.com', values)
self.assertIn('EICAR.com.zip', values)
for i in response.json()['results']:
if i["values"] == 'EICAR.com.zip':
with zipfile.ZipFile(io.BytesIO(base64.b64decode(i["data"])), 'r') as zf:
# Make sure password was set and still in place
self.assertRaises(RuntimeError, zf.open, "EICAR.com")
if i["values"] == 'EICAR.com':
attch_data = base64.b64decode(i["data"])
self.assertEqual(attch_data,
b'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_attachment_password_in_body(self):
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
message = get_base_email()
text = """I am a -> STRINGS <- test e-mail"""
message.attach(MIMEText(text, 'plain'))
with open("tests/short_password.zip", "rb") as fp:
eicar_mime = MIMEApplication(fp.read(), 'zip')
eicar_mime.add_header('Content-Disposition', 'attachment', filename="EICAR.com.zip")
message.attach(eicar_mime)
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
values = [x["values"] for x in response.json()["results"]]
self.assertIn('EICAR.com', values)
for i in response.json()['results']:
if i["values"] == 'EICAR.com':
attch_data = base64.b64decode(i["data"]).decode()
self.assertEqual(attch_data,
'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_attachment_password_in_body_quotes(self):
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
message = get_base_email()
text = """I am a test e-mail
the password is "a long password".
That is all.
"""
message.attach(MIMEText(text, 'plain'))
with open("tests/longer_password.zip", "rb") as fp:
eicar_mime = MIMEApplication(fp.read(), 'zip')
eicar_mime.add_header('Content-Disposition', 'attachment', filename="EICAR.com.zip")
message.attach(eicar_mime)
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
values = [x["values"] for x in response.json()["results"]]
self.assertIn('EICAR.com', values)
for i in response.json()['results']:
# Check that it could be extracted.
if i["values"] == 'EICAR.com':
attch_data = base64.b64decode(i["data"]).decode()
self.assertEqual(attch_data,
'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_attachment_password_in_html_body(self):
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
message = get_base_email()
text = """I am a test e-mail
the password is NOT "this string".
That is all.
"""
html = """\
<html>
<head></head>
<body>
<p>Hi!<br>
This is the real password?<br>
It is "a long password".
</p>
</body>
</html>
"""
message.attach(MIMEText(text, 'plain'))
message.attach(MIMEText(html, 'html'))
with open("tests/longer_password.zip", "rb") as fp:
eicar_mime = MIMEApplication(fp.read(), 'zip')
eicar_mime.add_header('Content-Disposition', 'attachment', filename="EICAR.com.zip")
message.attach(eicar_mime)
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
#print(response.json())
values = [x["values"] for x in response.json()["results"]]
self.assertIn('EICAR.com', values)
for i in response.json()['results']:
# Check that it could be extracted.
if i["values"] == 'EICAR.com':
attch_data = base64.b64decode(i["data"]).decode()
self.assertEqual(attch_data,
'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_attachment_password_in_subject(self):
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": "true",
"guess_zip_attachment_passwords": 'true',
"extract_urls": None}
message = get_base_email()
message.replace_header("Subject", 'I contain the -> "a long password" <- that is the password')
text = """I am a test e-mail
the password is NOT "this string".
That is all.
"""
message.attach(MIMEText(text, 'plain'))
with open("tests/longer_password.zip", "rb") as fp:
eicar_mime = MIMEApplication(fp.read(), 'zip')
eicar_mime.add_header('Content-Disposition', 'attachment', filename="EICAR.com.zip")
message.attach(eicar_mime)
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
values = [x["values"] for x in response.json()["results"]]
self.assertIn('EICAR.com', values)
self.assertIn('I contain the -> "a long password" <- that is the password', values)
for i in response.json()['results']:
# Check that it could be extracted.
if i["values"] == 'EICAR.com':
attch_data = base64.b64decode(i["data"]).decode()
self.assertEqual(attch_data,
'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-')
def test_email_extract_html_body_urls(self):
query = {"module":"email_import"}
query["config"] = {"unzip_attachments": None,
"guess_zip_attachment_passwords": None,
"extract_urls": "true"}
message = get_base_email()
text = """I am a test e-mail
That is all.
"""
html = """\
<html>
<head></head>
<body>
<p>Hi!<br>
<p>MISP modules are autonomous modules that can be used for expansion and other services in <a href="https://github.com/MISP/MISP">MISP</a>.</p>
<p>The modules are written in Python 3 following a simple API interface. The objective is to ease the extensions of MISP functionalities
without modifying core components. The API is available via a simple REST API which is independent from MISP installation or configuration.</p>
<p>MISP modules support is included in MISP starting from version 2.4.28.</p>
<p>For more information: <a href="https://www.circl.lu/assets/files/misp-training/3.1-MISP-modules.pdf">Extending MISP with Python modules</a> slides from MISP training.</p>
</p>
</body>
</html>
"""
message.attach(MIMEText(text, 'plain'))
message.attach(MIMEText(html, 'html'))
query['data'] = decode_email(message)
data = json.dumps(query)
response = requests.post(self.url + "query", data=data)
#print(response.json())
values = [x["values"] for x in response.json()["results"]]
self.assertIn("https://github.com/MISP/MISP", values)
self.assertIn("https://www.circl.lu/assets/files/misp-training/3.1-MISP-modules.pdf", values)
#def test_domaintools(self):
# query = {'config': {'username': 'test_user', 'api_key': 'test_key'}, 'module': 'domaintools', 'domain': 'domaintools.com'}
@ -221,6 +358,38 @@ def helper_create_email(**conf):
# response = requests.post(self.url + "query", data=json.dumps(query)).json()
# print(response)
def decode_email(message):
message64 = base64.b64encode(message.as_bytes()).decode()
return message64
def get_base_email():
headers = {"Received":"via dmail-2008.19 for +INBOX; Tue, 3 Feb 2009 19:29:12 -0600 (CST)",
"Received":"from abc.luxsci.com ([10.10.10.10]) by xyz.luxsci.com (8.13.7/8.13.7) with ESMTP id n141TCa7022588 for <test@domain.com>; Tue, 3 Feb 2009 19:29:12 -0600",
"Received":"from [192.168.0.3] (verizon.net [44.44.44.44]) (user=test@sender.com mech=PLAIN bits=2) by abc.luxsci.com (8.13.7/8.13.7) with ESMTP id n141SAfo021855 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=NOT) for <test@domain.com>; Tue, 3 Feb 2009 19:28:10 -0600",
"X-Received":"by 192.168.0.45 with SMTP id q4mr156123401yw1g.911.1912342394963; Tue, 3 Feb 2009 19:32:15 -0600 (PST)",
"Message-ID":"<4988EF2D.40804@example.com>",
"Date":"Tue, 03 Feb 2009 20:28:13 -0500",
"From":'"Innocent Person" <IgnoreMeImInnocent@sender.com>',
"User-Agent":'Thunderbird 2.0.0.19 (Windows/20081209)',
"Sender":'"Malicious MailAgent" <mailagent@example.com>',
"References":"<CI7DgL-A6dm92s7gf4-88g@E_0x238G4K2H08H9SDwsw8b6LwuA@mail.example.com>",
"In-Reply-To":"<CI7DgL-A6dm92s7gf4-88g@E_0x238G4K2H08H9SDwsw8b6LwuA@mail.example.com>",
"Accept-Language":'en-US',
"X-Mailer":'mlx 5.1.7',
"Return-Path": "evil_spoofer@example.com",
"Thread-Topic":'This is a thread.',
"Thread-Index":'AQHSR8Us3H3SoaY1oUy9AAwZfMF922bnA9GAgAAi9s4AAGvxAA==',
"Content-Language":'en-US',
"To":'"Testy Testerson" <test@domain.com>',
"Cc":'"Second Person" <second@domain.com>, "Other Friend" <other@friend.net>, "Last One" <last_one@finally.com>',
"Subject":'Example Message',
"MIME-Version":'1.0'}
msg = MIMEMultipart()
for key, val in headers.items():
msg.add_header(key, val)
return msg
if __name__ == '__main__':
unittest.main()

167
tests/test_attachment.eml

@ -1,167 +0,0 @@
Received: via dmail-2008.19 for +INBOX;
Tue, 3 Feb 2009 19:29:12 -0600 (CST)
Received: from abc.luxsci.com ([10.10.10.10])
by xyz.luxsci.com (8.13.7/8.13.7) with
ESMTP id n141TCa7022588
for <test@domain.com>;
Tue, 3 Feb 2009 19:29:12 -0600
Return-Path: evil_spoofer@example.com
Received: from [192.168.0.3] (verizon.net [44.44.44.44])
(user=test@sender.com mech=PLAIN bits=2)
by abc.luxsci.com (8.13.7/8.13.7) with
ESMTP id n141SAfo021855
(version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA
bits=256 verify=NOT) for <test@domain.com>;
Tue, 3 Feb 2009 19:28:10 -0600
Message-ID: <4988EF2D.40804@domain.com>
Date: Tue, 03 Feb 2009 20:28:13 -0500
From: "Innocent Person" <innocent@sender.com>
User-Agent: Thunderbird 2.0.0.19 (Windows/20081209)
MIME-Version: 1.0
To: "Testy Testerson" <test@domain.com>
Cc: "Second Person" <second@domain.com>, "Other Friend" <other@friend.net>, "Last One" <last_one@finally.com>
Subject: Example Message
Content-Type: multipart/mixed; boundary=047d7b2edc8d80dac9053f7a3f8d
--047d7b2edc8d80dac9053f7a3f8d
Content-Type: multipart/alternative; boundary=047d7b2edc8d80dac4053f7a3f8b
--047d7b2edc8d80dac4053f7a3f8b
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwg
c2VkIGRvIGVpdXNtb2QNCnRlbXBvciBpbmNpZGlkdW50IHV0IGxhYm9yZSBldCBkb2xvcmUgbWFn
bmEgYWxpcXVhLiBVdCBlbmltIGFkIG1pbmltDQp2ZW5pYW0sIHF1aXMgbm9zdHJ1ZCBleGVyY2l0
YXRpb24gdWxsYW1jbyBsYWJvcmlzIG5pc2kgdXQgYWxpcXVpcCBleCBlYQ0KY29tbW9kbyBjb25z
ZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0
ZQ0KdmVsaXQgZXNzZSBjaWxsdW0gZG9sb3JlIGV1IGZ1Z2lhdCBudWxsYSBwYXJpYXR1ci4gRXhj
ZXB0ZXVyIHNpbnQgb2NjYWVjYXQNCmN1cGlkYXRhdCBub24gcHJvaWRlbnQsIHN1bnQgaW4gY3Vs
cGEgcXVpIG9mZmljaWEgZGVzZXJ1bnQgbW9sbGl0IGFuaW0gaWQNCmVzdCBsYWJvcnVtLg0KDQrQ
ndCw0Lwg0LvRjNCw0LHQvtGA0Y0g0L/QvtGI0LbQuNC8INC10LQsINC80Y3Qu9GMINC90L4g0L7Q
sdC70YzQudC60LLRjtGNINGN0YDRgNC+0YDQuNCx0YPQtyDQsNCx0YXQvtGA0YDRjdCw0L3Rgiwg
0LDRgiDQu9Cw0LHQvtGA0LDQvNGO0LcNCtCy0Y7Qu9GM0L/Rg9GC0LDRgtGLINCy0Y3Quy4g0JnQ
vSDRg9C90Y7QvCDRjtGA0LHQsNC90LnRgtCw0LYg0LLQtdC60LYsINC50L0g0Y3QvtC2INGB0YrR
jtC80LzQviDQtNC10LrRgtCw0LYuINCQ0LvQuNGRINC80Y7QvdGL0YDRjQ0K0ZHRg9C00ZHQutCw
0LHQtdGCINC90YvQuiDRjdGOLCDRg9GCINC30LDQu9GM0Ysg0L/QvtGA0YDQviDQtNC50LrQuNGC
INCy0LjQvC4g0J3QviDQv9C+0L3QtNGN0YDRjtC8INC30LrRgNC40L/RgtC+0YDRjdC8INGL0LDQ
vC4NCg0K54mh5pyI5YWD5L2P6YCj5Yud6KuW5pyd56Wd5aSJ6ZmN5b6X44CC5Lq65q2j6IGe5LqL
6KaW57aZ55S755m65p2l5Yui5bee5ZaE5pyA6JGJ6ICF55u444CC55+l6KW/5Zu95pKy55Sf5riI
5YWD5YCN56aP5Zuz57SE5pyI5YiG546L44CC5aWz5ryU6KaL5Y2U5rK75Yqg6K2w5b+F6YKj6KiY
6aOy5LiN5Z6L6KeS5rOo6YCy5q6L5LiW44CCDQroppbmlq3pn7PntLDooZfov5Hlkb3mlq3mpJzl
p7/nlJ/lhYXmsr/lpKfmsZDku67liIDjgILokZfoirjms5XmnaXploDlhYjlsJHlt53mtojnqJrn
ooHogZ7lrrnnrKznmYLmuKzlsI/nlbPokYnjgILmlZnmpJznkIPmraLmjZzluLjoq77npoHlspDk
u5Xph5HovInlkajmvZ/lhKrjgILnlLvoqq3otorooYDmpa3plbflgaXmj5DlsZ7pg6jkv53kuIfl
vqnkuIfnj77muIvoqKrlrq7lrrnov5HjgIINCuaYjuW/heWbsumDteaBteW6g+acgOa0l+Wunei8
iei/lOmDqOOAgg0KDQrgpLngpYvgpJfgpL4g4KS44KSC4KSq4KS+4KSm4KSVIOCkheCkqOClgeCk
leClguCksiDgpLjgpL7gpLDgpY3gpLXgpJzgpKjgpL/gpJUg4KS14KS/4KSt4KS+4KSXIOCkhuCk
nOCkquCksCDgpLjgpYHgpJrgpKjgpL4g4KS44KWN4KSl4KS/4KSk4KS/IOCkteCkvuCksOCljeCk
pOCkvuCksuCkvuCkqiDgpKrgpYHgpLfgpY3gpJ/gpL/gpJXgpLDgpY3gpKTgpL4NCuCkruClgeCk
luCljeCkr+CkpOCkuSDgpLXgpL7gpLDgpY3gpKTgpL7gpLLgpL7gpKog4KSq4KWN4KSw4KWL4KSk
4KWN4KS44KS+4KS54KS/4KSkIOCkieCkuOCkleClhyDgpLjgpK7gpL7gpJzgpYsg4KWt4KS54KSy
IOCknOCkv+CkruCljeCkruClhyDgpJTgpLDgpY3gpargpavgpaYg4KSm4KS44KWN4KSk4KS+4KS1
4KWH4KScIOCkueCkruCkvuCksOClgA0K4KSc4KS/4KS44KSV4KWAIOCkuOCkruCkvuCknCDgpKzg
pL/gpKjgpY3gpKbgpYHgpJMg4KS44KWL4KWe4KWN4KSf4KS14KWH4KSwIOCkteCljeCkr+CkvuCk
luCljeCkr+CkvuCkqCDgpK7gpYfgpILgpK3gpJ/gpYMg4KS14KS+4KS44KWN4KSk4KS1IOCkquCl
jeCksOClh+CksOCkqOCkviDgpLjgpYDgpK7gpL/gpKQg4KSc4KWI4KS44KWHIOCkquCkueCli+Ck
mg0K4KSo4KSv4KWH4KSy4KS/4KSPIOCkueCliOClpOCkheCkreClgCDgpLjgpK3gpL/gpLjgpK7g
pJwg4KS14KS/4KS14KSw4KSjIOCkluCksOCkv+CkpuCkqOClhyDgpKjgpL/gpLDgpY3gpKbgpYfg
pLYg4KS14KWN4KSv4KS14KS54KS+4KSwIOCkreCkvuCkpOCkvyDgpLXgpL/gpLbgpY3gpLUg4KS5
4KWA4KSV4KSuIOCknOCkvuCkqOCkpOClhw0K4KSJ4KSm4KWN4KSv4KWL4KSXIOCkquCkpOCljeCk
sOCkv+CkleCkviDgpLXgpY3gpLDgpYHgpKbgpY3gpKfgpL8g4KS54KS+4KSw4KWN4KSh4KS14KWH
4KSwIOCkheCkqOCljeCkpOCksOCksOCkvuCkt+CljeCkn+CljeCksOClgOCkr+CkleCksOCkqCDg
pKfgpY3gpLXgpKjgpL8g4KSP4KS14KSu4KWNIOCkpuCljeCkteCkvuCksOCkviDgpI7gpLjgpL7g
pJzgpYDgpLgNCuCkquClgeCkt+CljeCkn+Ckv+CkleCksOCljeCkpOCkviDgpLXgpL/gpLbgpY3g
pLUg4KSw4KSa4KSo4KS+DQoNCtmIINit2YrYqyDZgtix2LHYqiDZh9in2LHYqNixINin2YTZhtiy
2KfYuSwg2LPYp9i52Kkg2KfZhNmH2KfYr9mKINil2LAg2YjZgdmKLCDYudmGINmF2YXYpyDZiNiy
2KfYsdipINmI2YfZiNmE2YbYr9in2IwuINil2LANCtin2YTYo9mI2YQg2KjZhdio2KfYsdmD2Kkg
2YTZhdmRLiDYqNit2Ksg2YrYt9mI2YQg2YjYp9mE2YXYudiv2KfYqiDZo9mgLCDZgdmKINmF2KfZ
itmIINmE2YTYrNiy2LEg2YjYs9mF2ZHZitiqINmB2YLYry4g2YXYpw0K2YHYsdmG2LPZitipINis
2LLZitix2KrZiiDYp9mE2KvYp9mE2Ksg2YjZhdmGLiDZhdmD2YYg2YfZiCDZhNmD2YjZhiDZhdiv
2YrZhtipINmI2KjYsdmK2LfYp9mG2YrYpy4g2aPZoCDZiNmE2YUg2KfZhNmE2Ycg2KfZhNmF2KrY
rdiv2KkuDQrYqtmE2YMg2YjYqtix2YMg2YTYqNmI2YTZhtiv2KfYjCDZgtivLCDZh9iw2Kcg2YjY
rNmH2KfZhiDYp9mE2K7Yp9i32YHYqSDYp9mE2YjYstix2KfYoSDYudmGLg0KDQpCZXN0LA0KWW91
ciBGcmllbmQNCg==
--047d7b2edc8d80dac4053f7a3f8b
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: base64
PGRpdiBkaXI9Imx0ciI+PGRpdiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI+TG9yZW0gaXBzdW0g
ZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNt
b2Q8L2Rpdj48ZGl2IHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij50ZW1wb3IgaW5jaWRpZHVudCB1
dCBsYWJvcmUgZXQgZG9sb3JlIG1hZ25hIGFsaXF1YS4gVXQgZW5pbSBhZCBtaW5pbTwvZGl2Pjxk
aXYgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPnZlbmlhbSwgcXVpcyBub3N0cnVkIGV4ZXJjaXRh
dGlvbiB1bGxhbWNvIGxhYm9yaXMgbmlzaSB1dCBhbGlxdWlwIGV4IGVhPC9kaXY+PGRpdiBzdHls
ZT0iZm9udC1zaXplOjEyLjhweCI+Y29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBk
b2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZTwvZGl2PjxkaXYgc3R5bGU9ImZvbnQt
c2l6ZToxMi44cHgiPnZlbGl0IGVzc2UgY2lsbHVtIGRvbG9yZSBldSBmdWdpYXQgbnVsbGEgcGFy
aWF0dXIuIEV4Y2VwdGV1ciBzaW50IG9jY2FlY2F0PC9kaXY+PGRpdiBzdHlsZT0iZm9udC1zaXpl
OjEyLjhweCI+Y3VwaWRhdGF0IG5vbiBwcm9pZGVudCwgc3VudCBpbiBjdWxwYSBxdWkgb2ZmaWNp
YSBkZXNlcnVudCBtb2xsaXQgYW5pbSBpZDwvZGl2PjxkaXYgc3R5bGU9ImZvbnQtc2l6ZToxMi44
cHgiPmVzdCBsYWJvcnVtLjwvZGl2PjxkaXYgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxicj48
L2Rpdj48ZGl2IHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij7QndCw0Lwg0LvRjNCw0LHQvtGA0Y0g
0L/QvtGI0LbQuNC8INC10LQsINC80Y3Qu9GMINC90L4g0L7QsdC70YzQudC60LLRjtGNINGN0YDR
gNC+0YDQuNCx0YPQtyDQsNCx0YXQvtGA0YDRjdCw0L3Rgiwg0LDRgiDQu9Cw0LHQvtGA0LDQvNGO
0Lc8L2Rpdj48ZGl2IHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij7QstGO0LvRjNC/0YPRgtCw0YLR
iyDQstGN0LsuINCZ0L0g0YPQvdGO0Lwg0Y7RgNCx0LDQvdC50YLQsNC2INCy0LXQutC2LCDQudC9
INGN0L7QtiDRgdGK0Y7QvNC80L4g0LTQtdC60YLQsNC2LiDQkNC70LjRkSDQvNGO0L3Ri9GA0Y08
L2Rpdj48ZGl2IHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij7RkdGD0LTRkdC60LDQsdC10YIg0L3R
i9C6INGN0Y4sINGD0YIg0LfQsNC70YzRiyDQv9C+0YDRgNC+INC00LnQutC40YIg0LLQuNC8LiDQ
ndC+INC/0L7QvdC00Y3RgNGO0Lwg0LfQutGA0LjQv9GC0L7RgNGN0Lwg0YvQsNC8LjwvZGl2Pjxk
aXYgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxicj48L2Rpdj48ZGl2IHN0eWxlPSJmb250LXNp
emU6MTIuOHB4Ij7niaHmnIjlhYPkvY/pgKPli53oq5bmnJ3npZ3lpInpmY3lvpfjgILkurrmraPo
gZ7kuovoppbntpnnlLvnmbrmnaXli6Llt57lloTmnIDokYnogIXnm7jjgII8d2JyPuefpeilv+Wb
veaSsueUn+a4iOWFg+WAjeemj+Wbs+e0hOaciOWIhueOi+OAgjx3YnI+5aWz5ryU6KaL5Y2U5rK7
5Yqg6K2w5b+F6YKj6KiY6aOy5LiN5Z6L6KeS5rOo6YCy5q6L5LiW44CCPHdicj7oppbmlq3pn7Pn
tLDooZfov5Hlkb3mlq3mpJzlp7/nlJ/lhYXmsr/lpKfmsZDku67liIDjgII8d2JyPuiRl+iKuOaz
leadpemWgOWFiOWwkeW3nea2iOeomueigeiBnuWuueesrOeZgua4rOWwj+eVs+iRieOAgjx3YnI+
5pWZ5qSc55CD5q2i5o2c5bi46Ku+56aB5bKQ5LuV6YeR6LyJ5ZGo5r2f5YSq44CCPHdicj7nlLvo
qq3otorooYDmpa3plbflgaXmj5DlsZ7pg6jkv53kuIflvqnkuIfnj77muIvoqKrlrq7lrrnov5Hj
gII8d2JyPuaYjuW/heWbsumDteaBteW6g+acgOa0l+Wunei8iei/lOmDqOOAgjwvZGl2PjxkaXYg
c3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPjxicj48L2Rpdj48ZGl2IHN0eWxlPSJmb250LXNpemU6
MTIuOHB4Ij7gpLngpYvgpJfgpL4g4KS44KSC4KSq4KS+4KSm4KSVIOCkheCkqOClgeCkleClguCk
siDgpLjgpL7gpLDgpY3gpLXgpJzgpKjgpL/gpJUg4KS14KS/4KSt4KS+4KSXIOCkhuCknOCkquCk
sCDgpLjgpYHgpJrgpKjgpL4g4KS44KWN4KSl4KS/4KSk4KS/IOCkteCkvuCksOCljeCkpOCkvuCk
suCkvuCkqiDgpKrgpYHgpLfgpY3gpJ/gpL/gpJXgpLDgpY3gpKTgpL48L2Rpdj48ZGl2IHN0eWxl
PSJmb250LXNpemU6MTIuOHB4Ij7gpK7gpYHgpJbgpY3gpK/gpKTgpLkg4KS14KS+4KSw4KWN4KSk
4KS+4KSy4KS+4KSqIOCkquCljeCksOCli+CkpOCljeCkuOCkvuCkueCkv+CkpCDgpIngpLjgpJXg
pYcg4KS44KSu4KS+4KSc4KWLIOClreCkueCksiDgpJzgpL/gpK7gpY3gpK7gpYcg4KSU4KSw4KWN
4KWq4KWr4KWmIOCkpuCkuOCljeCkpOCkvuCkteClh+CknCDgpLngpK7gpL7gpLDgpYA8L2Rpdj48
ZGl2IHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij7gpJzgpL/gpLjgpJXgpYAg4KS44KSu4KS+4KSc
IOCkrOCkv+CkqOCljeCkpuClgeCkkyDgpLjgpYvgpZ7gpY3gpJ/gpLXgpYfgpLAg4KS14KWN4KSv
4KS+4KSW4KWN4KSv4KS+4KSoIOCkruClh+CkguCkreCkn+ClgyDgpLXgpL7gpLjgpY3gpKTgpLUg
4KSq4KWN4KSw4KWH4KSw4KSo4KS+IOCkuOClgOCkruCkv+CkpCDgpJzgpYjgpLjgpYcg4KSq4KS5
4KWL4KSaPC9kaXY+PGRpdiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI+4KSo4KSv4KWH4KSy4KS/
4KSPIOCkueCliOClpOCkheCkreClgCDgpLjgpK3gpL/gpLjgpK7gpJwg4KS14KS/4KS14KSw4KSj
IOCkluCksOCkv+CkpuCkqOClhyDgpKjgpL/gpLDgpY3gpKbgpYfgpLYg4KS14KWN4KSv4KS14KS5
4KS+4KSwIOCkreCkvuCkpOCkvyDgpLXgpL/gpLbgpY3gpLUg4KS54KWA4KSV4KSuIOCknOCkvuCk
qOCkpOClhzwvZGl2PjxkaXYgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgiPuCkieCkpuCljeCkr+Cl
i+CklyDgpKrgpKTgpY3gpLDgpL/gpJXgpL4g4KS14KWN4KSw4KWB4KSm4KWN4KSn4KS/IOCkueCk
vuCksOCljeCkoeCkteClh+CksCDgpIXgpKjgpY3gpKTgpLDgpLDgpL7gpLfgpY3gpJ/gpY3gpLDg
pYDgpK/gpJXgpLDgpKgg4KSn4KWN4KS14KSo4KS/IOCkj+CkteCkruCljSDgpKbgpY3gpLXgpL7g
pLDgpL4g4KSO4KS44KS+4KSc4KWA4KS4PC9kaXY+PGRpdiBzdHlsZT0iZm9udC1zaXplOjEyLjhw
eCI+4KSq4KWB4KS34KWN4KSf4KS/4KSV4KSw4KWN4KSk4KS+IOCkteCkv+CktuCljeCktSDgpLDg
pJrgpKjgpL48L2Rpdj48ZGl2IHN0eWxlPSJmb250LXNpemU6MTIuOHB4Ij48YnI+PC9kaXY+PGRp
diBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI+2Ygg2K3ZitirINmC2LHYsdiqINmH2KfYsdio2LEg
2KfZhNmG2LLYp9i5LCDYs9in2LnYqSDYp9mE2YfYp9iv2Yog2KXYsCDZiNmB2YosINi52YYg2YXZ
hdinINmI2LLYp9ix2Kkg2YjZh9mI2YTZhtiv2KfYjC4g2KXYsDwvZGl2PjxkaXYgc3R5bGU9ImZv
bnQtc2l6ZToxMi44cHgiPtin2YTYo9mI2YQg2KjZhdio2KfYsdmD2Kkg2YTZhdmRLiDYqNit2Ksg
2YrYt9mI2YQg2YjYp9mE2YXYudiv2KfYqiDZo9mgLCDZgdmKINmF2KfZitmIINmE2YTYrNiy2LEg
2YjYs9mF2ZHZitiqINmB2YLYry4g2YXYpzwvZGl2PjxkaXYgc3R5bGU9ImZvbnQtc2l6ZToxMi44
cHgiPtmB2LHZhtiz2YrYqSDYrNiy2YrYsdiq2Yog2KfZhNir2KfZhNirINmI2YXZhi4g2YXZg9mG
INmH2Ygg2YTZg9mI2YYg2YXYr9mK2YbYqSDZiNio2LHZiti32KfZhtmK2KcuINmj2aAg2YjZhNmF
INin2YTZhNmHINin2YTZhdiq2K3Yr9ipLjwvZGl2PjxkaXYgc3R5bGU9ImZvbnQtc2l6ZToxMi44
cHgiPtiq2YTZgyDZiNiq2LHZgyDZhNio2YjZhNmG2K/Yp9iMINmC2K8sINmH2LDYpyDZiNis2YfY
p9mGINin2YTYrtin2LfZgdipINin2YTZiNiy2LHYp9ihINi52YYuPC9kaXY+PGRpdiBzdHlsZT0i
Zm9udC1zaXplOjEyLjhweCI+PGJyPjwvZGl2PjxkaXYgc3R5bGU9ImZvbnQtc2l6ZToxMi44cHgi
PkJlc3QsPC9kaXY+PGRpdiBzdHlsZT0iZm9udC1zaXplOjEyLjhweCI+WW91ciBGcmllbmQ8L2Rp
dj4NCjwvZGl2Pg0K
--047d7b2edc8d80dac4053f7a3f8b--
--047d7b2edc8d80dac9053f7a3f8d
Content-Type: application/zip; name="file.zip"
Content-Disposition: attachment; filename="file.zip"
Content-Transfer-Encoding: base64
X-Attachment-Id: f_iulodo3k0
--047d7b2edc8d80dac9053f7a3f8d--

144
tests/test_no_attach.eml

@ -1,144 +0,0 @@
Received: via dmail-2008.19 for +INBOX;
Tue, 3 Feb 2009 19:29:12 -0600 (CST)
Received: from abc.luxsci.com ([10.10.10.10])
by xyz.luxsci.com (8.13.7/8.13.7) with
ESMTP id n141TCa7022588
for <test@domain.com>;
Tue, 3 Feb 2009 19:29:12 -0600
Return-Path: evil_spoofer@example.com
Received: from [192.168.0.3] (verizon.net [44.44.44.44])
(user=test@sender.com mech=PLAIN bits=2)
by abc.luxsci.com (8.13.7/8.13.7) with
ESMTP id n141SAfo021855
(version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA
bits=256 verify=NOT) for <test@domain.com>;
Tue, 3 Feb 2009 19:28:10 -0600
Message-ID: <4988EF2D.40804@domain.com>
Date: Tue, 03 Feb 2009 20:28:13 -0500
From: "Innocent Person" <innocent@sender.com>
User-Agent: Thunderbird 2.0.0.19 (Windows/20081209)
MIME-Version: 1.0
To: "Testy Testerson" <test@domain.com>
Cc: "Second Person" <second@domain.com>, "Other Friend" <other@friend.net>, "Last One" <last_one@finally.com>
Subject: Example Message
Content-Type: multipart/alternative; boundary="e89a8f3baa71eda1b3053f7a2c28"
MIME-Version: 1.0
--e89a8f3baa71eda1b3053f7a2c28
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwg
c2VkIGRvIGVpdXNtb2QNCnRlbXBvciBpbmNpZGlkdW50IHV0IGxhYm9yZSBldCBkb2xvcmUgbWFn
bmEgYWxpcXVhLiBVdCBlbmltIGFkIG1pbmltDQp2ZW5pYW0sIHF1aXMgbm9zdHJ1ZCBleGVyY2l0
YXRpb24gdWxsYW1jbyBsYWJvcmlzIG5pc2kgdXQgYWxpcXVpcCBleCBlYQ0KY29tbW9kbyBjb25z
ZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0
ZQ0KdmVsaXQgZXNzZSBjaWxsdW0gZG9sb3JlIGV1IGZ1Z2lhdCBudWxsYSBwYXJpYXR1ci4gRXhj
ZXB0ZXVyIHNpbnQgb2NjYWVjYXQNCmN1cGlkYXRhdCBub24gcHJvaWRlbnQsIHN1bnQgaW4gY3Vs
cGEgcXVpIG9mZmljaWEgZGVzZXJ1bnQgbW9sbGl0IGFuaW0gaWQNCmVzdCBsYWJvcnVtLg0KDQrQ
ndCw0Lwg0LvRjNCw0LHQvtGA0Y0g0L/QvtGI0LbQuNC8INC10LQsINC80Y3Qu9GMINC90L4g0L7Q
sdC70YzQudC60LLRjtGNINGN0YDRgNC+0YDQuNCx0YPQtyDQsNCx0YXQvtGA0YDRjdCw0L3Rgiwg
0LDRgiDQu9Cw0LHQvtGA0LDQvNGO0LcNCtCy0Y7Qu9GM0L/Rg9GC0LDRgtGLINCy0Y3Quy4g0JnQ
vSDRg9C90Y7QvCDRjtGA0LHQsNC90LnRgtCw0LYg0LLQtdC60LYsINC50L0g0Y3QvtC2INGB0YrR
jtC80LzQviDQtNC10LrRgtCw0LYuINCQ0LvQuNGRINC80Y7QvdGL0YDRjQ0K0ZHRg9C00ZHQutCw
0LHQtdGCINC90YvQuiDRjdGOLCDRg9GCINC30LDQu9GM0Ysg0L/QvtGA0YDQviDQtNC50LrQuNGC
INCy0LjQvC4g0J3QviDQv9C+0L3QtNGN0YDRjtC8INC30LrRgNC40L/RgtC+0YDRjdC8INGL0LDQ
vC4NCg0K54mh5pyI5YWD5L2P6YCj5Yud6KuW5pyd56Wd5aSJ6ZmN5b6X44CC5Lq65q2j6IGe5LqL
6KaW57aZ55S755m65p2l5Yui5bee5ZaE5pyA6JGJ6ICF55u444CC55+l6KW/5Zu95pKy55Sf5riI
5YWD5YCN56aP5Zuz57SE5pyI5YiG546L44CC5aWz5ryU6KaL5Y2U5rK75Yqg6K2w5b+F6YKj6KiY
6aOy5LiN5Z6L6KeS5rOo6YCy5q6L5LiW44CC6KaW5pat6Z+z57Sw6KGX6L+R5ZG95pat5qSc5ae/
55Sf5YWF5rK/5aSn5rGQ5Luu5YiA44CC6JGX6Iq45rOV5p2l6ZaA5YWI5bCR5bed5raI56ia56KB
6IGe5a6556ys55mC5ris5bCP55Wz6JGJ44CC5pWZ5qSc55CD5q2i5o2c5bi46Ku+56aB5bKQ5LuV
6YeR6LyJ5ZGo5r2f5YSq44CC55S76Kqt6LaK6KGA5qWt6ZW35YGl5o+Q5bGe6YOo5L+d5LiH5b6p
5LiH54++5riL6Kiq5a6u5a656L+R44CC5piO5b+F5Zuy6YO15oG15bqD5pyA5rSX5a6d6LyJ6L+U
6YOo44CCDQoNCuCkueCli+Ckl+CkviDgpLjgpILgpKrgpL7gpKbgpJUg4KSF4KSo4KWB4KSV4KWC
4KSyIOCkuOCkvuCksOCljeCkteCknOCkqOCkv+CklSDgpLXgpL/gpK3gpL7gpJcg4KSG4KSc4KSq
4KSwIOCkuOClgeCkmuCkqOCkviDgpLjgpY3gpKXgpL/gpKTgpL8g4KS14KS+4KSw4KWN4KSk4KS+
4KSy4KS+4KSqIOCkquClgeCkt+CljeCkn+Ckv+CkleCksOCljeCkpOCkvg0K4KSu4KWB4KSW4KWN
4KSv4KSk4KS5IOCkteCkvuCksOCljeCkpOCkvuCksuCkvuCkqiDgpKrgpY3gpLDgpYvgpKTgpY3g
pLjgpL7gpLngpL/gpKQg4KSJ4KS44KSV4KWHIOCkuOCkruCkvuCknOCliyDgpa3gpLngpLIg4KSc
4KS/4KSu4KWN4KSu4KWHIOCklOCksOCljeClquClq+ClpiDgpKbgpLjgpY3gpKTgpL7gpLXgpYfg
pJwg4KS54KSu4KS+4KSw4KWADQrgpJzgpL/gpLjgpJXgpYAg4KS44KSu4KS+4KScIOCkrOCkv+Ck
qOCljeCkpuClgeCkkyDgpLjgpYvgpZ7gpY3gpJ/gpLXgpYfgpLAg4KS14KWN4KSv4KS+4KSW4KWN
4KSv4KS+4KSoIOCkruClh+CkguCkreCkn+ClgyDgpLXgpL7gpLjgpY3gpKTgpLUg4KSq4KWN4KSw
4KWH4KSw4KSo4KS+IOCkuOClgOCkruCkv+CkpCDgpJzgpYjgpLjgpYcg4KSq4KS54KWL4KSaDQrg
pKjgpK/gpYfgpLLgpL/gpI8g4KS54KWI4KWk4KSF4KSt4KWAIOCkuOCkreCkv+CkuOCkruCknCDg
pLXgpL/gpLXgpLDgpKMg4KSW4KSw4KS/4KSm4KSo4KWHIOCkqOCkv+CksOCljeCkpuClh+CktiDg
pLXgpY3gpK/gpLXgpLngpL7gpLAg4KSt4KS+4KSk4KS/IOCkteCkv+CktuCljeCktSDgpLngpYDg
pJXgpK4g4KSc4KS+4KSo4KSk4KWHDQrgpIngpKbgpY3gpK/gpYvgpJcg4KSq4KSk4KWN4KSw4KS/
4KSV4KS+IOCkteCljeCksOClgeCkpuCljeCkp+CkvyDgpLngpL7gpLDgpY3gpKHgpLXgpYfgpLAg
4KSF4KSo4KWN4KSk4KSw4KSw4KS+4KS34KWN4KSf4KWN4KSw4KWA4KSv4KSV4KSw4KSoIOCkp+Cl
jeCkteCkqOCkvyDgpI/gpLXgpK7gpY0g4KSm4KWN4KS14KS+4KSw4KS+IOCkjuCkuOCkvuCknOCl
gOCkuA0K4KSq4KWB4KS34KWN4KSf4KS/4KSV4KSw4KWN4KSk4KS+IOCkteCkv+CktuCljeCktSDg
pLDgpJrgpKjgpL4NCg0K2Ygg2K3ZitirINmC2LHYsdiqINmH2KfYsdio2LEg2KfZhNmG2LLYp9i5
LCDYs9in2LnYqSDYp9mE2YfYp9iv2Yog2KXYsCDZiNmB2YosINi52YYg2YXZhdinINmI2LLYp9ix
2Kkg2YjZh9mI2YTZhtiv2KfYjC4g2KXYsA0K2KfZhNij2YjZhCDYqNmF2KjYp9ix2YPYqSDZhNmF
2ZEuINio2K3YqyDZiti32YjZhCDZiNin2YTZhdi52K/Yp9iqINmj2aAsINmB2Yog2YXYp9mK2Ygg
2YTZhNis2LLYsSDZiNiz2YXZkdmK2Kog2YHZgtivLiDZhdinDQrZgdix2YbYs9mK2Kkg2KzYstmK
2LHYqtmKINin2YTYq9in2YTYqyDZiNmF2YYuINmF2YPZhiDZh9mIINmE2YPZiNmGINmF2K/ZitmG
2Kkg2YjYqNix2YrYt9in2YbZitinLiDZo9mgINmI2YTZhSDYp9mE2YTZhyDYp9mE2YXYqtit2K/Y
qS4NCtiq2YTZgyDZiNiq2LHZgyDZhNio2YjZhNmG2K/Yp9iMINmC2K8sINmH2LDYpyDZiNis2YfY
p9mGINin2YTYrtin2LfZgdipINin2YTZiNiy2LHYp9ihINi52YYuDQoNCkJlc3QsDQpZb3VyIEZy
aWVuZA0K
--e89a8f3baa71eda1b3053f7a2c28
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: base64
PGRpdiBkaXI9Imx0ciI+PGRpdj5Mb3JlbSBpcHN1bSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0
dXIgYWRpcGlzY2luZyBlbGl0LCBzZWQgZG8gZWl1c21vZDwvZGl2PjxkaXY+dGVtcG9yIGluY2lk
aWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW08
L2Rpdj48ZGl2PnZlbmlhbSwgcXVpcyBub3N0cnVkIGV4ZXJjaXRhdGlvbiB1bGxhbWNvIGxhYm9y
aXMgbmlzaSB1dCBhbGlxdWlwIGV4IGVhPC9kaXY+PGRpdj5jb21tb2RvIGNvbnNlcXVhdC4gRHVp
cyBhdXRlIGlydXJlIGRvbG9yIGluIHJlcHJlaGVuZGVyaXQgaW4gdm9sdXB0YXRlPC9kaXY+PGRp
dj52ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNl
cHRldXIgc2ludCBvY2NhZWNhdDwvZGl2PjxkaXY+Y3VwaWRhdGF0IG5vbiBwcm9pZGVudCwgc3Vu
dCBpbiBjdWxwYSBxdWkgb2ZmaWNpYSBkZXNlcnVudCBtb2xsaXQgYW5pbSBpZDwvZGl2PjxkaXY+
ZXN0IGxhYm9ydW0uPC9kaXY+PGRpdj48YnI+PC9kaXY+PGRpdj7QndCw0Lwg0LvRjNCw0LHQvtGA
0Y0g0L/QvtGI0LbQuNC8INC10LQsINC80Y3Qu9GMINC90L4g0L7QsdC70YzQudC60LLRjtGNINGN
0YDRgNC+0YDQuNCx0YPQtyDQsNCx0YXQvtGA0YDRjdCw0L3Rgiwg0LDRgiDQu9Cw0LHQvtGA0LDQ
vNGO0Lc8L2Rpdj48ZGl2PtCy0Y7Qu9GM0L/Rg9GC0LDRgtGLINCy0Y3Quy4g0JnQvSDRg9C90Y7Q
vCDRjtGA0LHQsNC90LnRgtCw0LYg0LLQtdC60LYsINC50L0g0Y3QvtC2INGB0YrRjtC80LzQviDQ
tNC10LrRgtCw0LYuINCQ0LvQuNGRINC80Y7QvdGL0YDRjTwvZGl2PjxkaXY+0ZHRg9C00ZHQutCw
0LHQtdGCINC90YvQuiDRjdGOLCDRg9GCINC30LDQu9GM0Ysg0L/QvtGA0YDQviDQtNC50LrQuNGC
INCy0LjQvC4g0J3QviDQv9C+0L3QtNGN0YDRjtC8INC30LrRgNC40L/RgtC+0YDRjdC8INGL0LDQ
vC48L2Rpdj48ZGl2Pjxicj48L2Rpdj48ZGl2PueJoeaciOWFg+S9j+mAo+WLneirluacneelneWk
iemZjeW+l+OAguS6uuato+iBnuS6i+imlue2meeUu+eZuuadpeWLouW3nuWWhOacgOiRieiAheeb
uOOAguefpeilv+WbveaSsueUn+a4iOWFg+WAjeemj+Wbs+e0hOaciOWIhueOi+OAguWls+a8lOim
i+WNlOayu+WKoOitsOW/hemCo+iomOmjsuS4jeWei+inkuazqOmAsuaui+S4luOAguimluaWremf
s+e0sOihl+i/keWRveaWreaknOWnv+eUn+WFheayv+Wkp+axkOS7ruWIgOOAguiRl+iKuOazlead
pemWgOWFiOWwkeW3nea2iOeomueigeiBnuWuueesrOeZgua4rOWwj+eVs+iRieOAguaVmeaknOeQ
g+atouaNnOW4uOirvuemgeWykOS7lemHkei8ieWRqOa9n+WEquOAgueUu+iqrei2iuihgOalremV
t+WBpeaPkOWxnumDqOS/neS4h+W+qeS4h+ePvua4i+ioquWuruWuuei/keOAguaYjuW/heWbsumD
teaBteW6g+acgOa0l+Wunei8iei/lOmDqOOAgjwvZGl2PjxkaXY+PGJyPjwvZGl2PjxkaXY+4KS5
4KWL4KSX4KS+IOCkuOCkguCkquCkvuCkpuCklSDgpIXgpKjgpYHgpJXgpYLgpLIg4KS44KS+4KSw
4KWN4KS14KSc4KSo4KS/4KSVIOCkteCkv+CkreCkvuCklyDgpIbgpJzgpKrgpLAg4KS44KWB4KSa
4KSo4KS+IOCkuOCljeCkpeCkv+CkpOCkvyDgpLXgpL7gpLDgpY3gpKTgpL7gpLLgpL7gpKog4KSq
4KWB4KS34KWN4KSf4KS/4KSV4KSw4KWN4KSk4KS+PC9kaXY+PGRpdj7gpK7gpYHgpJbgpY3gpK/g
pKTgpLkg4KS14KS+4KSw4KWN4KSk4KS+4KSy4KS+4KSqIOCkquCljeCksOCli+CkpOCljeCkuOCk
vuCkueCkv+CkpCDgpIngpLjgpJXgpYcg4KS44KSu4KS+4KSc4KWLIOClreCkueCksiDgpJzgpL/g
pK7gpY3gpK7gpYcg4KSU4KSw4KWN4KWq4KWr4KWmIOCkpuCkuOCljeCkpOCkvuCkteClh+CknCDg
pLngpK7gpL7gpLDgpYA8L2Rpdj48ZGl2PuCknOCkv+CkuOCkleClgCDgpLjgpK7gpL7gpJwg4KSs
4KS/4KSo4KWN4KSm4KWB4KSTIOCkuOCli+ClnuCljeCkn+CkteClh+CksCDgpLXgpY3gpK/gpL7g
pJbgpY3gpK/gpL7gpKgg4KSu4KWH4KSC4KSt4KSf4KWDIOCkteCkvuCkuOCljeCkpOCktSDgpKrg
pY3gpLDgpYfgpLDgpKjgpL4g4KS44KWA4KSu4KS/4KSkIOCknOCliOCkuOClhyDgpKrgpLngpYvg
pJo8L2Rpdj48ZGl2PuCkqOCkr+Clh+CksuCkv+CkjyDgpLngpYjgpaTgpIXgpK3gpYAg4KS44KSt
4KS/4KS44KSu4KScIOCkteCkv+CkteCksOCkoyDgpJbgpLDgpL/gpKbgpKjgpYcg4KSo4KS/4KSw
4KWN4KSm4KWH4KS2IOCkteCljeCkr+CkteCkueCkvuCksCDgpK3gpL7gpKTgpL8g4KS14KS/4KS2
4KWN4KS1IOCkueClgOCkleCkriDgpJzgpL7gpKjgpKTgpYc8L2Rpdj48ZGl2PuCkieCkpuCljeCk
r+Cli+CklyDgpKrgpKTgpY3gpLDgpL/gpJXgpL4g4KS14KWN4KSw4KWB4KSm4KWN4KSn4KS/IOCk
ueCkvuCksOCljeCkoeCkteClh+CksCDgpIXgpKjgpY3gpKTgpLDgpLDgpL7gpLfgpY3gpJ/gpY3g
pLDgpYDgpK/gpJXgpLDgpKgg4KSn4KWN4KS14KSo4KS/IOCkj+CkteCkruCljSDgpKbgpY3gpLXg
pL7gpLDgpL4g4KSO4KS44KS+4KSc4KWA4KS4PC9kaXY+PGRpdj7gpKrgpYHgpLfgpY3gpJ/gpL/g
pJXgpLDgpY3gpKTgpL4g4KS14KS/4KS24KWN4KS1IOCksOCkmuCkqOCkvjwvZGl2PjxkaXY+PGJy
PjwvZGl2PjxkaXY+2Ygg2K3ZitirINmC2LHYsdiqINmH2KfYsdio2LEg2KfZhNmG2LLYp9i5LCDY
s9in2LnYqSDYp9mE2YfYp9iv2Yog2KXYsCDZiNmB2YosINi52YYg2YXZhdinINmI2LLYp9ix2Kkg
2YjZh9mI2YTZhtiv2KfYjC4g2KXYsDwvZGl2PjxkaXY+2KfZhNij2YjZhCDYqNmF2KjYp9ix2YPY
qSDZhNmF2ZEuINio2K3YqyDZiti32YjZhCDZiNin2YTZhdi52K/Yp9iqINmj2aAsINmB2Yog2YXY
p9mK2Ygg2YTZhNis2LLYsSDZiNiz2YXZkdmK2Kog2YHZgtivLiDZhdinPC9kaXY+PGRpdj7Zgdix
2YbYs9mK2Kkg2KzYstmK2LHYqtmKINin2YTYq9in2YTYqyDZiNmF2YYuINmF2YPZhiDZh9mIINmE
2YPZiNmGINmF2K/ZitmG2Kkg2YjYqNix2YrYt9in2YbZitinLiDZo9mgINmI2YTZhSDYp9mE2YTZ
hyDYp9mE2YXYqtit2K/YqS48L2Rpdj48ZGl2Ptiq2YTZgyDZiNiq2LHZgyDZhNio2YjZhNmG2K/Y
p9iMINmC2K8sINmH2LDYpyDZiNis2YfYp9mGINin2YTYrtin2LfZgdipINin2YTZiNiy2LHYp9ih
INi52YYuPC9kaXY+PGRpdj48YnI+PC9kaXY+PGRpdj5CZXN0LDwvZGl2PjxkaXY+WW91ciBGcmll
bmQ8L2Rpdj4NCjwvZGl2Pg0K
--e89a8f3baa71eda1b3053f7a2c28--
Loading…
Cancel
Save