From 0b741d3d0d4b587c72aff98a46913439dffa80ed Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 2 Aug 2023 20:01:32 -0700 Subject: [PATCH 1/2] Progress on making the `asEmailMessage` function --- extract_msg/msg_classes/message_base.py | 49 +++++++++++++++++++++++-- temp-changelog.md | 5 +++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 temp-changelog.md diff --git a/extract_msg/msg_classes/message_base.py b/extract_msg/msg_classes/message_base.py index 8907c9e8..d0273ce5 100644 --- a/extract_msg/msg_classes/message_base.py +++ b/extract_msg/msg_classes/message_base.py @@ -5,6 +5,7 @@ import base64 import datetime +import email.message import email.utils import functools import html @@ -20,7 +21,9 @@ import compressed_rtf import RTFDE -from email.parser import Parser as EmailParser +from email import policy +from email.message import EmailMessage +from email.parser import HeaderParser from typing import Callable, List, Optional, Union from .. import constants @@ -155,6 +158,43 @@ def _genRecipient(self, recipientType, recipientInt : RecipientType) -> Optional return value + def asEmailMessage(self) -> EmailMessage: + """ + Returns an instance of EmailMessage used to represent the contents of + this message. + """ + ret = EmailMessage() + + # Copy the headers. + for key, value in self.header.items(): + ret[key] = value + + # Attach the body to the EmailMessage instance. + if self.htmlBody: + ret.set_content(self.body, subtype = 'html', cte = 'quoted-printable') + elif self.body: + ret.set_content(self.body, cte = 'quoted-printable') + + # Process attachments. + for att in self.attachments: + if att.dataType: + if issubclass(att.dataType, bytes): + mime = att.mimetype or 'application/octet-stream' + mainType, subType = mime.split('/')[0], mime.split('/')[-1] + ret.add_attachment(att.data, + maintype = mainType, + subtype = subType, + filename = att.getFilename(), + cid = att.contentId) + elif issubclass(att.dataType, MSGFile): + if hasattr(att.dataType, 'asEmailMessage'): + ret.add_attachment( + att.data.asEmailMessage(), + filename = att.getFilename(), + cid = att.contentId) + + return ret + def deencapsulateBody(self, rtfBody : bytes, bodyType : DeencapType) -> Optional[Union[bytes, str]]: """ A function to deencapsulate the specified body from the rtfBody. Returns @@ -1009,11 +1049,12 @@ def header(self) -> email.message.Message: """ headerText = self.headerText if headerText: - header = EmailParser().parsestr(headerText) - header['date'] = self.date + header = HeaderParser(policy = policy.default).parsestr(headerText) + del header['Date'] + header['Date'] = self.date else: logger.info('Header is empty or was not found. Header will be generated from other streams.') - header = EmailParser().parsestr('') + header = HeaderParser(policy = policy.default).parsestr('') header.add_header('Date', self.date) header.add_header('From', self.sender) header.add_header('To', self.to) diff --git a/temp-changelog.md b/temp-changelog.md new file mode 100644 index 00000000..f6a8c671 --- /dev/null +++ b/temp-changelog.md @@ -0,0 +1,5 @@ +**v0.?.?** +* Changed imports in `message_base.py` to help with type checkers. +* Added new function `MessageBase.asEmailMessage` which will convert the `MessageBase` instance, if possible, to an `email.message.EmailMessage` object. If an embedded MSG file on a `MessageBase` object is of a class that does not have this function, it will simply be attached to the message as bytes. +* Changed from using `email.parser.EmailParser` to `email.parser.HeaderParser` in `MessageBase.header`. +* Changed some of the internal code for `MessageBase.header`. This should improve usage of it, and should not have any notiocable negative changes. You man notice some of the values parse slightly differently, but this effect should be mostly supressed. \ No newline at end of file From 2f45aaa83b87c056c662a65ef6ae39af39f8e2e7 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 2 Aug 2023 20:29:21 -0700 Subject: [PATCH 2/2] Ready for version 0.43.0 --- CHANGELOG.md | 6 +++++ README.rst | 4 +-- extract_msg/__init__.py | 2 +- extract_msg/msg_classes/message_base.py | 36 ++++++++++++++++++------- temp-changelog.md | 5 ---- 5 files changed, 35 insertions(+), 18 deletions(-) delete mode 100644 temp-changelog.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b3bef9..0fc594aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +**v0.43.0** +* [[TeamMsgExtractor #56](https://github.com/TeamMsgExtractor/msg-extractor/issues/56)] [[TeamMsgExtractor #248](https://github.com/TeamMsgExtractor/msg-extractor/issues/248)] Added new function `MessageBase.asEmailMessage` which will convert the `MessageBase` instance, if possible, to an `email.message.EmailMessage` object. If an embedded MSG file on a `MessageBase` object is of a class that does not have this function, it will simply be attached to the instance as bytes. +* Changed imports in `message_base.py` to help with type checkers. +* Changed from using `email.parser.EmailParser` to `email.parser.HeaderParser` in `MessageBase.header`. +* Changed some of the internal code for `MessageBase.header`. This should improve usage of it, and should not have any notiocable negative changes. You man notice some of the values parse slightly differently, but this effect should be mostly supressed. + **v0.42.2** * Fix bug in `AttachmentBase.mimetype` that would cause it to throw an error when accessed. This bug was introduced in `v0.42.0`. diff --git a/README.rst b/README.rst index 94a6f4f5..c6a6fd48 100644 --- a/README.rst +++ b/README.rst @@ -242,8 +242,8 @@ your access to the newest major version of extract-msg. .. |License: GPL v3| image:: https://img.shields.io/badge/License-GPLv3-blue.svg :target: LICENSE.txt -.. |PyPI3| image:: https://img.shields.io/badge/pypi-0.42.2-blue.svg - :target: https://pypi.org/project/extract-msg/0.42.2/ +.. |PyPI3| image:: https://img.shields.io/badge/pypi-0.43.0-blue.svg + :target: https://pypi.org/project/extract-msg/0.43.0/ .. |PyPI2| image:: https://img.shields.io/badge/python-3.8+-brightgreen.svg :target: https://www.python.org/downloads/release/python-3816/ diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 165b6ea7..69e50276 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -28,7 +28,7 @@ __author__ = 'Destiny Peterson & Matthew Walker' __date__ = '2023-08-02' -__version__ = '0.42.2' +__version__ = '0.43.0' __all__ = [ # Modules: diff --git a/extract_msg/msg_classes/message_base.py b/extract_msg/msg_classes/message_base.py index d0273ce5..bcc3f304 100644 --- a/extract_msg/msg_classes/message_base.py +++ b/extract_msg/msg_classes/message_base.py @@ -33,8 +33,8 @@ BodyTypes, DeencapType, ErrorBehavior, RecipientType, SaveType ) from ..exceptions import ( - DataNotFoundError, DeencapMalformedData, DeencapNotEncapsulated, - IncompatibleOptionsError, WKError + ConversionError, DataNotFoundError, DeencapMalformedData, + DeencapNotEncapsulated, IncompatibleOptionsError, WKError ) from .msg import MSGFile from ..structures.report_tag import ReportTag @@ -162,6 +162,10 @@ def asEmailMessage(self) -> EmailMessage: """ Returns an instance of EmailMessage used to represent the contents of this message. + + :raises ConversionError: The function failed to convert one of the + attachments into a form that it could attach, and the attachment + data type was not None. """ ret = EmailMessage() @@ -178,20 +182,32 @@ def asEmailMessage(self) -> EmailMessage: # Process attachments. for att in self.attachments: if att.dataType: - if issubclass(att.dataType, bytes): + if hasattr(att.dataType, 'asEmailMessage'): + # Replace the extension with '.eml'. + filename = att.getFilename() + if filename.lower().endswith('.msg'): + filename = filename[:-4] + '.eml' + ret.add_attachment( + att.data.asEmailMessage(), + filename = filename, + cid = att.contentId) + else: + if issubclass(att.dataType, bytes): + data = att.data + elif issubclass(att.dataType, MSGFile): + if hasattr(att.dataType, 'asBytes'): + data = att.asBytes + else: + data = att.data.exportBytes() + else: + raise ConversionError(f'Could not find a suitable method to attach attachment data type "{att.dataType}".') mime = att.mimetype or 'application/octet-stream' mainType, subType = mime.split('/')[0], mime.split('/')[-1] - ret.add_attachment(att.data, + ret.add_attachment(data, maintype = mainType, subtype = subType, filename = att.getFilename(), cid = att.contentId) - elif issubclass(att.dataType, MSGFile): - if hasattr(att.dataType, 'asEmailMessage'): - ret.add_attachment( - att.data.asEmailMessage(), - filename = att.getFilename(), - cid = att.contentId) return ret diff --git a/temp-changelog.md b/temp-changelog.md deleted file mode 100644 index f6a8c671..00000000 --- a/temp-changelog.md +++ /dev/null @@ -1,5 +0,0 @@ -**v0.?.?** -* Changed imports in `message_base.py` to help with type checkers. -* Added new function `MessageBase.asEmailMessage` which will convert the `MessageBase` instance, if possible, to an `email.message.EmailMessage` object. If an embedded MSG file on a `MessageBase` object is of a class that does not have this function, it will simply be attached to the message as bytes. -* Changed from using `email.parser.EmailParser` to `email.parser.HeaderParser` in `MessageBase.header`. -* Changed some of the internal code for `MessageBase.header`. This should improve usage of it, and should not have any notiocable negative changes. You man notice some of the values parse slightly differently, but this effect should be mostly supressed. \ No newline at end of file