extract-msg 0.55.0__tar.gz → 0.56.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {extract_msg-0.55.0 → extract_msg-0.56.1}/CHANGELOG.md +10 -0
  2. {extract_msg-0.55.0 → extract_msg-0.56.1}/PKG-INFO +3 -3
  3. {extract_msg-0.55.0 → extract_msg-0.56.1}/README.rst +2 -2
  4. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/__init__.py +2 -2
  5. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/__main__.py +2 -1
  6. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/custom_att_handler/lnk_obj_att.py +2 -2
  7. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/web_att.py +1 -1
  8. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/constants/ps.py +1 -1
  9. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/constants/st.py +2 -2
  10. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/message_base.py +118 -8
  11. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/utils.py +1 -1
  12. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg.egg-info/PKG-INFO +3 -3
  13. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg.egg-info/requires.txt +2 -2
  14. {extract_msg-0.55.0 → extract_msg-0.56.1}/requirements.txt +4 -2
  15. {extract_msg-0.55.0 → extract_msg-0.56.1}/LICENSE.txt +0 -0
  16. {extract_msg-0.55.0 → extract_msg-0.56.1}/MANIFEST.in +0 -0
  17. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/_rtf/__init__.py +0 -0
  18. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/_rtf/create_doc.py +0 -0
  19. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/_rtf/inject_rtf.py +0 -0
  20. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/_rtf/token.py +0 -0
  21. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/_rtf/tokenize_rtf.py +0 -0
  22. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/__init__.py +0 -0
  23. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/attachment.py +0 -0
  24. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/attachment_base.py +0 -0
  25. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/broken_att.py +0 -0
  26. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/custom_att.py +0 -0
  27. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/custom_att_handler/__init__.py +0 -0
  28. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/custom_att_handler/custom_handler.py +0 -0
  29. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/custom_att_handler/outlook_image_dib.py +0 -0
  30. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/custom_att_handler/outlook_image_meta.py +0 -0
  31. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/emb_msg_att.py +0 -0
  32. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/signed_att.py +0 -0
  33. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/attachments/unsupported_att.py +0 -0
  34. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/constants/__init__.py +0 -0
  35. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/constants/re.py +0 -0
  36. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/data/logging-config/logging-nt.json +0 -0
  37. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/data/logging-config/logging-posix.json +0 -0
  38. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/__init__.py +0 -0
  39. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/_dt/__init__.py +0 -0
  40. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/_dt/_mac_ce.py +0 -0
  41. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/_dt/_mac_cyrillic.py +0 -0
  42. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/_dt/_mac_greek.py +0 -0
  43. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/_dt/_mac_iceland.py +0 -0
  44. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/_dt/_mac_turkish.py +0 -0
  45. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/_dt/_win874_dec.py +0 -0
  46. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/_dt/_win950_dec.py +0 -0
  47. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/encoding/utils.py +0 -0
  48. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/enums.py +0 -0
  49. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/exceptions.py +0 -0
  50. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/__init__.py +0 -0
  51. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/appointment.py +0 -0
  52. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/calendar.py +0 -0
  53. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/calendar_base.py +0 -0
  54. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/contact.py +0 -0
  55. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/journal.py +0 -0
  56. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/meeting_cancellation.py +0 -0
  57. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/meeting_exception.py +0 -0
  58. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/meeting_forward.py +0 -0
  59. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/meeting_related.py +0 -0
  60. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/meeting_request.py +0 -0
  61. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/meeting_response.py +0 -0
  62. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/message.py +0 -0
  63. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/message_signed.py +0 -0
  64. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/message_signed_base.py +0 -0
  65. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/msg.py +0 -0
  66. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/post.py +0 -0
  67. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/sticky_note.py +0 -0
  68. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/task.py +0 -0
  69. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/msg_classes/task_request.py +0 -0
  70. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/null_date.py +0 -0
  71. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/ole_writer.py +0 -0
  72. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/open_msg.py +0 -0
  73. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/properties/__init__.py +0 -0
  74. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/properties/named.py +0 -0
  75. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/properties/prop.py +0 -0
  76. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/properties/properties_store.py +0 -0
  77. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/py.typed +0 -0
  78. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/recipient.py +0 -0
  79. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/__init__.py +0 -0
  80. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/_helpers.py +0 -0
  81. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/business_card.py +0 -0
  82. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/cfoas.py +0 -0
  83. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/contact_link_entry.py +0 -0
  84. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/dev_mode_a.py +0 -0
  85. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/dv_target_device.py +0 -0
  86. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/entry_id.py +0 -0
  87. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/misc_id.py +0 -0
  88. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/mon_stream.py +0 -0
  89. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/odt.py +0 -0
  90. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/ole_pres.py +0 -0
  91. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/ole_stream_struct.py +0 -0
  92. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/recurrence_pattern.py +0 -0
  93. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/report_tag.py +0 -0
  94. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/system_time.py +0 -0
  95. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/time_zone_definition.py +0 -0
  96. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/time_zone_struct.py +0 -0
  97. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/toc_entry.py +0 -0
  98. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg/structures/tz_rule.py +0 -0
  99. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg.egg-info/SOURCES.txt +0 -0
  100. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg.egg-info/dependency_links.txt +0 -0
  101. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg.egg-info/entry_points.txt +0 -0
  102. {extract_msg-0.55.0 → extract_msg-0.56.1}/extract_msg.egg-info/top_level.txt +0 -0
  103. {extract_msg-0.55.0 → extract_msg-0.56.1}/setup.cfg +0 -0
  104. {extract_msg-0.55.0 → extract_msg-0.56.1}/setup.py +0 -0
@@ -1,3 +1,13 @@
1
+ **v0.56.1**
2
+ * [[TeamMsgExtractor #484](https://github.com/TeamMsgExtractor/msg-extractor/issues/484)] Corrected the way the HTML body is decoded in `MessageBase.asEmailMessage()` to prevent encoding errors.
3
+
4
+ **v0.56.0**
5
+ * [[TeamMsgExtractor #478](https://github.com/TeamMsgExtractor/msg-extractor/issues/478)] Expand allowable versions of `ebcdic` to include `2.x`. The 2.0 release is a pure technical release with no functional changes; it only drops support for Python 2 and 3.8. The lower bound is unchanged so installs on Python 3.8 will continue to resolve to `ebcdic 1.1.1`.
6
+ * [[TeamMsgExtractor #469](https://github.com/TeamMsgExtractor/msg-extractor/issues/469)] Expanded `beautifulsoup` to allow for versions up to `5.*.*`. I was previously hesitant to do so because they sometimes do what seems like non backwards compatible changes in minor versions, but it looks like this should be fine.
7
+ * [[TeamMsgExtractor #471](https://github.com/TeamMsgExtractor/msg-extractor/issues/471)] Added `.strip()` to file name determination to ensure that folders with trailing spaces would be corrected before being used.
8
+ * [[TeamMsgExtractor #481](https://github.com/TeamMsgExtractor/msg-extractor/issues/481)] Fixed a typo that caused an HTML body created from plain text to end with `</head>` instead of `</html>`.
9
+ * [[TeamMsgExtractor #476](https://github.com/TeamMsgExtractor/msg-extractor/issues/476)] Fixed `MessageBase.asEmailMessage()` raising `ValueError` when a message has multiple `To` recipients.
10
+
1
11
  **v0.55.0**
2
12
  * [[TeamMsgExtractor #465](https://github.com/TeamMsgExtractor/msg-extractor/issues/465)] Added missing `msg.close()` to `openMsg()`. If the MSG file was actually just a plain OLE file, it would be left open.
3
13
  * Adjusted the default value of `maxNameLength` for `MessageBase.save()` to 40 instead of 256.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: extract_msg
3
- Version: 0.55.0
3
+ Version: 0.56.1
4
4
  Summary: Extracts emails and attachments saved in Microsoft Outlook's .msg files
5
5
  Home-page: https://github.com/TeamMsgExtractor/msg-extractor
6
6
  Download-URL: https://github.com/TeamMsgExtractor/msg-extractor/archives/master
@@ -278,8 +278,8 @@ your access to the newest major version of extract-msg.
278
278
  .. |License: GPL v3| image:: https://img.shields.io/badge/License-GPLv3-blue.svg
279
279
  :target: LICENSE.txt
280
280
 
281
- .. |PyPI3| image:: https://img.shields.io/badge/pypi-0.55.0-blue.svg
282
- :target: https://pypi.org/project/extract-msg/0.55.0/
281
+ .. |PyPI3| image:: https://img.shields.io/badge/pypi-0.56.1-blue.svg
282
+ :target: https://pypi.org/project/extract-msg/0.56.1/
283
283
 
284
284
  .. |PyPI2| image:: https://img.shields.io/badge/python-3.8+-brightgreen.svg
285
285
  :target: https://www.python.org/downloads/release/python-3810/
@@ -260,8 +260,8 @@ your access to the newest major version of extract-msg.
260
260
  .. |License: GPL v3| image:: https://img.shields.io/badge/License-GPLv3-blue.svg
261
261
  :target: LICENSE.txt
262
262
 
263
- .. |PyPI3| image:: https://img.shields.io/badge/pypi-0.55.0-blue.svg
264
- :target: https://pypi.org/project/extract-msg/0.55.0/
263
+ .. |PyPI3| image:: https://img.shields.io/badge/pypi-0.56.1-blue.svg
264
+ :target: https://pypi.org/project/extract-msg/0.56.1/
265
265
 
266
266
  .. |PyPI2| image:: https://img.shields.io/badge/python-3.8+-brightgreen.svg
267
267
  :target: https://www.python.org/downloads/release/python-3810/
@@ -27,8 +27,8 @@ https://github.com/TeamMsgExtractor/msg-extractor
27
27
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
28
28
 
29
29
  __author__ = 'Destiny Peterson & Matthew Walker'
30
- __date__ = '2025-08-12'
31
- __version__ = '0.55.0'
30
+ __date__ = '2026-08-13'
31
+ __version__ = '0.56.1'
32
32
 
33
33
  __all__ = [
34
34
  # Modules:
@@ -101,7 +101,7 @@ def main(argv: List[str] = sys.argv) -> None:
101
101
  msg.saveAttachments(**kwargs)
102
102
  else:
103
103
  msg.save(**kwargs)
104
- except Exception as e:
104
+ except Exception:
105
105
  try:
106
106
  print(f'Error with file "{x}": {traceback.format_exc()}')
107
107
  except UnicodeEncodeError:
@@ -111,5 +111,6 @@ def main(argv: List[str] = sys.argv) -> None:
111
111
  if createdZip:
112
112
  _zip.close()
113
113
 
114
+
114
115
  if __name__ == '__main__':
115
116
  main(sys.argv)
@@ -7,7 +7,7 @@ __all__ = [
7
7
 
8
8
 
9
9
  from functools import cached_property
10
- from typing import List, Optional, TYPE_CHECKING
10
+ from typing import Optional, TYPE_CHECKING
11
11
 
12
12
  from . import registerHandler
13
13
  from .custom_handler import CustomAttachmentHandler
@@ -106,4 +106,4 @@ class LinkedObjectAttachment(CustomAttachmentHandler):
106
106
 
107
107
 
108
108
 
109
- registerHandler(LinkedObjectAttachment)
109
+ registerHandler(LinkedObjectAttachment)
@@ -71,4 +71,4 @@ class WebAttachment(AttachmentBase):
71
71
  The url for the web attachment. If this is not set, that is probably an
72
72
  error.
73
73
  """
74
- return self.longPathname
74
+ return self.longPathname
@@ -43,4 +43,4 @@ PSETID_AIRSYNC: Final[str] = '{71035549-0739-4DCB-9163-00F0580DBBDF}'
43
43
  PSETID_SHARING: Final[str] = '{00062040-0000-0000-C000-000000000046}'
44
44
  PSETID_XMLEXTRACTEDENTITIES: Final[str] = '{23239608-685D-4732-9C55-4C95CB4E8E33}'
45
45
  PSETID_ATTACHMENT: Final[str] = '{96357F7F-59E1-47D0-99A7-46515C183B54}'
46
- PSETID_CALENDAR_ASSISTANT: Final[str] = '{11000E07-B51B-40D6-AF21-CAA85EDAB1D0}'
46
+ PSETID_CALENDAR_ASSISTANT: Final[str] = '{11000E07-B51B-40D6-AF21-CAA85EDAB1D0}'
@@ -93,7 +93,7 @@ ST_BE_UI8: Final[struct.Struct] = struct.Struct('>B')
93
93
  ST_BE_UI16: Final[struct.Struct] = struct.Struct('>H')
94
94
  ST_BE_UI32: Final[struct.Struct] = struct.Struct('>I')
95
95
  ST_BE_UI64: Final[struct.Struct] = struct.Struct('>Q')
96
- ST_BE_F32: Final[struct.Struct] = struct.Struct('>f')
96
+ ST_BE_F32: Final[struct.Struct] = struct.Struct('>f')
97
97
  ST_BE_F64: Final[struct.Struct] = struct.Struct('>d')
98
98
  # Structs that use the system byte order, where consistency on a single system
99
99
  # is all that matters. Mainly used for quick casts between signed and unsigned.
@@ -107,4 +107,4 @@ ST_SBO_UI32: Final[struct.Struct] = struct.Struct('@I')
107
107
  ST_SBO_UI64: Final[struct.Struct] = struct.Struct('@Q')
108
108
  # Struct for an RGB value that, in little endian, would be an int written in hex
109
109
  # as 0x00BBGGRR.
110
- ST_RGB: Final[struct.Struct] = struct.Struct('<BBBx')
110
+ ST_RGB: Final[struct.Struct] = struct.Struct('<BBBx')
@@ -25,6 +25,7 @@ import RTFDE.exceptions
25
25
 
26
26
  from email import policy
27
27
  from email.charset import Charset, QP
28
+ from email.header import decode_header as _decode_header, Header as _Header
28
29
  from email.message import EmailMessage
29
30
  from email.mime.multipart import MIMEMultipart
30
31
  from email.mime.text import MIMEText
@@ -56,6 +57,77 @@ from ..utils import (
56
57
  logger = logging.getLogger(__name__)
57
58
  logger.addHandler(logging.NullHandler())
58
59
 
60
+ _RFC2047_WORD = re.compile(r'=\?[^?]+\?[bBqQ]\?[^?]*\?=')
61
+
62
+ # Encodings to try when a declared charset fails, keyed by normalised charset name.
63
+ # GBK is a strict superset of GB2312 and accepts the ASCII-range second bytes that
64
+ # GB2312 rejects, so real-world GB2312-labelled headers often decode correctly as GBK.
65
+ _CHARSET_FALLBACKS: Dict[str, Tuple[str, ...]] = {
66
+ 'gb2312': ('gbk', 'cp936'),
67
+ 'gb_2312': ('gbk', 'cp936'),
68
+ 'gb_2312-80': ('gbk', 'cp936'),
69
+ }
70
+
71
+
72
+ def _fix_encoded_word(m: re.Match) -> str:
73
+ """
74
+ Regex substitution callback: decode one RFC 2047 encoded word and
75
+ re-emit it as a valid UTF-8 encoded word.
76
+
77
+ If the declared charset fails (e.g. GB2312-labelled GBK bytes), charset
78
+ fallbacks from _CHARSET_FALLBACKS are tried before falling back to
79
+ latin-1 with replacement characters.
80
+ """
81
+ word = m.group(0)
82
+ try:
83
+ parts = _decode_header(word)
84
+ if len(parts) != 1 or not isinstance(parts[0][0], bytes):
85
+ return word
86
+ btext, cs = parts[0]
87
+ try:
88
+ text = btext.decode(cs or 'ascii')
89
+ except (UnicodeDecodeError, LookupError):
90
+ for fallback in _CHARSET_FALLBACKS.get((cs or '').lower(), ()):
91
+ try:
92
+ text = btext.decode(fallback)
93
+ break
94
+ except (UnicodeDecodeError, LookupError):
95
+ continue
96
+ else:
97
+ text = btext.decode('latin-1', 'replace')
98
+ return str(_Header(text, charset='utf-8'))
99
+ except Exception:
100
+ return word
101
+
102
+
103
+ def _preprocess_encoded_words(text: str) -> str:
104
+ """
105
+ Replace every RFC 2047 encoded word in *text* with a clean UTF-8
106
+ encoded word. Safe to apply to an entire raw header block before
107
+ feeding it to the modern email policy parser.
108
+ """
109
+ return _RFC2047_WORD.sub(_fix_encoded_word, text)
110
+
111
+
112
+ def _sanitize_header(value: str) -> str:
113
+ """
114
+ Prepare a header value from a compat32-parsed Message for safe assignment
115
+ to an EmailMessage (fallback path when no raw headerText is available).
116
+
117
+ RFC 2047 encoded words are re-encoded as UTF-8; raw non-ASCII characters
118
+ are also RFC 2047-encoded so that EmailMessage.as_bytes() never raises
119
+ UnicodeEncodeError.
120
+ """
121
+ if '=?' in value:
122
+ value = _RFC2047_WORD.sub(_fix_encoded_word, value)
123
+
124
+ try:
125
+ value.encode('ascii')
126
+ except (UnicodeEncodeError, UnicodeDecodeError):
127
+ value = str(_Header(value, charset='utf-8'))
128
+
129
+ return value
130
+
59
131
 
60
132
  class MessageBase(MSGFile):
61
133
  """
@@ -165,10 +237,48 @@ class MessageBase(MSGFile):
165
237
  """
166
238
  ret = EmailMessage()
167
239
 
168
- # Copy the headers.
169
- for key, value in self.header.items():
170
- if key.lower() != 'content-type':
171
- ret[key] = value.replace('\r\n', '').replace('\n', '')
240
+ # Prefer the raw transport-header block: pre-fix any malformed RFC 2047
241
+ # encoded words (e.g. GB2312-labelled GBK bytes), then parse with the
242
+ # modern policy so that RFC 5322 unfolding and RFC 2047 decoding are
243
+ # handled natively and the values arrive as clean Unicode strings.
244
+ # Fall back to the compat32-derived self.header when no raw text is
245
+ # stored (synthesised headers from MAPI properties).
246
+ _ADDRESS_HEADERS = frozenset({'to', 'cc', 'bcc', 'reply-to'})
247
+ if self.headerText:
248
+ raw = self.headerText
249
+ if raw.startswith('Microsoft Mail Internet Headers Version 2.0'):
250
+ raw = raw[43:].lstrip()
251
+ raw = _preprocess_encoded_words(raw)
252
+ source_items = list(HeaderParser(policy = policy.default).parsestr(raw).items())
253
+ sanitize = False
254
+ else:
255
+ source_items = list(self.header.items())
256
+ sanitize = True
257
+
258
+ # Address headers (To/CC/BCC/Reply-To) may appear once per recipient in
259
+ # the stored header block; merge them into a single comma-separated value.
260
+ # All other headers — including multi-valued trace headers like Received
261
+ # and Authentication-Results — are forwarded as-is; EmailMessage appends
262
+ # rather than replaces, so natural repetition is preserved correctly.
263
+ address_merged: Dict[str, Tuple[str, str]] = {}
264
+ for key, value in source_items:
265
+ if key.lower() == 'content-type':
266
+ continue
267
+ if sanitize:
268
+ # compat32 values are folded and may contain raw encoded words.
269
+ value = re.sub(r'\r?\n[ \t]', ' ', value)
270
+ value = value.replace('\r\n', '').replace('\n', '')
271
+ value = _sanitize_header(value)
272
+ lower = key.lower()
273
+ if lower in _ADDRESS_HEADERS:
274
+ if lower in address_merged:
275
+ address_merged[lower] = (address_merged[lower][0], address_merged[lower][1] + ', ' + value)
276
+ else:
277
+ address_merged[lower] = (key, value)
278
+ else:
279
+ ret[key] = value
280
+ for _, (key, value) in address_merged.items():
281
+ ret[key] = value
172
282
 
173
283
  ret['Content-Type'] = 'multipart/mixed'
174
284
 
@@ -184,7 +294,7 @@ class MessageBase(MSGFile):
184
294
  if self.body:
185
295
  bodyParts.attach(MIMEText(self.body, 'plain', c))
186
296
  if self.htmlBody:
187
- bodyParts.attach(MIMEText(self.htmlBody.decode('utf-8'), 'html', c))
297
+ bodyParts.attach(MIMEText(str(bs4.BeautifulSoup(self.htmlBody, features = 'html.parser')), 'html', c))
188
298
 
189
299
  # Process attachments.
190
300
  for att in self.attachments:
@@ -855,9 +965,9 @@ class MessageBase(MSGFile):
855
965
  raise ValueError(f'Invalid filename found in self.filename: "{self.filename}"')
856
966
 
857
967
  # Add the file name to the path.
858
- path /= filename[:maxNameLength]
968
+ path /= filename[:maxNameLength].strip()
859
969
  else:
860
- path /= self.defaultFolderName[:maxNameLength]
970
+ path /= self.defaultFolderName[:maxNameLength].strip()
861
971
 
862
972
  # Create the folders.
863
973
  if not _zip:
@@ -1178,7 +1288,7 @@ class MessageBase(MSGFile):
1178
1288
  # Convert the plain text body to html.
1179
1289
  logger.info('HTML body was not found, attempting to generate from plain text body.')
1180
1290
  correctedBody = html.escape(self.body).replace('\r', '').replace('\n', '<br />')
1181
- htmlBody = f'<html><body>{correctedBody}</body></head>'.encode('ascii', 'xmlcharrefreplace')
1291
+ htmlBody = f'<html><body>{correctedBody}</body></html>'.encode('ascii', 'xmlcharrefreplace')
1182
1292
 
1183
1293
  if not htmlBody:
1184
1294
  logger.info('HTML body could not be found nor generated.')
@@ -458,7 +458,7 @@ def getCommandArgs(args: Sequence[str]) -> argparse.Namespace:
458
458
  help='Extracts the embedded MSG files as MSG files instead of running their save functions.')
459
459
  # --overwrite-existing
460
460
  parser.add_argument('--overwrite-existing', dest='overwriteExisting', action='store_true',
461
- help='Disables filename conflict resolution code for attachments when saving a file, causing files to be overwriten if two attachments with the same filename are on an MSG file.')
461
+ help='Disables filename conflict resolution code for attachments when saving a file, causing files to be overwritten if two attachments with the same filename are on an MSG file.')
462
462
  # --skip-not-implemented
463
463
  parser.add_argument('--skip-not-implemented', '--skip-ni', dest='skipNotImplemented', action='store_true',
464
464
  help='Skips any attachments that are not implemented, allowing saving of the rest of the message.')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: extract-msg
3
- Version: 0.55.0
3
+ Version: 0.56.1
4
4
  Summary: Extracts emails and attachments saved in Microsoft Outlook's .msg files
5
5
  Home-page: https://github.com/TeamMsgExtractor/msg-extractor
6
6
  Download-URL: https://github.com/TeamMsgExtractor/msg-extractor/archives/master
@@ -278,8 +278,8 @@ your access to the newest major version of extract-msg.
278
278
  .. |License: GPL v3| image:: https://img.shields.io/badge/License-GPLv3-blue.svg
279
279
  :target: LICENSE.txt
280
280
 
281
- .. |PyPI3| image:: https://img.shields.io/badge/pypi-0.55.0-blue.svg
282
- :target: https://pypi.org/project/extract-msg/0.55.0/
281
+ .. |PyPI3| image:: https://img.shields.io/badge/pypi-0.56.1-blue.svg
282
+ :target: https://pypi.org/project/extract-msg/0.56.1/
283
283
 
284
284
  .. |PyPI2| image:: https://img.shields.io/badge/python-3.8+-brightgreen.svg
285
285
  :target: https://www.python.org/downloads/release/python-3810/
@@ -1,8 +1,8 @@
1
1
  olefile==0.47
2
2
  tzlocal<6,>=4.2
3
3
  compressed-rtf<2,>=1.0.6
4
- ebcdic<2,>=1.1.1
5
- beautifulsoup4<4.14,>=4.11.1
4
+ ebcdic<3,>=1.1.1
5
+ beautifulsoup4<5,>=4.11.1
6
6
  RTFDE<0.2,>=0.1.1
7
7
  red-black-tree-mod<=1.23,>=1.20
8
8
 
@@ -5,7 +5,9 @@
5
5
  olefile==0.47
6
6
  tzlocal>=4.2,<6
7
7
  compressed-rtf>=1.0.6,<2
8
- ebcdic>=1.1.1,<2
9
- beautifulsoup4>=4.11.1,<4.14
8
+ ebcdic>=1.1.1,<3
9
+ # Beautiful soup can sometimes make weird versioning, so something *could*
10
+ # break, but so long as they at least reasonably do sem-var this should be fine.
11
+ beautifulsoup4>=4.11.1,<5
10
12
  RTFDE>=0.1.1,<0.2
11
13
  red-black-tree-mod>=1.20, <=1.23
File without changes
File without changes
File without changes
File without changes