From 2334b2f39949030a8fa3332c374e281afca9d246 Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Wed, 25 Nov 2020 21:01:49 +0200 Subject: [PATCH 1/8] put cmd-line attch zip generation support where it belongs --- tnefparse/cmdline.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tnefparse/cmdline.py b/tnefparse/cmdline.py index 8862803..65b2ece 100644 --- a/tnefparse/cmdline.py +++ b/tnefparse/cmdline.py @@ -5,7 +5,7 @@ import sys from . import properties -from .tnef import TNEF +from .tnef import TNEF, to_zip logging.basicConfig() logger = logging.getLogger(__package__) @@ -25,6 +25,9 @@ argument('-a', '--attachments', action='store_true', help='extract attachments, by default to current dir') +argument('-z', '--zip', action='store_true', + help='extract attachments to zip file, by default to current dir') + argument('-p', '--path', help='optional explicit path to extract attachments to') @@ -97,6 +100,16 @@ def tnefparse(): sys.stderr.write("Successfully wrote %i files\n" % len(t.attachments)) sys.exit() + elif args.zip: + tfp.seek(0) + pth = args.path.rstrip(os.sep) + os.sep if args.path else '' + test = tfp.read() + a = to_zip(test) + with open(pth + 'attachments.zip', "wb") as afp: + afp.write(a) + sys.stderr.write("Successfully wrote attachments.zip\n") + sys.exit() + def print_body(attr, description): body = getattr(t, attr) if body is None: From 445b4c251bc25fdfb4887a9bef7e3098d263ad1c Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Wed, 25 Nov 2020 21:18:02 +0200 Subject: [PATCH 2/8] add test to exercise zip export --- tests/test_cmdline.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 993a96c..04596ef 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -1,3 +1,4 @@ +import os import json import shutil import sys @@ -25,6 +26,15 @@ def test_cmdline_attch_extract(script_runner): shutil.rmtree(tmpdir) +def test_cmdline_zip_extract(script_runner): + tmpdir = tempfile.mkdtemp() + ret = script_runner.run('tnefparse', '-z', '-p', tmpdir, 'tests/examples/one-file.tnef') + assert os.path.isfile(tmpdir + '/attachments.zip') + assert ret.stderr == 'Successfully wrote attachments.zip\n' + assert ret.success + shutil.rmtree(tmpdir) + + def test_cmdline_no_body(script_runner): ret = script_runner.run('tnefparse', '-b', 'tests/examples/one-file.tnef') assert ret.stderr == 'No body found\n' From 14f175278856e7e971ab43850a6c04ad18a24b2e Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Wed, 25 Nov 2020 23:45:48 +0200 Subject: [PATCH 3/8] add changelog entry, credit Beercow --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 8493b16..dfc2b1b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,7 @@ tnefparse 1.4.0 (unreleased) - drop Python 2 support - drop Python 3.5 support (jugmac00) - add Python 3.9 support (jugmac00) +- command-line support for zipped export of attachments (Beercow) tnefparse 1.3.1 (2020-09-30) ============================= From 45590d68d5bf38cbd8b6ec09229d44b8e3531b10 Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Wed, 25 Nov 2020 23:58:22 +0200 Subject: [PATCH 4/8] document zip export feature --- README.rst | 1 + tnefparse/cmdline.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0bc858b..e1abdd0 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,7 @@ found inside them and so on:: -h, --help show this help message and exit -o, --overview show (possibly long) overview of TNEF file contents -a, --attachments extract attachments, by default to current dir + -z, --zip extract attachments into a single zip file, by default to current dir -p PATH, --path PATH optional explicit path to extract attachments to -b, --body extract the body to stdout -hb, --htmlbody extract the HTML body to stdout diff --git a/tnefparse/cmdline.py b/tnefparse/cmdline.py index 65b2ece..1af4f06 100644 --- a/tnefparse/cmdline.py +++ b/tnefparse/cmdline.py @@ -26,7 +26,7 @@ help='extract attachments, by default to current dir') argument('-z', '--zip', action='store_true', - help='extract attachments to zip file, by default to current dir') + help='extract attachments into a single zip file, by default to current dir') argument('-p', '--path', help='optional explicit path to extract attachments to') From bd498832bc3b525cd014f70d22a56259c899bd44 Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Thu, 26 Nov 2020 19:02:00 +0200 Subject: [PATCH 5/8] add test to verify attachments zip file list --- tests/test_cmdline.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 04596ef..59433fb 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -1,4 +1,6 @@ import os +import io +import zipfile import json import shutil import sys @@ -31,6 +33,10 @@ def test_cmdline_zip_extract(script_runner): ret = script_runner.run('tnefparse', '-z', '-p', tmpdir, 'tests/examples/one-file.tnef') assert os.path.isfile(tmpdir + '/attachments.zip') assert ret.stderr == 'Successfully wrote attachments.zip\n' + with open(tmpdir + '/attachments.zip', 'rb') as zip_fp: + zip_stream = io.BytesIO(zip_fp.read()) + zip_file = zipfile.ZipFile(zip_stream) + assert zip_file.namelist() == ['AUTHORS'] assert ret.success shutil.rmtree(tmpdir) From 4ab0b26b57c31cb088a6c8d024e4ada77fb359de Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Thu, 26 Nov 2020 20:55:41 +0200 Subject: [PATCH 6/8] streamline & optimize zip export --- tnefparse/cmdline.py | 6 ++---- tnefparse/tnef.py | 29 ++++++++++++++++------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tnefparse/cmdline.py b/tnefparse/cmdline.py index 1af4f06..bc0b2b1 100644 --- a/tnefparse/cmdline.py +++ b/tnefparse/cmdline.py @@ -101,12 +101,10 @@ def tnefparse(): sys.exit() elif args.zip: - tfp.seek(0) + zipped = to_zip(t) pth = args.path.rstrip(os.sep) + os.sep if args.path else '' - test = tfp.read() - a = to_zip(test) with open(pth + 'attachments.zip', "wb") as afp: - afp.write(a) + afp.write(zipped) sys.stderr.write("Successfully wrote attachments.zip\n") sys.exit() diff --git a/tnefparse/tnef.py b/tnefparse/tnef.py index 255c89f..33de0ba 100644 --- a/tnefparse/tnef.py +++ b/tnefparse/tnef.py @@ -2,6 +2,11 @@ """ import logging import os +import warnings +from typing import Union +from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED +from io import BytesIO +from functools import singledispatch from datetime import datetime from uuid import UUID @@ -389,13 +394,15 @@ def triples(data): return sender.rstrip(b'\x00'), etype, email.rstrip(b'\x00') +def to_zip(tnef:Union[TNEF, bytes], default_name='no-name', deflate=True): + "Convert attachments in TNEF data to zip format." -def to_zip(data, default_name='no-name', deflate=True): - "Convert attachments in TNEF data to zip format. Accepts and returns str type." - # Parse the TNEF data - tnef = TNEF(data) + if isinstance(tnef, bytes): + msg = "passing bytes to tnef.to_zip will be deprecated, pass a TNEF object instead" + warnings.warn(msg, DeprecationWarning) + tnef = TNEF(tnef) - # Convert the TNEF file to an equivalent ZIP file + # Extract attachments found in the TNEF object tozip = {} for attachment in tnef.attachments: filename = attachment.name or default_name @@ -407,18 +414,14 @@ def to_zip(data, default_name='no-name', deflate=True): else: tozip[filename] = [(attachment.data, filename)] - # Add each attachment in the TNEF file to the zip file - from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED - from io import BytesIO - import contextlib - + # Add each attachment to the zip file sfp = BytesIO() - zf = ZipFile(sfp, "w", ZIP_DEFLATED if deflate else ZIP_STORED) - with contextlib.closing(zf) as z: + with ZipFile(sfp, "w", ZIP_DEFLATED if deflate else ZIP_STORED) as zf: for filename, entries in list(tozip.items()): for entry in entries: data, name = entry - z.writestr(name, data) + zf.writestr(name, data) # Return the binary data for the zip file return sfp.getvalue() + From ef8539eff2c0fcbc0f2d039b6e0d12aff6013c0b Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Thu, 26 Nov 2020 21:03:48 +0200 Subject: [PATCH 7/8] make linters happy --- tnefparse/tnef.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tnefparse/tnef.py b/tnefparse/tnef.py index 33de0ba..1bd5c0b 100644 --- a/tnefparse/tnef.py +++ b/tnefparse/tnef.py @@ -6,7 +6,6 @@ from typing import Union from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED from io import BytesIO -from functools import singledispatch from datetime import datetime from uuid import UUID @@ -394,7 +393,8 @@ def triples(data): return sender.rstrip(b'\x00'), etype, email.rstrip(b'\x00') -def to_zip(tnef:Union[TNEF, bytes], default_name='no-name', deflate=True): + +def to_zip(tnef: Union[TNEF, bytes], default_name='no-name', deflate=True): "Convert attachments in TNEF data to zip format." if isinstance(tnef, bytes): @@ -424,4 +424,3 @@ def to_zip(tnef:Union[TNEF, bytes], default_name='no-name', deflate=True): # Return the binary data for the zip file return sfp.getvalue() - From 96d6805b32de0f3511960040588851977e4f9d11 Mon Sep 17 00:00:00 2001 From: Petri Savolainen Date: Thu, 26 Nov 2020 21:17:50 +0200 Subject: [PATCH 8/8] marked test_zip for removal at later time --- tests/test_decoding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_decoding.py b/tests/test_decoding.py index d385aab..464db5d 100644 --- a/tests/test_decoding.py +++ b/tests/test_decoding.py @@ -105,6 +105,7 @@ def test_decode(tnefspec): def test_zip(): + # remove this test once tnef.to_zip(bytes) is no longer supported with open(datadir + os.sep + 'one-file.tnef', "rb") as tfile: zip_data = to_zip(tfile.read()) with tempfile.TemporaryFile() as out: