From ef981863b193695333192abf74c1619e07cacbb2 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Nov 2023 10:04:48 -0800 Subject: [PATCH 01/17] Attempt to transfer type support doc from wiki --- docs/type-support.csv | 17 +++++++++++++++++ docs/type_support.rst | 10 ++++++++++ 2 files changed, 27 insertions(+) create mode 100644 docs/type-support.csv create mode 100644 docs/type_support.rst diff --git a/docs/type-support.csv b/docs/type-support.csv new file mode 100644 index 00000000..0a0eca09 --- /dev/null +++ b/docs/type-support.csv @@ -0,0 +1,17 @@ +Message Class,Class Name,Support Level,Support Added,Saving Added,Saving Completed +IPM,`MSGFile`,Open,0.29.0,Won't Add, +REPORT.*,,,,, +IPM.Note.*,`Message`,Full,0.29.0,0.29.0,0.35.0 +IPM.Activity.*,`Journal`,Full,0.46.0,0.46.0,0.46.0 +IPM.Appointment.*,`AppointmentMeeting`(`Appointment` before 0.35.0),Full,0.29.0,0.35.0,0.35.0 +IPM.Contact.*,,,,, +IPM.Distlist.*,`Contact`,Full,0.29.0,0.35.0,0.35.0 +IPM.Note.*.SMIME,,,,, +IPM.Note.*.SMIME.MultipartSigned,`MessageSigned`,Full,0.31.0,0.31.0,0.35.0 +IPM.Post.*,`Post`,Full,0.35.0,0.35.0,0.35.0 +IPM.Schedule.Meeting.Cancelation.*,`MeetingCancellation`,Full,0.35.0,0.35.0,0.35.0 +IPM.Schedule.Meeting.Request.*,`MeetingRequest`,Full,0.35.0,0.35.0,0.35.0 +IPM.Schedule.Meeting.Resp.*,`MeetingResponse`,Full,0.35.0,0.35.0,0.35.0 +IPM.Schedule.Meeting.Notification.Forward.*,`MeetingForwardNotification`,Full,0.35.0,0.35.0,0.35.0 +IPM.Task.*,`Task`,Full,0.33.0,0.35.0,0.35.0 +IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046},`MeetingException`,Full,0.35.0,0.35.0,0.35.0 \ No newline at end of file diff --git a/docs/type_support.rst b/docs/type_support.rst new file mode 100644 index 00000000..6f6f15cd --- /dev/null +++ b/docs/type_support.rst @@ -0,0 +1,10 @@ +This page lists how much support a certain object has. There are three classifications: Open, meaning that the file can be opened but not saved at all, Partial, meaning that the file uses default saving characteristics and has not been fully implemented, and Full, meaning that the class has been completely written. A class with incomplete properties may still be listed as Full if the saving capabilities are done. In addition, the version for each milestone of a class is listed. + +The first column is the internal class type that is used to figure out the value for the second column, the extract-msg class that is used to handle it. If the Class Name column is blank, look to the next one down for details, as it shares a class. Message class types ending with `.*` must start with the string but may have anything after it. If there is no more specialized version later in the table, anything starting with that will be handled by the specified class. + +For things added before 0.29.0, 0.29.0 is listed as when they were added, as that is the oldest version officially supported. + +Saving before version 0.35.0 may work for some things, but is not considered complete. + +.. csv-table:: + :file: type-support.csv From d8f8fdc6e15698c149b5360d8ca09f6a7ec84a18 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 11 Nov 2023 11:24:19 -0800 Subject: [PATCH 02/17] Fix some of the documentation issues --- docs/_gen.py | 2 + ...act_msg.attachments.custom_att_handler.rst | 18 +-- docs/extract_msg.attachments.rst | 48 +++---- docs/extract_msg.constants.rst | 18 +-- docs/extract_msg.encoding.rst | 6 +- docs/extract_msg.msg_classes.rst | 120 +++++++++--------- docs/extract_msg.properties.rst | 18 +-- docs/extract_msg.rst | 36 +++--- docs/extract_msg.structures.rst | 108 ++++++++-------- docs/index.rst | 1 + docs/type-support.csv | 28 ++-- docs/type_support.rst | 3 + 12 files changed, 206 insertions(+), 200 deletions(-) diff --git a/docs/_gen.py b/docs/_gen.py index d7a8599a..4a40f9c0 100644 --- a/docs/_gen.py +++ b/docs/_gen.py @@ -97,6 +97,8 @@ def generateFile(name : str, package : Package): if package.modules: f.write('Submodules\n----------\n\n') for module in package.modules: + if module.endswith('.py'): + module = module[:-3] temp = module.replace('_', '\\_') + ' module' f.write(f'{temp}\n{"-" * len(temp)}\n\n') f.write(f'.. automodule:: {module}\n') diff --git a/docs/extract_msg.attachments.custom_att_handler.rst b/docs/extract_msg.attachments.custom_att_handler.rst index 5788b238..c878b2c8 100644 --- a/docs/extract_msg.attachments.custom_att_handler.rst +++ b/docs/extract_msg.attachments.custom_att_handler.rst @@ -4,26 +4,26 @@ extract\_msg.attachments.custom\_att\_handler package Submodules ---------- -extract\_msg.attachments.custom\_att\_handler.custom\_handler.py module ------------------------------------------------------------------------ +extract\_msg.attachments.custom\_att\_handler.custom\_handler module +-------------------------------------------------------------------- -.. automodule:: extract_msg.attachments.custom_att_handler.custom_handler.py +.. automodule:: extract_msg.attachments.custom_att_handler.custom_handler :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.custom\_att\_handler.lnk\_obj\_att.py module ---------------------------------------------------------------------- +extract\_msg.attachments.custom\_att\_handler.lnk\_obj\_att module +------------------------------------------------------------------ -.. automodule:: extract_msg.attachments.custom_att_handler.lnk_obj_att.py +.. automodule:: extract_msg.attachments.custom_att_handler.lnk_obj_att :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.custom\_att\_handler.outlook\_image\_dib.py module ---------------------------------------------------------------------------- +extract\_msg.attachments.custom\_att\_handler.outlook\_image\_dib module +------------------------------------------------------------------------ -.. automodule:: extract_msg.attachments.custom_att_handler.outlook_image_dib.py +.. automodule:: extract_msg.attachments.custom_att_handler.outlook_image_dib :members: :undoc-members: :show-inheritance: diff --git a/docs/extract_msg.attachments.rst b/docs/extract_msg.attachments.rst index d1f7fdae..5a83614c 100644 --- a/docs/extract_msg.attachments.rst +++ b/docs/extract_msg.attachments.rst @@ -12,66 +12,66 @@ Subpackages Submodules ---------- -extract\_msg.attachments.attachment.py module ---------------------------------------------- +extract\_msg.attachments.attachment module +------------------------------------------ -.. automodule:: extract_msg.attachments.attachment.py +.. automodule:: extract_msg.attachments.attachment :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.attachment\_base.py module ---------------------------------------------------- +extract\_msg.attachments.attachment\_base module +------------------------------------------------ -.. automodule:: extract_msg.attachments.attachment_base.py +.. automodule:: extract_msg.attachments.attachment_base :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.broken\_att.py module ----------------------------------------------- +extract\_msg.attachments.broken\_att module +------------------------------------------- -.. automodule:: extract_msg.attachments.broken_att.py +.. automodule:: extract_msg.attachments.broken_att :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.custom\_att.py module ----------------------------------------------- +extract\_msg.attachments.custom\_att module +------------------------------------------- -.. automodule:: extract_msg.attachments.custom_att.py +.. automodule:: extract_msg.attachments.custom_att :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.emb\_msg\_att.py module ------------------------------------------------- +extract\_msg.attachments.emb\_msg\_att module +--------------------------------------------- -.. automodule:: extract_msg.attachments.emb_msg_att.py +.. automodule:: extract_msg.attachments.emb_msg_att :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.signed\_att.py module ----------------------------------------------- +extract\_msg.attachments.signed\_att module +------------------------------------------- -.. automodule:: extract_msg.attachments.signed_att.py +.. automodule:: extract_msg.attachments.signed_att :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.unsupported\_att.py module ---------------------------------------------------- +extract\_msg.attachments.unsupported\_att module +------------------------------------------------ -.. automodule:: extract_msg.attachments.unsupported_att.py +.. automodule:: extract_msg.attachments.unsupported_att :members: :undoc-members: :show-inheritance: -extract\_msg.attachments.web\_att.py module -------------------------------------------- +extract\_msg.attachments.web\_att module +---------------------------------------- -.. automodule:: extract_msg.attachments.web_att.py +.. automodule:: extract_msg.attachments.web_att :members: :undoc-members: :show-inheritance: diff --git a/docs/extract_msg.constants.rst b/docs/extract_msg.constants.rst index b9fa05b1..98bcd76d 100644 --- a/docs/extract_msg.constants.rst +++ b/docs/extract_msg.constants.rst @@ -4,26 +4,26 @@ extract\_msg.constants package Submodules ---------- -extract\_msg.constants.ps.py module ------------------------------------ +extract\_msg.constants.ps module +-------------------------------- -.. automodule:: extract_msg.constants.ps.py +.. automodule:: extract_msg.constants.ps :members: :undoc-members: :show-inheritance: -extract\_msg.constants.re.py module ------------------------------------ +extract\_msg.constants.re module +-------------------------------- -.. automodule:: extract_msg.constants.re.py +.. automodule:: extract_msg.constants.re :members: :undoc-members: :show-inheritance: -extract\_msg.constants.st.py module ------------------------------------ +extract\_msg.constants.st module +-------------------------------- -.. automodule:: extract_msg.constants.st.py +.. automodule:: extract_msg.constants.st :members: :undoc-members: :show-inheritance: diff --git a/docs/extract_msg.encoding.rst b/docs/extract_msg.encoding.rst index 39b3c7a0..e30566d9 100644 --- a/docs/extract_msg.encoding.rst +++ b/docs/extract_msg.encoding.rst @@ -4,10 +4,10 @@ extract\_msg.encoding package Submodules ---------- -extract\_msg.encoding.utils.py module -------------------------------------- +extract\_msg.encoding.utils module +---------------------------------- -.. automodule:: extract_msg.encoding.utils.py +.. automodule:: extract_msg.encoding.utils :members: :undoc-members: :show-inheritance: diff --git a/docs/extract_msg.msg_classes.rst b/docs/extract_msg.msg_classes.rst index cc20b339..55d07899 100644 --- a/docs/extract_msg.msg_classes.rst +++ b/docs/extract_msg.msg_classes.rst @@ -4,162 +4,162 @@ extract\_msg.msg\_classes package Submodules ---------- -extract\_msg.msg\_classes.appointment.py module ------------------------------------------------ +extract\_msg.msg\_classes.appointment module +-------------------------------------------- -.. automodule:: extract_msg.msg_classes.appointment.py +.. automodule:: extract_msg.msg_classes.appointment :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.calendar.py module --------------------------------------------- +extract\_msg.msg\_classes.calendar module +----------------------------------------- -.. automodule:: extract_msg.msg_classes.calendar.py +.. automodule:: extract_msg.msg_classes.calendar :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.calendar\_base.py module --------------------------------------------------- +extract\_msg.msg\_classes.calendar\_base module +----------------------------------------------- -.. automodule:: extract_msg.msg_classes.calendar_base.py +.. automodule:: extract_msg.msg_classes.calendar_base :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.contact.py module -------------------------------------------- +extract\_msg.msg\_classes.contact module +---------------------------------------- -.. automodule:: extract_msg.msg_classes.contact.py +.. automodule:: extract_msg.msg_classes.contact :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.journal.py module -------------------------------------------- +extract\_msg.msg\_classes.journal module +---------------------------------------- -.. automodule:: extract_msg.msg_classes.journal.py +.. automodule:: extract_msg.msg_classes.journal :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.meeting\_cancellation.py module ---------------------------------------------------------- +extract\_msg.msg\_classes.meeting\_cancellation module +------------------------------------------------------ -.. automodule:: extract_msg.msg_classes.meeting_cancellation.py +.. automodule:: extract_msg.msg_classes.meeting_cancellation :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.meeting\_exception.py module ------------------------------------------------------- +extract\_msg.msg\_classes.meeting\_exception module +--------------------------------------------------- -.. automodule:: extract_msg.msg_classes.meeting_exception.py +.. automodule:: extract_msg.msg_classes.meeting_exception :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.meeting\_forward.py module ----------------------------------------------------- +extract\_msg.msg\_classes.meeting\_forward module +------------------------------------------------- -.. automodule:: extract_msg.msg_classes.meeting_forward.py +.. automodule:: extract_msg.msg_classes.meeting_forward :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.meeting\_related.py module ----------------------------------------------------- +extract\_msg.msg\_classes.meeting\_related module +------------------------------------------------- -.. automodule:: extract_msg.msg_classes.meeting_related.py +.. automodule:: extract_msg.msg_classes.meeting_related :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.meeting\_request.py module ----------------------------------------------------- +extract\_msg.msg\_classes.meeting\_request module +------------------------------------------------- -.. automodule:: extract_msg.msg_classes.meeting_request.py +.. automodule:: extract_msg.msg_classes.meeting_request :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.meeting\_response.py module ------------------------------------------------------ +extract\_msg.msg\_classes.meeting\_response module +-------------------------------------------------- -.. automodule:: extract_msg.msg_classes.meeting_response.py +.. automodule:: extract_msg.msg_classes.meeting_response :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.message.py module -------------------------------------------- +extract\_msg.msg\_classes.message module +---------------------------------------- -.. automodule:: extract_msg.msg_classes.message.py +.. automodule:: extract_msg.msg_classes.message :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.message\_base.py module -------------------------------------------------- +extract\_msg.msg\_classes.message\_base module +---------------------------------------------- -.. automodule:: extract_msg.msg_classes.message_base.py +.. automodule:: extract_msg.msg_classes.message_base :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.message\_signed.py module ---------------------------------------------------- +extract\_msg.msg\_classes.message\_signed module +------------------------------------------------ -.. automodule:: extract_msg.msg_classes.message_signed.py +.. automodule:: extract_msg.msg_classes.message_signed :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.message\_signed\_base.py module ---------------------------------------------------------- +extract\_msg.msg\_classes.message\_signed\_base module +------------------------------------------------------ -.. automodule:: extract_msg.msg_classes.message_signed_base.py +.. automodule:: extract_msg.msg_classes.message_signed_base :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.msg.py module ---------------------------------------- +extract\_msg.msg\_classes.msg module +------------------------------------ -.. automodule:: extract_msg.msg_classes.msg.py +.. automodule:: extract_msg.msg_classes.msg :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.post.py module ----------------------------------------- +extract\_msg.msg\_classes.post module +------------------------------------- -.. automodule:: extract_msg.msg_classes.post.py +.. automodule:: extract_msg.msg_classes.post :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.sticky\_note.py module ------------------------------------------------- +extract\_msg.msg\_classes.sticky\_note module +--------------------------------------------- -.. automodule:: extract_msg.msg_classes.sticky_note.py +.. automodule:: extract_msg.msg_classes.sticky_note :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.task.py module ----------------------------------------- +extract\_msg.msg\_classes.task module +------------------------------------- -.. automodule:: extract_msg.msg_classes.task.py +.. automodule:: extract_msg.msg_classes.task :members: :undoc-members: :show-inheritance: -extract\_msg.msg\_classes.task\_request.py module -------------------------------------------------- +extract\_msg.msg\_classes.task\_request module +---------------------------------------------- -.. automodule:: extract_msg.msg_classes.task_request.py +.. automodule:: extract_msg.msg_classes.task_request :members: :undoc-members: :show-inheritance: diff --git a/docs/extract_msg.properties.rst b/docs/extract_msg.properties.rst index cf0c16a1..ffbb9930 100644 --- a/docs/extract_msg.properties.rst +++ b/docs/extract_msg.properties.rst @@ -4,26 +4,26 @@ extract\_msg.properties package Submodules ---------- -extract\_msg.properties.named.py module ---------------------------------------- +extract\_msg.properties.named module +------------------------------------ -.. automodule:: extract_msg.properties.named.py +.. automodule:: extract_msg.properties.named :members: :undoc-members: :show-inheritance: -extract\_msg.properties.prop.py module --------------------------------------- +extract\_msg.properties.prop module +----------------------------------- -.. automodule:: extract_msg.properties.prop.py +.. automodule:: extract_msg.properties.prop :members: :undoc-members: :show-inheritance: -extract\_msg.properties.properties\_store.py module ---------------------------------------------------- +extract\_msg.properties.properties\_store module +------------------------------------------------ -.. automodule:: extract_msg.properties.properties_store.py +.. automodule:: extract_msg.properties.properties_store :members: :undoc-members: :show-inheritance: diff --git a/docs/extract_msg.rst b/docs/extract_msg.rst index ea00a260..c50939ee 100644 --- a/docs/extract_msg.rst +++ b/docs/extract_msg.rst @@ -17,50 +17,50 @@ Subpackages Submodules ---------- -extract\_msg.enums.py module ----------------------------- +extract\_msg.enums module +------------------------- -.. automodule:: extract_msg.enums.py +.. automodule:: extract_msg.enums :members: :undoc-members: :show-inheritance: -extract\_msg.exceptions.py module ---------------------------------- +extract\_msg.exceptions module +------------------------------ -.. automodule:: extract_msg.exceptions.py +.. automodule:: extract_msg.exceptions :members: :undoc-members: :show-inheritance: -extract\_msg.ole\_writer.py module ----------------------------------- +extract\_msg.ole\_writer module +------------------------------- -.. automodule:: extract_msg.ole_writer.py +.. automodule:: extract_msg.ole_writer :members: :undoc-members: :show-inheritance: -extract\_msg.open\_msg.py module --------------------------------- +extract\_msg.open\_msg module +----------------------------- -.. automodule:: extract_msg.open_msg.py +.. automodule:: extract_msg.open_msg :members: :undoc-members: :show-inheritance: -extract\_msg.recipient.py module --------------------------------- +extract\_msg.recipient module +----------------------------- -.. automodule:: extract_msg.recipient.py +.. automodule:: extract_msg.recipient :members: :undoc-members: :show-inheritance: -extract\_msg.utils.py module ----------------------------- +extract\_msg.utils module +------------------------- -.. automodule:: extract_msg.utils.py +.. automodule:: extract_msg.utils :members: :undoc-members: :show-inheritance: diff --git a/docs/extract_msg.structures.rst b/docs/extract_msg.structures.rst index 213e1928..86f44fcc 100644 --- a/docs/extract_msg.structures.rst +++ b/docs/extract_msg.structures.rst @@ -4,146 +4,146 @@ extract\_msg.structures package Submodules ---------- -extract\_msg.structures.business\_card.py module ------------------------------------------------- +extract\_msg.structures.business\_card module +--------------------------------------------- -.. automodule:: extract_msg.structures.business_card.py +.. automodule:: extract_msg.structures.business_card :members: :undoc-members: :show-inheritance: -extract\_msg.structures.cfoas.py module ---------------------------------------- +extract\_msg.structures.cfoas module +------------------------------------ -.. automodule:: extract_msg.structures.cfoas.py +.. automodule:: extract_msg.structures.cfoas :members: :undoc-members: :show-inheritance: -extract\_msg.structures.contact\_link\_entry.py module ------------------------------------------------------- +extract\_msg.structures.contact\_link\_entry module +--------------------------------------------------- -.. automodule:: extract_msg.structures.contact_link_entry.py +.. automodule:: extract_msg.structures.contact_link_entry :members: :undoc-members: :show-inheritance: -extract\_msg.structures.dev\_mode\_a.py module ----------------------------------------------- +extract\_msg.structures.dev\_mode\_a module +------------------------------------------- -.. automodule:: extract_msg.structures.dev_mode_a.py +.. automodule:: extract_msg.structures.dev_mode_a :members: :undoc-members: :show-inheritance: -extract\_msg.structures.dv\_target\_device.py module ----------------------------------------------------- +extract\_msg.structures.dv\_target\_device module +------------------------------------------------- -.. automodule:: extract_msg.structures.dv_target_device.py +.. automodule:: extract_msg.structures.dv_target_device :members: :undoc-members: :show-inheritance: -extract\_msg.structures.entry\_id.py module -------------------------------------------- +extract\_msg.structures.entry\_id module +---------------------------------------- -.. automodule:: extract_msg.structures.entry_id.py +.. automodule:: extract_msg.structures.entry_id :members: :undoc-members: :show-inheritance: -extract\_msg.structures.misc\_id.py module ------------------------------------------- +extract\_msg.structures.misc\_id module +--------------------------------------- -.. automodule:: extract_msg.structures.misc_id.py +.. automodule:: extract_msg.structures.misc_id :members: :undoc-members: :show-inheritance: -extract\_msg.structures.mon\_stream.py module ---------------------------------------------- +extract\_msg.structures.mon\_stream module +------------------------------------------ -.. automodule:: extract_msg.structures.mon_stream.py +.. automodule:: extract_msg.structures.mon_stream :members: :undoc-members: :show-inheritance: -extract\_msg.structures.odt.py module -------------------------------------- +extract\_msg.structures.odt module +---------------------------------- -.. automodule:: extract_msg.structures.odt.py +.. automodule:: extract_msg.structures.odt :members: :undoc-members: :show-inheritance: -extract\_msg.structures.ole\_pres.py module -------------------------------------------- +extract\_msg.structures.ole\_pres module +---------------------------------------- -.. automodule:: extract_msg.structures.ole_pres.py +.. automodule:: extract_msg.structures.ole_pres :members: :undoc-members: :show-inheritance: -extract\_msg.structures.ole\_stream\_struct.py module ------------------------------------------------------ +extract\_msg.structures.ole\_stream\_struct module +-------------------------------------------------- -.. automodule:: extract_msg.structures.ole_stream_struct.py +.. automodule:: extract_msg.structures.ole_stream_struct :members: :undoc-members: :show-inheritance: -extract\_msg.structures.recurrence\_pattern.py module ------------------------------------------------------ +extract\_msg.structures.recurrence\_pattern module +-------------------------------------------------- -.. automodule:: extract_msg.structures.recurrence_pattern.py +.. automodule:: extract_msg.structures.recurrence_pattern :members: :undoc-members: :show-inheritance: -extract\_msg.structures.report\_tag.py module ---------------------------------------------- +extract\_msg.structures.report\_tag module +------------------------------------------ -.. automodule:: extract_msg.structures.report_tag.py +.. automodule:: extract_msg.structures.report_tag :members: :undoc-members: :show-inheritance: -extract\_msg.structures.system\_time.py module ----------------------------------------------- +extract\_msg.structures.system\_time module +------------------------------------------- -.. automodule:: extract_msg.structures.system_time.py +.. automodule:: extract_msg.structures.system_time :members: :undoc-members: :show-inheritance: -extract\_msg.structures.time\_zone\_definition.py module --------------------------------------------------------- +extract\_msg.structures.time\_zone\_definition module +----------------------------------------------------- -.. automodule:: extract_msg.structures.time_zone_definition.py +.. automodule:: extract_msg.structures.time_zone_definition :members: :undoc-members: :show-inheritance: -extract\_msg.structures.time\_zone\_struct.py module ----------------------------------------------------- +extract\_msg.structures.time\_zone\_struct module +------------------------------------------------- -.. automodule:: extract_msg.structures.time_zone_struct.py +.. automodule:: extract_msg.structures.time_zone_struct :members: :undoc-members: :show-inheritance: -extract\_msg.structures.toc\_entry.py module --------------------------------------------- +extract\_msg.structures.toc\_entry module +----------------------------------------- -.. automodule:: extract_msg.structures.toc_entry.py +.. automodule:: extract_msg.structures.toc_entry :members: :undoc-members: :show-inheritance: -extract\_msg.structures.tz\_rule.py module ------------------------------------------- +extract\_msg.structures.tz\_rule module +--------------------------------------- -.. automodule:: extract_msg.structures.tz_rule.py +.. automodule:: extract_msg.structures.tz_rule :members: :undoc-members: :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index 039022ea..810356f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Welcome to extract-msg's documentation! :caption: Contents: modules + type_support Indices and tables ================== diff --git a/docs/type-support.csv b/docs/type-support.csv index 0a0eca09..bdb3eac3 100644 --- a/docs/type-support.csv +++ b/docs/type-support.csv @@ -1,17 +1,17 @@ -Message Class,Class Name,Support Level,Support Added,Saving Added,Saving Completed -IPM,`MSGFile`,Open,0.29.0,Won't Add, +**Message Class**,**Class Name**,**Support Level**,**Support Added**,**Saving Added**,**Saving Completed** +IPM,``MSGFile``,Open,0.29.0,Won't Add, REPORT.*,,,,, -IPM.Note.*,`Message`,Full,0.29.0,0.29.0,0.35.0 -IPM.Activity.*,`Journal`,Full,0.46.0,0.46.0,0.46.0 -IPM.Appointment.*,`AppointmentMeeting`(`Appointment` before 0.35.0),Full,0.29.0,0.35.0,0.35.0 +IPM.Note.*,``Message``,Full,0.29.0,0.29.0,0.35.0 +IPM.Activity.*,``Journal``,Full,0.46.0,0.46.0,0.46.0 +IPM.Appointment.*,``AppointmentMeeting`` (``Appointment`` before 0.35.0),Full,0.29.0,0.35.0,0.35.0 IPM.Contact.*,,,,, -IPM.Distlist.*,`Contact`,Full,0.29.0,0.35.0,0.35.0 +IPM.Distlist.*,``Contact``,Full,0.29.0,0.35.0,0.35.0 IPM.Note.*.SMIME,,,,, -IPM.Note.*.SMIME.MultipartSigned,`MessageSigned`,Full,0.31.0,0.31.0,0.35.0 -IPM.Post.*,`Post`,Full,0.35.0,0.35.0,0.35.0 -IPM.Schedule.Meeting.Cancelation.*,`MeetingCancellation`,Full,0.35.0,0.35.0,0.35.0 -IPM.Schedule.Meeting.Request.*,`MeetingRequest`,Full,0.35.0,0.35.0,0.35.0 -IPM.Schedule.Meeting.Resp.*,`MeetingResponse`,Full,0.35.0,0.35.0,0.35.0 -IPM.Schedule.Meeting.Notification.Forward.*,`MeetingForwardNotification`,Full,0.35.0,0.35.0,0.35.0 -IPM.Task.*,`Task`,Full,0.33.0,0.35.0,0.35.0 -IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046},`MeetingException`,Full,0.35.0,0.35.0,0.35.0 \ No newline at end of file +IPM.Note.*.SMIME.MultipartSigned,``MessageSigned``,Full,0.31.0,0.31.0,0.35.0 +IPM.Post.*,``Post``,Full,0.35.0,0.35.0,0.35.0 +IPM.Schedule.Meeting.Cancelation.*,``MeetingCancellation``,Full,0.35.0,0.35.0,0.35.0 +IPM.Schedule.Meeting.Request.*,``MeetingRequest``,Full,0.35.0,0.35.0,0.35.0 +IPM.Schedule.Meeting.Resp.*,``MeetingResponse``,Full,0.35.0,0.35.0,0.35.0 +IPM.Schedule.Meeting.Notification.Forward.*,``MeetingForwardNotification``,Full,0.35.0,0.35.0,0.35.0 +IPM.Task.*,``Task``,Full,0.33.0,0.35.0,0.35.0 +IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046},``MeetingException``,Full,0.35.0,0.35.0,0.35.0 \ No newline at end of file diff --git a/docs/type_support.rst b/docs/type_support.rst index 6f6f15cd..5647b2f9 100644 --- a/docs/type_support.rst +++ b/docs/type_support.rst @@ -1,3 +1,6 @@ +Type Support +============ + This page lists how much support a certain object has. There are three classifications: Open, meaning that the file can be opened but not saved at all, Partial, meaning that the file uses default saving characteristics and has not been fully implemented, and Full, meaning that the class has been completely written. A class with incomplete properties may still be listed as Full if the saving capabilities are done. In addition, the version for each milestone of a class is listed. The first column is the internal class type that is used to figure out the value for the second column, the extract-msg class that is used to handle it. If the Class Name column is blank, look to the next one down for details, as it shares a class. Message class types ending with `.*` must start with the string but may have anything after it. If there is no more specialized version later in the table, anything starting with that will be handled by the specified class. From 069e1140adde4d213478ae20c0d03e2562a0b5f7 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Fri, 1 Dec 2023 10:27:58 -0800 Subject: [PATCH 03/17] Update formatting to match 0.47.0 --- docs/_gen.py | 282 +++++++++++++++++++++++++-------------------------- 1 file changed, 141 insertions(+), 141 deletions(-) diff --git a/docs/_gen.py b/docs/_gen.py index 4a40f9c0..e9a58ce7 100644 --- a/docs/_gen.py +++ b/docs/_gen.py @@ -1,142 +1,142 @@ -""" -Helper script for generating the necessary RST files. -""" - -import os -import pathlib - -from typing import Dict, List, NamedTuple, Tuple - - -DIRECTORY = pathlib.Path(__file__).parent - - -class Package(NamedTuple): - """ - A class representing one of the subpackages of a module. - """ - modules : List[str] - packages : List[str] - - - -def _readProject(root) -> Dict[str, Dict[str, bool]]: - """ - Searches a project for Python files, using their locations to create a - dictionary of module paths and their submodules/subpackages. Submodules/subpackages will be a dictionary, where the key is the name and the - """ - # This whole function could almost certainly be optimized, but I'll worry - # about that some other time. - root = pathlib.Path(root) - rootName = root.name - ret = {rootName: {}} - for x in root.glob('**/*.py'): - # Ignore internal files. - if x.name.startswith('_'): - continue - - # Get all parent components. - parents = [] - parent = x.parent - while parent != root: - parents.append(parent.name) - parent = parent.parent - - # Check if any of the parents start with an underscore. If they do, - # ignore the current path. - if any(y.startswith('_') for y in parents): - continue - - parents.append(rootName) - - parents.reverse() - - # Add the subpackages and submodules. - for index, name in enumerate(parents[1:]): - path = '.'.join(parents[:index + 1]) - if path not in ret: - ret[path] = {} - if name not in ret[path]: - ret[path][name] = True - if (path := '.'.join(parents)) not in ret: - ret[path] = {} - ret[path][x.name] = False - - return ret - - -def _makePackage(name : str, data : Dict[str, bool]) -> Package: - return Package([f'{name}.{x}' for x in data if not data[x]], [f'{name}.{x}' for x in data if data[x]]) - - -def run(): - for x in getAutoGenerated(): - os.remove(DIRECTORY / x) - project = readProject(DIRECTORY.parent / 'extract_msg') - for x, y in project.items(): - generateFile(x, y) - - writeAutoGenerated((x + '.rst' for x in project)) - - -def generateFile(name : str, package : Package): - with open(DIRECTORY / (name + '.rst'), 'w') as f: - # Header. - temp = name.replace('_', '\\_') + ' package' - f.write(f'{temp}\n{"=" * len(temp)}\n\n') - - # Subpackages. - if package.packages: - f.write('Subpackages\n-----------\n\n') - f.write('.. toctree::\n') - f.write(' :maxdepth: 4\n\n') - f.write(' ' + '\n '.join(package.packages)) - f.write('\n\n') - - # Submodules. - if package.modules: - f.write('Submodules\n----------\n\n') - for module in package.modules: - if module.endswith('.py'): - module = module[:-3] - temp = module.replace('_', '\\_') + ' module' - f.write(f'{temp}\n{"-" * len(temp)}\n\n') - f.write(f'.. automodule:: {module}\n') - f.write(' :members:\n') - f.write(' :undoc-members:\n') - f.write(' :show-inheritance:\n\n') - - # Module contents. - f.write('Module contents\n---------------\n\n') - f.write(f'.. automodule:: {name}\n') - f.write(' :members:\n') - f.write(' :undoc-members:\n') - f.write(' :show-inheritance:\n') - - -def getAutoGenerated() -> List[str]: - """ - Retrieves the list of previously autogenerated files. - """ - with open(DIRECTORY / '_autogen.txt', 'r') as f: - return [x.strip() for x in f if x] - - -def readProject(root) -> Dict[str, Package]: - """ - Returns a dictionary of package names to Package instances for a project. - """ - initialRead = _readProject(root) - return {x : _makePackage(x, y) for x, y in initialRead.items()} - - -def writeAutoGenerated(files : List[str]) -> None: - """ - Writes the _autogen.txt file. - """ - with open(DIRECTORY / '_autogen.txt', 'w') as f: - f.write('\n'.join(files)) - - -if __name__ == '__main__': +""" +Helper script for generating the necessary RST files. +""" + +import os +import pathlib + +from typing import Dict, List, NamedTuple, Tuple + + +DIRECTORY = pathlib.Path(__file__).parent + + +class Package(NamedTuple): + """ + A class representing one of the subpackages of a module. + """ + modules: List[str] + packages: List[str] + + + +def _readProject(root) -> Dict[str, Dict[str, bool]]: + """ + Searches a project for Python files, using their locations to create a + dictionary of module paths and their submodules/subpackages. Submodules/subpackages will be a dictionary, where the key is the name and the + """ + # This whole function could almost certainly be optimized, but I'll worry + # about that some other time. + root = pathlib.Path(root) + rootName = root.name + ret = {rootName: {}} + for x in root.glob('**/*.py'): + # Ignore internal files. + if x.name.startswith('_'): + continue + + # Get all parent components. + parents = [] + parent = x.parent + while parent != root: + parents.append(parent.name) + parent = parent.parent + + # Check if any of the parents start with an underscore. If they do, + # ignore the current path. + if any(y.startswith('_') for y in parents): + continue + + parents.append(rootName) + + parents.reverse() + + # Add the subpackages and submodules. + for index, name in enumerate(parents[1:]): + path = '.'.join(parents[:index + 1]) + if path not in ret: + ret[path] = {} + if name not in ret[path]: + ret[path][name] = True + if (path := '.'.join(parents)) not in ret: + ret[path] = {} + ret[path][x.name] = False + + return ret + + +def _makePackage(name: str, data: Dict[str, bool]) -> Package: + return Package([f'{name}.{x}' for x in data if not data[x]], [f'{name}.{x}' for x in data if data[x]]) + + +def run(): + for x in getAutoGenerated(): + os.remove(DIRECTORY / x) + project = readProject(DIRECTORY.parent / 'extract_msg') + for x, y in project.items(): + generateFile(x, y) + + writeAutoGenerated((x + '.rst' for x in project)) + + +def generateFile(name: str, package: Package): + with open(DIRECTORY / (name + '.rst'), 'w') as f: + # Header. + temp = name.replace('_', '\\_') + ' package' + f.write(f'{temp}\n{"=" * len(temp)}\n\n') + + # Subpackages. + if package.packages: + f.write('Subpackages\n-----------\n\n') + f.write('.. toctree::\n') + f.write(' :maxdepth: 4\n\n') + f.write(' ' + '\n '.join(package.packages)) + f.write('\n\n') + + # Submodules. + if package.modules: + f.write('Submodules\n----------\n\n') + for module in package.modules: + if module.endswith('.py'): + module = module[:-3] + temp = module.replace('_', '\\_') + ' module' + f.write(f'{temp}\n{"-" * len(temp)}\n\n') + f.write(f'.. automodule:: {module}\n') + f.write(' :members:\n') + f.write(' :undoc-members:\n') + f.write(' :show-inheritance:\n\n') + + # Module contents. + f.write('Module contents\n---------------\n\n') + f.write(f'.. automodule:: {name}\n') + f.write(' :members:\n') + f.write(' :undoc-members:\n') + f.write(' :show-inheritance:\n') + + +def getAutoGenerated() -> List[str]: + """ + Retrieves the list of previously autogenerated files. + """ + with open(DIRECTORY / '_autogen.txt', 'r') as f: + return [x.strip() for x in f if x] + + +def readProject(root) -> Dict[str, Package]: + """ + Returns a dictionary of package names to Package instances for a project. + """ + initialRead = _readProject(root) + return {x : _makePackage(x, y) for x, y in initialRead.items()} + + +def writeAutoGenerated(files: List[str]) -> None: + """ + Writes the _autogen.txt file. + """ + with open(DIRECTORY / '_autogen.txt', 'w') as f: + f.write('\n'.join(files)) + + +if __name__ == '__main__': run() \ No newline at end of file From 5d220239ce3467df4168733e2b9aae2949cc36a1 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 9 Dec 2023 10:56:06 -0800 Subject: [PATCH 04/17] Undo change to allow merge --- docs/_gen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_gen.py b/docs/_gen.py index e9a58ce7..bf622a3e 100644 --- a/docs/_gen.py +++ b/docs/_gen.py @@ -15,8 +15,8 @@ class Package(NamedTuple): """ A class representing one of the subpackages of a module. """ - modules: List[str] - packages: List[str] + modules : List[str] + packages : List[str] From 17d0d3e3bbdb14754261477fd9525101cb59aa01 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 9 Dec 2023 11:01:17 -0800 Subject: [PATCH 05/17] Undo changes for merge --- docs/_gen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_gen.py b/docs/_gen.py index bf622a3e..79edc67f 100644 --- a/docs/_gen.py +++ b/docs/_gen.py @@ -65,7 +65,7 @@ def _readProject(root) -> Dict[str, Dict[str, bool]]: return ret -def _makePackage(name: str, data: Dict[str, bool]) -> Package: +def _makePackage(name : str, data : Dict[str, bool]) -> Package: return Package([f'{name}.{x}' for x in data if not data[x]], [f'{name}.{x}' for x in data if data[x]]) @@ -79,7 +79,7 @@ def run(): writeAutoGenerated((x + '.rst' for x in project)) -def generateFile(name: str, package: Package): +def generateFile(name : str, package : Package): with open(DIRECTORY / (name + '.rst'), 'w') as f: # Header. temp = name.replace('_', '\\_') + ' package' @@ -130,7 +130,7 @@ def readProject(root) -> Dict[str, Package]: return {x : _makePackage(x, y) for x, y in initialRead.items()} -def writeAutoGenerated(files: List[str]) -> None: +def writeAutoGenerated(files : List[str]) -> None: """ Writes the _autogen.txt file. """ From 22a94ff317d1249cea9172581577c92028cd9415 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 13 Dec 2023 05:38:37 -0800 Subject: [PATCH 06/17] Minor docstring updates --- CHANGELOG.md | 3 +++ README.rst | 4 ++-- extract_msg/__init__.py | 4 ++-- extract_msg/attachments/attachment_base.py | 13 +++++++------ 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc256c5b..8e7a12c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +**v0.47.1** +* Minor docstring updates. + **v0.47.0** * Changed the public API for `PropertiesStore` to improve the quality of its code. The properties type is now mandatory, and the intelligence field (and the related enum) has been removed. * Additionally, the `toBytes` and `__bytes__` methods will both generate based on the contents of this class, allowing for new properties to be created and for existing properties to be modified or removed if the class is set to writable on creation. diff --git a/README.rst b/README.rst index 9300e380..20439c3c 100644 --- a/README.rst +++ b/README.rst @@ -259,8 +259,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.47.0-blue.svg - :target: https://pypi.org/project/extract-msg/0.47.0/ +.. |PyPI3| image:: https://img.shields.io/badge/pypi-0.47.1-blue.svg + :target: https://pypi.org/project/extract-msg/0.47.1/ .. |PyPI2| image:: https://img.shields.io/badge/python-3.8+-brightgreen.svg :target: https://www.python.org/downloads/release/python-3810/ diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 4c4f8cb1..547bf46a 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -27,8 +27,8 @@ # along with this program. If not, see . __author__ = 'Destiny Peterson & Matthew Walker' -__date__ = '2023-12-09' -__version__ = '0.47.0' +__date__ = '2023-12-13' +__version__ = '0.47.1' __all__ = [ # Modules: diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index 9b63e701..f774c5cb 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -290,7 +290,7 @@ def getNamedAs(self, propertyName: str, guid: str, overrideClass: OVERRIDE_CLASS def getNamedProp(self, propertyName: str, guid: str, default: _T = None) -> Union[Any, _T]: """ - instance.namedProperties.get((propertyName, guid), default) + ``instance.namedProperties.get((propertyName, guid), default)`` Can be overriden to create new behavior. """ @@ -400,7 +400,7 @@ def getStringStream(self, filename: MSG_PATH) -> Optional[str]: the filename this function should receive should be "__substg1.0_001A". This should ALWAYS return a string if it was found, otherwise returns - None. + ``None``. :raises ReferenceError: The associated ``MSGFile`` instance has been garbage collected. @@ -415,10 +415,11 @@ def getStringStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T class if it is found. :param overrideClass: Class/function to use to morph the data that was - read. The data will be the first argument to the class's __init__ - function or the function itself, if that is what is provided. If - the value is None, this function is not called. If you want it to - be called regardless, you should handle the data directly. + read. The data will be the first argument to the class's + ``__init__`` method or the function itself, if that is what is + provided. If the value is ``None``, this function is not called. If + you want it to be called regardless, you should handle the data + directly. """ value = self.getStream(streamID) From 651fb852eeef72df7644ac3fb4febaa3535abc14 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 13 Dec 2023 05:42:28 -0800 Subject: [PATCH 07/17] More minor docstring updates --- extract_msg/attachments/attachment_base.py | 3 ++- extract_msg/utils.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index f774c5cb..2a685132 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -671,7 +671,8 @@ def renderingPosition(self) -> Optional[int]: The offset, in rendered characters, to use when rendering the attachment within the main message text. - A value of 0xFFFFFFFF indicates a hidden attachment that is not to be rendered. + A value of ``0xFFFFFFFF`` indicates a hidden attachment that is not to + be rendered. """ return self.getPropertyVal('370B0003') diff --git a/extract_msg/utils.py b/extract_msg/utils.py index 7a871778..f6a496af 100644 --- a/extract_msg/utils.py +++ b/extract_msg/utils.py @@ -129,11 +129,11 @@ def bitwiseAdjust(inp: int, mask: int) -> int: bitwise AND. This is useful for things like flags where you are trying to get a small - portion of a larger number. Say for example, you had the number 0xED - (0b11101101) and you needed the adjusted result of the AND operation with - 0x70 (0b01110000). The result of the AND operation (0b01100000) and the mask - used to get it (0x70) are given to this function and the adjustment will be - done automatically. + portion of a larger number. Say for example, you had the number ``0xED`` + (``0b11101101``) and you needed the adjusted result of the AND operation + with ``0x70`` (``0b01110000``). The result of the AND operation + (``0b01100000``) and the mask used to get it (``0x70``) are given to this + function and the adjustment will be done automatically. :param mask: MUST be greater than 0. @@ -147,7 +147,7 @@ def bitwiseAdjust(inp: int, mask: int) -> int: def bitwiseAdjustedAnd(inp: int, mask: int) -> int: """ Preforms the bitwise AND operation between :param inp: and :param mask: and - adjusts the results based on the rules of the bitwiseAdjust function. + adjusts the results based on the rules of :func:`bitwiseAdjust`. :raises ValueError: The mask is not greater than 0. """ @@ -166,19 +166,19 @@ def bytesToGuid(bytesInput: bytes) -> str: def ceilDiv(n: int, d: int) -> int: """ - Returns the int from the ceil division of n / d. + Returns the ``int`` from the ceiling division of n / d. - ONLY use ints as inputs to this function. + ONLY use ``int``\\s as inputs to this function. - For ints, this is faster and more accurate for numbers - outside the precision range of float. + For ``int``\\s, this is faster and more accurate for numbers outside the + precision range of ``float``. """ return -(n // -d) def cloneOleFile(sourcePath, outputPath) -> None: """ - Uses the OleWriter class to clone the specified OLE file into a new + Uses the ``OleWriter`` class to clone the specified OLE file into a new location. Mainly designed for testing. From 26c5fa6f2cca5e200c99c09fda46e6fa5f50f65d Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 13 Dec 2023 05:49:10 -0800 Subject: [PATCH 08/17] Minor docstring updates --- extract_msg/encoding/utils.py | 2 +- extract_msg/properties/properties_store.py | 2 +- extract_msg/structures/cfoas.py | 8 ++++---- extract_msg/structures/entry_id.py | 2 +- extract_msg/structures/ole_pres.py | 16 ++++++++-------- extract_msg/structures/ole_stream_struct.py | 4 ++-- extract_msg/structures/recurrence_pattern.py | 4 ++-- extract_msg/structures/report_tag.py | 6 +++--- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/extract_msg/encoding/utils.py b/extract_msg/encoding/utils.py index f08f2fd6..b326346f 100644 --- a/extract_msg/encoding/utils.py +++ b/extract_msg/encoding/utils.py @@ -202,7 +202,7 @@ def variableByteDecode(codecName: str, data, errors: str, decodeTable: Dict[int, """ Function for decoding variable-byte codecs that use one or two bytes per character. - Checks if a character is less than 0x80, mapping it directly if so. + Checks if a character is less than ``0x80``, mapping it directly if so. Otherwise, it reads the next byte and combines the two before looking up the new value. diff --git a/extract_msg/properties/properties_store.py b/extract_msg/properties/properties_store.py index 3d58bcdb..d1bbf3e4 100644 --- a/extract_msg/properties/properties_store.py +++ b/extract_msg/properties/properties_store.py @@ -157,7 +157,7 @@ def _mapId(self, id_: Union[int, str]) -> str: This is a complex function, allowing the user to specify an int or string. If the input is a string that is not 4 characters, it is returned. Otherwise, a series of checks will be - performed. If the input is an int that is less than 0x10000, it is + performed. If the input is an int that is less than ``0x10000``, it is considered a property ID without a type and converted to a 4 character hexadecimal string. Otherwise, it is converted to an 8 character hexadecimal string and returned. diff --git a/extract_msg/structures/cfoas.py b/extract_msg/structures/cfoas.py index cbb31dd5..c856dfef 100644 --- a/extract_msg/structures/cfoas.py +++ b/extract_msg/structures/cfoas.py @@ -47,8 +47,8 @@ def toBytes(self) -> bytes: def ansiString(self) -> Optional[bytes]: """ The null-terminated ANSI string, as bytes, of the name of a registered - clipboard format. Only set if markerOrLength is not 0x00000000, - 0xFFFFFFFE, or 0xFFFFFFFF. + clipboard format. Only set if markerOrLength is not ``0x00000000``, + ``0xFFFFFFFE``, or ``0xFFFFFFFF``. Setting this will modify the markerOrLength field automatically. """ @@ -66,8 +66,8 @@ def clipboardFormat(self) -> Optional[ClipboardFormat]: """ The clipboard format, if any. - To set this, make sure that :property markerOrLength: is 0xFFFFFFFE or - 0xFFFFFFFF *before* setting. + To set this, make sure that :property markerOrLength: is ``0xFFFFFFFE`` + or ``0xFFFFFFFF`` *before* setting. """ return self.__clipboardFormat diff --git a/extract_msg/structures/entry_id.py b/extract_msg/structures/entry_id.py index 303fb966..a51c720c 100644 --- a/extract_msg/structures/entry_id.py +++ b/extract_msg/structures/entry_id.py @@ -365,7 +365,7 @@ def __init__(self, data: bytes): @property def folderType(self) -> int: """ - The type of folder. MUST be 0x000C. + The type of folder. MUST be ``0x000C``. """ return self.__folderType diff --git a/extract_msg/structures/ole_pres.py b/extract_msg/structures/ole_pres.py index df959ff9..3045e6c7 100644 --- a/extract_msg/structures/ole_pres.py +++ b/extract_msg/structures/ole_pres.py @@ -209,7 +209,7 @@ def reserved1(self, val: bytes) -> None: def reserved2(self) -> Optional[bytes]: """ Optional additional data that is only set if the clipboard format of - :property ansiClipboardFormat: is CF_METAFILEPICT. + :property ansiClipboardFormat: is ``CF_METAFILEPICT``. Getting this will automatically correct the value retrieved based on the clipboard format, but will *not* modify the underlying data. @@ -246,9 +246,11 @@ def targetDevice(self, val: Optional[DVTargetDevice]) -> None: def tocEntries(self) -> List[TOCEntry]: """ A list of TOCEntry structures. If :property tocSignature: is not set to - 0x494E414E, accessing this value will clear the list. + ``0x494E414E``, accessing this value will clear the list. - :returns: A direct reference to the list, allowing for modification. This class WILL NOT change this reference over the lifetime of the object. + :returns: A direct reference to the list, allowing for modification. + This class WILL NOT change this reference over the lifetime of the + object. """ if self.__tocSignature != 0x494E414E: self.__tocEntries.clear() @@ -257,12 +259,10 @@ def tocEntries(self) -> List[TOCEntry]: @property def tocSignature(self) -> int: """ - If this field does not contain 0x494E414E, then :property tocEntries: - MUST be empty. Modifications to the list will be lost when it is next - retrieved, meaning changes while this property is not 0x494E414E WILL be - lost. + If this field does not contain ``0x494E414E``, then + :property tocEntries: MUST be empty. Modifications to the list will be lost when it is next retrieved, meaning changes while this property is not ``0x494E414E`` WILL be lost. - Setting this to a value other than 0x494E414E will clear the list + Setting this to a value other than ``0x494E414E`` will clear the list immediately. """ return self.__tocSignature diff --git a/extract_msg/structures/ole_stream_struct.py b/extract_msg/structures/ole_stream_struct.py index f3e6abb0..9a693bbc 100644 --- a/extract_msg/structures/ole_stream_struct.py +++ b/extract_msg/structures/ole_stream_struct.py @@ -59,8 +59,8 @@ def flags(self) -> int: """ The flags for the OLEStream. - The bit with mask 0x00001000 is an implementation-specific hint - supplied by the application or bby a higher-level protocol that creates + The bit with mask ``0x00001000`` is an implementation-specific hint + supplied by the application or by a higher-level protocol that creates the data structure. It MAY be ignored on processing. A server implementation which does not ignore this bit MAY cache the storage when the bit is set. diff --git a/extract_msg/structures/recurrence_pattern.py b/extract_msg/structures/recurrence_pattern.py index f98fc344..048f9d78 100644 --- a/extract_msg/structures/recurrence_pattern.py +++ b/extract_msg/structures/recurrence_pattern.py @@ -81,9 +81,9 @@ def endDate(self) -> int: The value is the number of minutes between midnight, January 1, 1601, and midnight of the date of the last occurrence. When the value of the - endType field is END_AFTER_N_OCCURRENCES, this value is calculated + endType field is ``END_AFTER_N_OCCURRENCES``, this value is calculated based on the number of occurrences. If the recurrence does not have an - end date, the value of the endDate field MUST be set to 0x5AE980DF. + end date, the value of the endDate field MUST be set to ``0x5AE980DF``. """ return self.__endDate diff --git a/extract_msg/structures/report_tag.py b/extract_msg/structures/report_tag.py index 99d43950..2b91001f 100644 --- a/extract_msg/structures/report_tag.py +++ b/extract_msg/structures/report_tag.py @@ -77,7 +77,7 @@ def cookie(self) -> bytes: """ String used for validation. - Set to b'PCDFEB09\x00'. + Set to ``b'PCDFEB09\x00'``. """ return self.__cookie @@ -121,7 +121,7 @@ def version(self) -> int: """ The version used. - If SearchFolderEntryID is present, this MUST be 0x00020001, otherwise - it MUST be 0x00010001. + If SearchFolderEntryID is present, this MUST be ``0x00020001``, + otherwise it MUST be ``0x00010001``. """ return self.__version From 2c730f7fa4f35fed336fdaa8b033352c46f71b6b Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sun, 17 Dec 2023 09:10:18 -0800 Subject: [PATCH 09/17] Doc updates --- extract_msg/__init__.py | 2 +- extract_msg/msg_classes/msg.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 547bf46a..4827697d 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -27,7 +27,7 @@ # along with this program. If not, see . __author__ = 'Destiny Peterson & Matthew Walker' -__date__ = '2023-12-13' +__date__ = '2023-12-17' __version__ = '0.47.1' __all__ = [ diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index e9b7e8b5..b193e067 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -83,8 +83,8 @@ def __init__(self, path, **kwargs): :param errorBehavior: Optional, the behavior to use in the event of certain types of errors. Uses the ``ErrorBehavior`` enum. :param overrideEncoding: Optional, an encoding to use instead of the one - specified by the MSG file. If the value is "chardet" and you have - the chardet module installed, an attempt will be made to + specified by the MSG file. If the value is ``"chardet"`` and you + have the ``chardet`` module installed, an attempt will be made to auto-detect the encoding based on some of the string properties. Do not report encoding errors caused by this. :param treePath: Internal variable used for giving representation of the From cc96b8a3e397ffdd63a20a1a9ce91b8141eeb317 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 23 Dec 2023 11:17:40 -0800 Subject: [PATCH 10/17] Updates to ole writer for version 4 ole file (correct branch) --- extract_msg/ole_writer.py | 95 ++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/extract_msg/ole_writer.py b/extract_msg/ole_writer.py index 94798baa..e81ee5cb 100644 --- a/extract_msg/ole_writer.py +++ b/extract_msg/ole_writer.py @@ -110,6 +110,9 @@ def __init__(self, rootClsid: bytes = constants.DEFAULT_CLSID): self.__largeEntrySectors = 0 self.__numMinifatSectors = 0 + # In a future version, this will be setable as an optional argument. + self.__version = 3 + def __getContainingStorage(self, path: List[str], entryExists: bool = True, create: bool = False) -> Dict: """ Finds the storage ``dict`` internally where the entry specified by @@ -262,7 +265,7 @@ def __recalculateSectors(self) -> None: self.__numMinifatSectors += ceilDiv(len(entry.data), 64) else: self.__largeEntries.append(entry) - self.__largeEntrySectors += ceilDiv(len(entry.data), 512) + self.__largeEntrySectors += ceilDiv(len(entry.data), self.__sectorSize) def __walkEntries(self) -> Iterator[DirectoryEntry]: """ @@ -282,8 +285,35 @@ def __walkEntries(self) -> Iterator[DirectoryEntry]: else: yield item + @property + def __dirEntsPerSector(self) -> int: + """ + The number of Directory Entries that can fit in a sector. + """ + return self.__sectorSize // 128 + + @property + def __linksPerSector(self) -> int: + """ + The number of links per FAT/DIFAT sector. + """ + self.__sectorSize // 4 + + @property + def __miniSectorsPerSector(self) -> int: + """ + The number of mini sectors that a regular sector will hold. + """ + return self.__sectorSize // 64 + @property def __numberOfSectors(self) -> int: + # Most of this should be pretty self evident, but line by line the + # calculation is as such: + # 1. How many sectors are needed for the directory entries. + # 2. How many FAT sectors are needed for the MiniStream. + # 3. How many sectors are needed for the MiniFat (ceil divide #2 by 16). + # 4. The number of FAT sectors needed to store the larger data. return ceilDiv(self.__dirEntryCount, 4) + \ self.__numMinifat + \ ceilDiv(self.__numMinifat, 16) + \ @@ -292,9 +322,17 @@ def __numberOfSectors(self) -> int: @property def __numMinifat(self) -> int: """ - The number of FAT sectors needed to store the mini FAT. + The number of FAT sectors needed to store the mini stream. """ - return ceilDiv(self.__numMinifatSectors, 8) + return ceilDiv(64 * self.__numMinifatSectors, self.__sectorSize) + + @property + def __sectorSize(self) -> int: + """ + The size of each sector, in bytes. + """ + # Specifically ignore that part of the code is unreachable. + return 512 is self.__version == 3 else 4096 # type: ignore def _cleanupEntries(self) -> None: """ @@ -319,12 +357,12 @@ def _getFatSectors(self) -> Tuple[int, int, int]: # Right now we just use an annoying while loop to get the numbers. numDifat = 0 # All divisions are ceiling divisions,. - numFat = ceilDiv(self.__numberOfSectors or 1, 127) + numFat = ceilDiv(self.__numberOfSectors or 1, self.__linksPerSector - 1) newNumFat = 1 while numFat != newNumFat: numFat = newNumFat - numDifat = ceilDiv(max(numFat - 109, 0), 127) - newNumFat = ceilDiv(self.__numberOfSectors + numDifat, 127) + numDifat = ceilDiv(max(numFat - 109, 0), self.__linksPerSector - 1) + newNumFat = ceilDiv(self.__numberOfSectors + numDifat, self.__linksPerSector - 1) return (numFat, numDifat, self.__numberOfSectors + numDifat + numFat) @@ -337,7 +375,7 @@ def _treeSort(self, startingSector: int) -> List[DirectoryEntry]: root = copy.copy(self.__rootEntry) # Add the location of the start of the mini stream. - root.startingSectorLocation = (startingSector + ceilDiv(self.__dirEntryCount, 4) + ceilDiv(self.__numMinifatSectors, 128)) if self.__numMinifat > 0 else 0xFFFFFFFE + root.startingSectorLocation = (startingSector + ceilDiv(self.__dirEntryCount, 4) + ceilDiv(self.__numMinifatSectors, self.__linksPerSector)) if self.__numMinifat > 0 else 0xFFFFFFFE root.streamSize = self.__numMinifatSectors * 64 root.childTreeRoot = None root.childID = 0xFFFFFFFF @@ -448,12 +486,13 @@ def _writeBeginning(self, f) -> int: f.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') # Minor version. f.write(b'\x3E\x00') - # Major version. For now, we only support version 3. - f.write(b'\x03\x00') + # Major version. For now, we only support version 3, but support for + # version 4 is planned. + f.write(b'\x03\x00' if self.__version == 3 else b'\x04\x00') # Byte order. Specifies that it is little endian. f.write(b'\xFE\xFF') # Sector shift. - f.write(b'\x09\x00') + f.write(b'\x09\x00' if self.__version == 3 else b'\x0C\x00') # Mini sector shift. f.write(b'\x06\x00') # Reserved. @@ -472,7 +511,7 @@ def _writeBeginning(self, f) -> int: # First mini FAT sector location. f.write(constants.st.ST_LE_UI32.pack((numFat + numDifat + ceilDiv(self.__dirEntryCount, 4)) if self.__numMinifat > 0 else 0xFFFFFFFE)) # Number of mini FAT sectors. - f.write(constants.st.ST_LE_UI32.pack(ceilDiv(self.__numMinifatSectors, 128))) + f.write(constants.st.ST_LE_UI32.pack(ceilDiv(self.__numMinifatSectors, self.__linksPerSector))) # First DIFAT sector location. If there are none, set to 0xFFFFFFFE (End # of chain). f.write(constants.st.ST_LE_UI32.pack(0 if numDifat else 0xFFFFFFFE)) @@ -485,16 +524,20 @@ def _writeBeginning(self, f) -> int: # Write the DIFAT sectors. for x in range(numFat): + # Quickly check if we have hit 109. If we have, and we are writing + # a version 4 file, we need to pad a bunch of null bytes. + if x == 109 and self.__version == 4: + f.write(b'\x00' * 3584) # This kind of sucks to code, ngl. - if x > 109 and (x - 109) % 127 == 0: + if x > 109 and (x - 109) % (self.__linksPerSector - 1) == 0: # If we are at the end of a DIFAT sector, write the jump. - f.write(constants.st.ST_LE_UI32.pack((x - 109) // 127)) + f.write(constants.st.ST_LE_UI32.pack((x - 109) // (self.__linksPerSector - 1))) # Write the next FAT sector location. f.write(constants.st.ST_LE_UI32.pack(x + numDifat)) # Finally, fill out the last DIFAT sector with null entries. if numFat > 109: - f.write(b'\xFF\xFF\xFF\xFF' * (127 - ((numFat - 109) % 127))) + f.write(b'\xFF\xFF\xFF\xFF' * ((self.__linksPerSector - 1) - ((numFat - 109) % (self.__linksPerSector - 1)))) # Finally, make sure to write the end of chain marker for the DIFAT. f.write(b'\xFE\xFF\xFF\xFF') else: @@ -511,13 +554,13 @@ def _writeBeginning(self, f) -> int: offset = numDifat + numFat # Fill in the values for the directory stream. - for x in range(offset + 1, offset + ceilDiv(self.__dirEntryCount, 4)): + for x in range(offset + 1, offset + ceilDiv(self.__dirEntryCount, self.__dirEntsPerSector)): f.write(constants.st.ST_LE_UI32.pack(x)) # Write the end of chain marker. f.write(b'\xFE\xFF\xFF\xFF') - offset += ceilDiv(self.__dirEntryCount, 4) + offset += ceilDiv(self.__dirEntryCount, self.__dirEntsPerSector) # Check if we have minifat *at all* first. if self.__numMinifatSectors > 0: @@ -544,7 +587,7 @@ def _writeBeginning(self, f) -> int: # to that list if the size was more than 4096. The order in the list is # how they will eventually be stored into the file correctly. for entry in self.__largeEntries: - size = ceilDiv(len(entry.data), 512) + size = ceilDiv(len(entry.data), self.__sectorSize) entry.startingSectorLocation = offset for x in range(offset + 1, offset + size): f.write(constants.st.ST_LE_UI32.pack(x)) @@ -555,9 +598,9 @@ def _writeBeginning(self, f) -> int: offset += size # Finally, fill fat with markers to specify no block exists. - freeSectors = totalSectors & 0x7F + freeSectors = totalSectors & (self.__linksPerSector - 1) if freeSectors: - f.write(b'\xFF\xFF\xFF\xFF' * (128 - freeSectors)) + f.write(b'\xFF\xFF\xFF\xFF' * (self.__linksPerSector - freeSectors)) # Finally, return the current sector index for use in other places. return numDifat + numFat @@ -587,8 +630,8 @@ def _writeFinal(self, f) -> None: """ for x in self.__largeEntries: f.write(x.data) - if len(x.data) & 511: - f.write(b'\x00' * (512 - (len(x.data) & 511))) + if len(x.data) & (self.__sectorSize - 1): + f.write(b'\x00' * (self.__sectorSize - (len(x.data) & (self.__sectorSize - 1)))) def _writeMini(self, f, entries: List[DirectoryEntry]) -> None: """ @@ -606,8 +649,8 @@ def _writeMini(self, f, entries: List[DirectoryEntry]) -> None: currentSector += size # Finally, write the remaining slots. - if currentSector & 127: - f.write(b'\xFF\xFF\xFF\xFF' * (128 - (currentSector & 127))) + if currentSector & (self.__linksPerSector - 1): + f.write(b'\xFF\xFF\xFF\xFF' * (self.__linksPerSector - (currentSector & (self.__linksPerSector - 1)))) # Write the mini stream. for x in entries: @@ -617,8 +660,8 @@ def _writeMini(self, f, entries: List[DirectoryEntry]) -> None: f.write(b'\x00' * (64 - (len(x.data) & 63))) # Pad the final mini stream block. - if self.__numMinifatSectors & 7: - f.write((b'\x00' * 64) * (8 - (self.__numMinifatSectors & 7))) + if self.__numMinifatSectors & (self.__miniSectorsPerSector - 1): + f.write((b'\x00' * 64) * (self.__miniSectorsPerSector - (self.__numMinifatSectors & (self.__miniSectorsPerSector - 1)))) def addEntry(self, path: MSG_PATH, data: Optional[Union[bytes, SupportsBytes]] = None, storage: bool = False, **kwargs) -> None: """ @@ -983,6 +1026,8 @@ def write(self, path) -> None: If :param path: has a ``write`` method, the object will be used directly. + If a failure occurs, the file or IO device may have been modified. + :raises TooManySectorsError: The number of sectors requires for a part of writing is too large. """ From 61b9dc5342d39d91c744cf6b6a26fa1c5e824692 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 23 Dec 2023 11:25:34 -0800 Subject: [PATCH 11/17] Fix mistakes in new OleWriter code --- CHANGELOG.md | 2 ++ extract_msg/ole_writer.py | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e7a12c6..72de28e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ **v0.47.1** * Minor docstring updates. +* Minor adjustments to `OleWriter` to prepare the code for being able to write version 4 files. Version 3 files are currently the only one's supported, but much of the code had hard-coded values that could be replaced with variables and small conditionals. This will have very little performance impact, and should not be noticeable. +* Improved comments on `OleWriter` to make private sections more understandable. **v0.47.0** * Changed the public API for `PropertiesStore` to improve the quality of its code. The properties type is now mandatory, and the intelligence field (and the related enum) has been removed. diff --git a/extract_msg/ole_writer.py b/extract_msg/ole_writer.py index e81ee5cb..1118d613 100644 --- a/extract_msg/ole_writer.py +++ b/extract_msg/ole_writer.py @@ -297,7 +297,7 @@ def __linksPerSector(self) -> int: """ The number of links per FAT/DIFAT sector. """ - self.__sectorSize // 4 + return self.__sectorSize // 4 @property def __miniSectorsPerSector(self) -> int: @@ -331,8 +331,7 @@ def __sectorSize(self) -> int: """ The size of each sector, in bytes. """ - # Specifically ignore that part of the code is unreachable. - return 512 is self.__version == 3 else 4096 # type: ignore + return 512 if self.__version == 3 else 4096 def _cleanupEntries(self) -> None: """ From 76bd94fbace196d41fd42b0c394cf6c1da6b418a Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Mon, 19 Feb 2024 08:47:08 -0800 Subject: [PATCH 12/17] Fix error handling of named properties. --- CHANGELOG.md | 3 ++- README.rst | 4 ++-- extract_msg/__init__.py | 4 ++-- extract_msg/properties/named.py | 32 +++++++++++++++++++++++++++----- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72de28e0..dbc5702b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ -**v0.47.1** +**v0.48.0** +* Adjusted error handling for named properties to handle critical streams being missing and to allow suppression of those errors. * Minor docstring updates. * Minor adjustments to `OleWriter` to prepare the code for being able to write version 4 files. Version 3 files are currently the only one's supported, but much of the code had hard-coded values that could be replaced with variables and small conditionals. This will have very little performance impact, and should not be noticeable. * Improved comments on `OleWriter` to make private sections more understandable. diff --git a/README.rst b/README.rst index 20439c3c..529dcf85 100644 --- a/README.rst +++ b/README.rst @@ -259,8 +259,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.47.1-blue.svg - :target: https://pypi.org/project/extract-msg/0.47.1/ +.. |PyPI3| image:: https://img.shields.io/badge/pypi-0.48.0-blue.svg + :target: https://pypi.org/project/extract-msg/0.48.0/ .. |PyPI2| image:: https://img.shields.io/badge/python-3.8+-brightgreen.svg :target: https://www.python.org/downloads/release/python-3810/ diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 4827697d..35cc8d41 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -27,8 +27,8 @@ # along with this program. If not, see . __author__ = 'Destiny Peterson & Matthew Walker' -__date__ = '2023-12-17' -__version__ = '0.47.1' +__date__ = '2024-02-19' +__version__ = '0.48.0' __all__ = [ # Modules: diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index 85d11fd8..9d96dad2 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -22,8 +22,8 @@ ) from .. import constants -from ..enums import NamedPropertyType -from ..exceptions import InvalidPropertyIdError +from ..enums import ErrorBehavior, NamedPropertyType +from ..exceptions import InvalidPropertyIdError, StandardViolationError from ..utils import bytesToGuid, divide, msgPathToString, verifyPropertyId from compressed_rtf.crc32 import crc32 @@ -49,16 +49,38 @@ class Named: def __init__(self, msg: MSGFile): self.__msg = weakref.ref(msg) # Get the basic streams. If all are emtpy, then nothing to do. - guidStream = self.getStream('__substg1.0_00020102') or b'' - entryStream = self.getStream('__substg1.0_00030102') or b'' + guidStream = self.getStream('__substg1.0_00020102') + entryStream = self.getStream('__substg1.0_00030102') self.guidStream = guidStream self.entryStream = entryStream - self.namesStream = self.getStream('__substg1.0_00040102') or b'' + self.namesStream = self.getStream('__substg1.0_00040102') self.__propertiesDict: Dict[Tuple[str, str], NamedPropertyBase] = {} self.__streamIDDict: Dict[str, Tuple[str, str]] = {} + if guidStream is None: + if ErrorBehavior.STANDARDS_VIOLATION in msg.errorBehavior: + logger.warning('Standards Violation: Guid stream missing from named properties.') + guidStream = b'' + else: + raise StandardViolationError('Guid stream missing from named properties.') + + if entryStream is None: + if ErrorBehavior.STANDARDS_VIOLATION in msg.errorBehavior: + logger.warning('Standards Violation: Entry stream missing from named properties.') + entryStream = b'' + else: + raise StandardViolationError('Entry stream missing from named properties.') + + if self.namesStream is None: + if ErrorBehavior.STANDARDS_VIOLATION in msg.errorBehavior: + logger.warning('Standards Violation: Guid stream missing from named properties. Will not parse named properties.') + # Return immediately since the entry stream will likely fail. + return + else: + raise StandardViolationError('Guid stream missing from named properties.') + # Check that we even have any entries. If there are none, nothing to do. if entryStream: guids = tuple([None, constants.ps.PS_MAPI, constants.ps.PS_PUBLIC_STRINGS] + [bytesToGuid(x) for x in divide(guidStream, 16)]) From 602393f6a8ef1583585d6000e7b62225ee4054dc Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Mon, 19 Feb 2024 10:58:27 -0800 Subject: [PATCH 13/17] Adjust error behavior of named properties --- CHANGELOG.md | 1 + extract_msg/enums.py | 7 ++++++- extract_msg/properties/named.py | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc5702b..f06b1d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ **v0.48.0** * Adjusted error handling for named properties to handle critical streams being missing and to allow suppression of those errors. +* Adjusted error handling for named properties to allow silencing of errors caused by invalid references to the name stream. If `ErrorBehavior.NAMED_NAME_STREAM` is provided to the `MSGFile` instance, a warning will be logged and that entry will simply be dropped. * Minor docstring updates. * Minor adjustments to `OleWriter` to prepare the code for being able to write version 4 files. Version 3 files are currently the only one's supported, but much of the code had hard-coded values that could be replaced with variables and small conditionals. This will have very little performance impact, and should not be noticeable. * Improved comments on `OleWriter` to make private sections more understandable. diff --git a/extract_msg/enums.py b/extract_msg/enums.py index f1c67bde..0f099942 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -674,6 +674,9 @@ class ErrorBehavior(enum.IntFlag): * STANDARDS_VIOLATION: Silences StandardViolationError where acceptable. * OLE_DEFECT_INCORRECT: Silences defects of type DEFECT_INCORRECT that are enabled by default. This can lead to strange bugs. + * NAMED_NAME_STREAM: Silences errors caused by invalid indexes into the + name stream for named properties. Any properties with an invalid name will + simply be dropped. * SUPPRESS_ALL: Silences all of the above. """ THROW = 0b000000 @@ -688,8 +691,10 @@ class ErrorBehavior(enum.IntFlag): # General. STANDARDS_VIOLATION = 0b010000 OLE_DEFECT_INCORRECT = 0b100000 + # Named Properties + NAMED_NAME_STREAM = 0b1000000 - SUPPRESS_ALL = 0b111111 + SUPPRESS_ALL = 0b1111111 diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index 9d96dad2..8603b0ec 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -100,7 +100,20 @@ def __init__(self, msg: MSGFile): properties: List[NamedPropertyBase] = [] for entry in entries: - properties.append(StringNamedProperty(entry, self.__getName(entry['id'])) if entry['pkind'] == NamedPropertyType.STRING_NAMED else NumericalNamedProperty(entry)) + if entry['pkind'] == NamedPropertyType.STRING_NAMED: + name = None + try: + name = self.__getName(entry['id']) + except ValueError as e: + if ErrorBehavior.NAMED_NAME_STREAM in msg.errorBehavior: + logger.warning(f'Dropping named property because it failed to acquire name from name stream: {e}') + else: + raise + + if name: + properties.append(StringNamedProperty(entry, name)) + else: + properties.append(NumericalNamedProperty(entry)) for property in properties: id_ = property.identifier From 95f79bc6a006c6f50c7f2dca6f3ee3d0e58c2097 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 21 Feb 2024 10:19:12 -0800 Subject: [PATCH 14/17] Changed warning logs to error for standard violation --- extract_msg/properties/named.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index 8603b0ec..1a1ebd85 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -75,7 +75,7 @@ def __init__(self, msg: MSGFile): if self.namesStream is None: if ErrorBehavior.STANDARDS_VIOLATION in msg.errorBehavior: - logger.warning('Standards Violation: Guid stream missing from named properties. Will not parse named properties.') + logger.error('Standards Violation: Guid stream missing from named properties. Will not parse named properties.') # Return immediately since the entry stream will likely fail. return else: @@ -106,7 +106,7 @@ def __init__(self, msg: MSGFile): name = self.__getName(entry['id']) except ValueError as e: if ErrorBehavior.NAMED_NAME_STREAM in msg.errorBehavior: - logger.warning(f'Dropping named property because it failed to acquire name from name stream: {e}') + logger.error(f'Dropping named property because it failed to acquire name from name stream: {e}') else: raise From 3ac96a5ad0bf8a89913e44210643dc0c9dc03346 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 21 Feb 2024 10:43:17 -0800 Subject: [PATCH 15/17] Update behavior of error handling for signed --- CHANGELOG.md | 2 + extract_msg/__init__.py | 2 +- .../msg_classes/message_signed_base.py | 49 +++++++++++++++---- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f06b1d6e..73b7a652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ **v0.48.0** * Adjusted error handling for named properties to handle critical streams being missing and to allow suppression of those errors. * Adjusted error handling for named properties to allow silencing of errors caused by invalid references to the name stream. If `ErrorBehavior.NAMED_NAME_STREAM` is provided to the `MSGFile` instance, a warning will be logged and that entry will simply be dropped. +* Adjusted error handling for signed messages to better check for issues with the signed attachment. This should make errors from violating the standard much easier to understand. These errors can be ignored, but the attachment will *not* be parsed as a signed attachment. * Minor docstring updates. * Minor adjustments to `OleWriter` to prepare the code for being able to write version 4 files. Version 3 files are currently the only one's supported, but much of the code had hard-coded values that could be replaced with variables and small conditionals. This will have very little performance impact, and should not be noticeable. * Improved comments on `OleWriter` to make private sections more understandable. +* Changed `MessageSignedBase._rawAttachments` to `MessageSignedBase.rawAttachments` to provide non-private access in a reliable way. **v0.47.0** * Changed the public API for `PropertiesStore` to improve the quality of its code. The properties type is now mandatory, and the intelligence field (and the related enum) has been removed. diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 35cc8d41..442411a6 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -27,7 +27,7 @@ # along with this program. If not, see . __author__ = 'Destiny Peterson & Matthew Walker' -__date__ = '2024-02-19' +__date__ = '2024-02-21' __version__ = '0.48.0' __all__ = [ diff --git a/extract_msg/msg_classes/message_signed_base.py b/extract_msg/msg_classes/message_signed_base.py index ba6d034c..dc9d6e7e 100644 --- a/extract_msg/msg_classes/message_signed_base.py +++ b/extract_msg/msg_classes/message_signed_base.py @@ -11,7 +11,7 @@ from typing import Generic, List, Optional, Type, TypeVar from ..attachments import AttachmentBase, SignedAttachment -from ..enums import DeencapType, ErrorBehavior +from ..enums import AttachmentType, DeencapType, ErrorBehavior from ..exceptions import StandardViolationError from .message_base import MessageBase from ..utils import inputToBytes, inputToString, unwrapMultipart @@ -47,21 +47,52 @@ def attachments(self) -> List[_T]: :raises StandardViolationError: The standard for signed messages was blatantly violated. """ - atts = self._rawAttachments + # Set these to None to make error handling easier. + self._signedBody = None + self._signedHtmlBody = None + + atts = self.rawAttachments if len(atts) != 1: if ErrorBehavior.STANDARDS_VIOLATION in self.errorBehavior: if len(atts) == 0: - logger.error('Signed message has no attachments, a violation of the standard.') - self._sAttachments = [] - self._signedBody = None - self._signedHtmlBody = None + logger.error('Standards Violation: Signed message has no attachments.') return [] - # If there is at least one attachment, just try to use the - # first. + # If there is at least one attachment, log an error about it + # and then try to use the first one. + logger.error('Standards Violation: Signed message has more than one attachment. Attempting to use first one.') else: raise StandardViolationError('Signed messages without exactly 1 (regular) attachment constitute a violation of the standard.') + # If we are here, validate that the found attachment is acceptable. + if atts[0].type is not AttachmentType.DATA: + # This is unacceptable and the attachment cannot be used. + if ErrorBehavior.STANDARDS_VIOLATION in self.errorBehavior: + logger.error('Standards Violation: Attachment on signed message is unacceptable (not binary).') + return [] + raise StandardViolationError('Signed messages *must* have a binary attachment. Signed attachments cannot be parsed otherwise.') + + # Check the mimetype *directly*. Detection of mimetype through data is + # unacceptable for this check. + temp = atts[0].getStringStream('__substg1.0_370E') + if temp != 'multipart/signed': + # While this *is* a violation, we are considering it a warning, + # however it will give context if an error occurs later on about + # the data being messed up. + logger.warning(f'Standards Violation: Signed attachment *must* have mimetype set to "multipart/signed" (got "{temp or ""}").') + # We won't do much analyzing here, but we want to see if the + # mimetype module, should it be present, can at least detect the + # data as being multipart. If it is and the next parts fail, then + # something weird likely happened, otherwise we have a good + # indication of what actually happened. + if not temp: + if atts[0].mimetype: + # No mimetype listed, but mimetype was detected. Check if it + # at least *starts* with "multipart/" and call it good + # enough if it does. + if not atts[0].mimetype.startswith('multipart/'): + logger.warning(f'Standards Violation: Signed attachment had not set mimetype, and detected mimetype is not multipart (got {atts[0].mimetype}).') + # We need to unwrap the multipart stream. unwrapped = unwrapMultipart(atts[0].data) @@ -117,7 +148,7 @@ def htmlBody(self) -> Optional[bytes]: return htmlBody @functools.cached_property - def _rawAttachments(self) -> List[AttachmentBase]: + def rawAttachments(self) -> List[AttachmentBase]: """ A property to allow access to the non-signed attachments. """ From 4db291f82e40a383ece609667cbce3b731d8d9cb Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 21 Feb 2024 11:22:11 -0800 Subject: [PATCH 16/17] Removed bad error handling code --- .../msg_classes/message_signed_base.py | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/extract_msg/msg_classes/message_signed_base.py b/extract_msg/msg_classes/message_signed_base.py index dc9d6e7e..487b54e9 100644 --- a/extract_msg/msg_classes/message_signed_base.py +++ b/extract_msg/msg_classes/message_signed_base.py @@ -74,24 +74,14 @@ def attachments(self) -> List[_T]: # Check the mimetype *directly*. Detection of mimetype through data is # unacceptable for this check. - temp = atts[0].getStringStream('__substg1.0_370E') - if temp != 'multipart/signed': - # While this *is* a violation, we are considering it a warning, - # however it will give context if an error occurs later on about - # the data being messed up. - logger.warning(f'Standards Violation: Signed attachment *must* have mimetype set to "multipart/signed" (got "{temp or ""}").') - # We won't do much analyzing here, but we want to see if the - # mimetype module, should it be present, can at least detect the - # data as being multipart. If it is and the next parts fail, then - # something weird likely happened, otherwise we have a good - # indication of what actually happened. - if not temp: - if atts[0].mimetype: - # No mimetype listed, but mimetype was detected. Check if it - # at least *starts* with "multipart/" and call it good - # enough if it does. - if not atts[0].mimetype.startswith('multipart/'): - logger.warning(f'Standards Violation: Signed attachment had not set mimetype, and detected mimetype is not multipart (got {atts[0].mimetype}).') + #temp = atts[0].getStringStream('__substg1.0_370E') + #if temp == 'multipart/signed': + ## Do nothing, but this is how we detect that it is clear signed. + #pass + #else: + ## Do nothing, but this is how we detect that it is *not* clear + # signed. + #pass # We need to unwrap the multipart stream. unwrapped = unwrapMultipart(atts[0].data) From b236c77a1a61ceab95ee8710ef899f500876e277 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 28 Feb 2024 08:03:08 -0800 Subject: [PATCH 17/17] Confirmed all tests passed --- extract_msg/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 442411a6..6d5090f1 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -27,7 +27,7 @@ # along with this program. If not, see . __author__ = 'Destiny Peterson & Matthew Walker' -__date__ = '2024-02-21' +__date__ = '2024-02-28' __version__ = '0.48.0' __all__ = [