initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
25
README.md
Normal file
25
README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
Convert Outlook .msg Files to .eml (MIME format)
|
||||
================================================
|
||||
|
||||
This repository contains a Python 3.6 module for
|
||||
reading Microsoft Outlook .msg files and converting
|
||||
them to .eml format, which is the standard MIME
|
||||
format for email messages.
|
||||
|
||||
The module requires Python 3.6 and the [compoundfiles](https://pypi.python.org/pypi/compoundfiles)
|
||||
package, so first install that:
|
||||
|
||||
pip3.6 install compoundfiles
|
||||
|
||||
We also rely on our Python 3 port of [compressed_rtf](https://github.com/delimitry/compressed_rtf), which is included in this repository.
|
||||
|
||||
Then either convert a single file by piping:
|
||||
|
||||
python3.6 outlookmsgfile.py < message.msg > message.eml
|
||||
|
||||
Or convert a set of files:
|
||||
|
||||
python3.6 outlookmsgfile.py *.msg
|
||||
|
||||
When passing filenames as command-line arguments, a new file with `.eml`
|
||||
appended to the filename is written out with the message in MIME format.
|
||||
224
compressed_rtf.py
Normal file
224
compressed_rtf.py
Normal file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python
|
||||
#-*- coding: utf8 -*-
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# This is a Python 3 port of compressed_rtf at https://github.com/delimitry/compressed_rtf,
|
||||
# which is MIT licensed.
|
||||
# -----------------------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
Compressed Rich Text Format (RTF) worker
|
||||
|
||||
Based on Rich Text Format (RTF) Compression Algorithm
|
||||
https://msdn.microsoft.com/en-us/library/cc463890(v=exchg.80).aspx
|
||||
"""
|
||||
|
||||
import struct
|
||||
from crc32 import crc32
|
||||
from io import BytesIO
|
||||
|
||||
__all__ = ['decompress', 'compress']
|
||||
|
||||
INIT_DICT = (
|
||||
b'{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\'
|
||||
b'fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New '
|
||||
b'RomanCourier{\\colortbl\\red0\\green0\\blue0\r\n\\par \\pard\\plain\\'
|
||||
b'f0\\fs20\\b\\i\\u\\tab\\tx'
|
||||
)
|
||||
|
||||
INIT_DICT_SIZE = 207
|
||||
MAX_DICT_SIZE = 4096
|
||||
|
||||
COMPRESSED = b'LZFu'
|
||||
UNCOMPRESSED = b'MELA'
|
||||
|
||||
|
||||
def decompress(data):
|
||||
"""
|
||||
Decompress data
|
||||
"""
|
||||
# set init dict
|
||||
init_dict = list(INIT_DICT)
|
||||
init_dict += ' ' * (MAX_DICT_SIZE - INIT_DICT_SIZE)
|
||||
if len(data) < 16:
|
||||
raise Exception('Data must be at least 16 bytes long')
|
||||
write_offset = INIT_DICT_SIZE
|
||||
output_buffer = BytesIO()
|
||||
# make stream
|
||||
in_stream = BytesIO(data)
|
||||
# read compressed RTF header
|
||||
comp_size = struct.unpack('<I', in_stream.read(4))[0]
|
||||
raw_size = struct.unpack('<I', in_stream.read(4))[0]
|
||||
comp_type = in_stream.read(4)
|
||||
crc_value = struct.unpack('<I', in_stream.read(4))[0]
|
||||
# get only data
|
||||
contents = BytesIO(in_stream.read(comp_size - 12))
|
||||
if comp_type == COMPRESSED:
|
||||
# check CRC
|
||||
if crc_value != crc32(contents.read()):
|
||||
raise Exception('CRC is invalid! The file is corrupt!')
|
||||
contents.seek(0)
|
||||
end = False
|
||||
while not end:
|
||||
val = contents.read(1)
|
||||
if not val:
|
||||
break
|
||||
control = '{0:08b}'.format(ord(val))
|
||||
# check bits from LSB to MSB
|
||||
for i in range(1, 9):
|
||||
if control[-i] == '1':
|
||||
# token is reference (16 bit)
|
||||
val = contents.read(2)
|
||||
if not val:
|
||||
break
|
||||
token = struct.unpack('>H', val)[0] # big-endian
|
||||
# extract [12 bit offset][4 bit length]
|
||||
offset = (token >> 4) & 0b111111111111
|
||||
length = token & 0b1111
|
||||
# end indicator
|
||||
if write_offset == offset:
|
||||
end = True
|
||||
break
|
||||
actual_length = length + 2
|
||||
for step in range(actual_length):
|
||||
read_offset = (offset + step) % MAX_DICT_SIZE
|
||||
char = init_dict[read_offset]
|
||||
output_buffer.write(bytes([char]))
|
||||
init_dict[write_offset] = char
|
||||
write_offset = (write_offset + 1) % MAX_DICT_SIZE
|
||||
else:
|
||||
# token is literal (8 bit)
|
||||
val = contents.read(1)
|
||||
if not val:
|
||||
break
|
||||
output_buffer.write(val)
|
||||
init_dict[write_offset] = val[0]
|
||||
write_offset = (write_offset + 1) % MAX_DICT_SIZE
|
||||
elif comp_type == UNCOMPRESSED:
|
||||
return contents.read(raw_size)
|
||||
else:
|
||||
raise Exception('Unknown type of RTF compression!')
|
||||
return output_buffer.getvalue()
|
||||
|
||||
|
||||
def compress(data, compressed=True):
|
||||
"""
|
||||
Compress `data` with `compressed` flag
|
||||
"""
|
||||
output_buffer = ''
|
||||
# set init dict
|
||||
init_dict = list(INIT_DICT + ' ' * (MAX_DICT_SIZE - INIT_DICT_SIZE))
|
||||
write_offset = INIT_DICT_SIZE
|
||||
# compressed
|
||||
if compressed:
|
||||
comp_type = COMPRESSED
|
||||
# make stream
|
||||
in_stream = BytesIO(data)
|
||||
# init params
|
||||
control_byte = 0
|
||||
control_bit = 1
|
||||
token_offset = 0
|
||||
token_buffer = ''
|
||||
match_len = 0
|
||||
longest_match = 0
|
||||
while True:
|
||||
# find longest match
|
||||
dict_offset, longest_match, write_offset = \
|
||||
_find_longest_match(init_dict, in_stream, write_offset)
|
||||
char = in_stream.read(longest_match if longest_match > 1 else 1)
|
||||
# EOF input stream
|
||||
if not char:
|
||||
# update params
|
||||
control_byte |= 1 << control_bit - 1
|
||||
control_bit += 1
|
||||
token_offset += 2
|
||||
# add dict reference
|
||||
dict_ref = (write_offset & 0xfff) << 4
|
||||
token_buffer += struct.pack('>H', dict_ref)
|
||||
# add to output
|
||||
output_buffer += struct.pack('B', control_byte)
|
||||
output_buffer += token_buffer[:token_offset]
|
||||
break
|
||||
else:
|
||||
if longest_match > 1:
|
||||
# update params
|
||||
control_byte |= 1 << control_bit - 1
|
||||
control_bit += 1
|
||||
token_offset += 2
|
||||
# add dict reference
|
||||
dict_ref = (dict_offset & 0xfff) << 4 | (
|
||||
longest_match - 2) & 0xf
|
||||
token_buffer += struct.pack('>H', dict_ref)
|
||||
else:
|
||||
# character is not found in dictionary
|
||||
if longest_match == 0:
|
||||
init_dict[write_offset] = char
|
||||
write_offset = (write_offset + 1) % MAX_DICT_SIZE
|
||||
# update params
|
||||
control_byte |= 0 << control_bit - 1
|
||||
control_bit += 1
|
||||
token_offset += 1
|
||||
# add literal
|
||||
token_buffer += char
|
||||
longest_match = 0
|
||||
if control_bit > 8:
|
||||
# add to output
|
||||
output_buffer += struct.pack('B', control_byte)
|
||||
output_buffer += token_buffer[:token_offset]
|
||||
# reset params
|
||||
control_byte = 0
|
||||
control_bit = 1
|
||||
token_offset = 0
|
||||
token_buffer = ''
|
||||
else:
|
||||
# if uncompressed - copy data to output
|
||||
comp_type = UNCOMPRESSED
|
||||
output_buffer = data
|
||||
# write compressed RTF header
|
||||
comp_size = struct.pack('<I', len(output_buffer) + 12)
|
||||
raw_size = struct.pack('<I', len(data))
|
||||
crc_value = struct.pack('<I', crc32(output_buffer))
|
||||
return comp_size + raw_size + comp_type + crc_value + output_buffer
|
||||
|
||||
|
||||
def _find_longest_match(init_dict, stream, write_offset):
|
||||
"""
|
||||
Find the longest match
|
||||
"""
|
||||
# read the first char
|
||||
char = stream.read(1)
|
||||
if not char:
|
||||
return 0, 0, write_offset
|
||||
prev_write_offset = write_offset
|
||||
dict_index = 0
|
||||
match_len = 0
|
||||
longest_match_len = 0
|
||||
dict_offset = 0
|
||||
# find the first char
|
||||
while True:
|
||||
if init_dict[dict_index % MAX_DICT_SIZE] == char:
|
||||
match_len += 1
|
||||
# if found longest match
|
||||
if match_len <= 17 and match_len > longest_match_len:
|
||||
dict_offset = dict_index - match_len + 1
|
||||
# add to dictionary and update longest match
|
||||
init_dict[write_offset] = char
|
||||
write_offset = (write_offset + 1) % MAX_DICT_SIZE
|
||||
longest_match_len = match_len
|
||||
# read the next char
|
||||
char = stream.read(1)
|
||||
if not char:
|
||||
stream.seek(stream.tell() - match_len, 0)
|
||||
return dict_offset, longest_match_len, write_offset
|
||||
else:
|
||||
stream.seek(stream.tell() - match_len - 1, 0)
|
||||
match_len = 0
|
||||
# read the first char
|
||||
char = stream.read(1)
|
||||
if not char:
|
||||
break
|
||||
dict_index += 1
|
||||
if dict_index >= prev_write_offset + longest_match_len:
|
||||
break
|
||||
stream.seek(stream.tell() - match_len - 1, 0)
|
||||
return dict_offset, longest_match_len, write_offset
|
||||
98
crc32.py
Normal file
98
crc32.py
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python
|
||||
#-*- coding: utf8 -*-
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# This is a Python 3 port of compressed_rtf at https://github.com/delimitry/compressed_rtf,
|
||||
# which is MIT licensed.
|
||||
# -----------------------------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
Module for CRC32 calculation
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
__all__ = ['crc32']
|
||||
|
||||
table = [
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
|
||||
0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
|
||||
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
|
||||
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
|
||||
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
|
||||
0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
|
||||
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
|
||||
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
||||
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
|
||||
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
|
||||
0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
|
||||
0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
|
||||
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
|
||||
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
|
||||
0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
|
||||
0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
|
||||
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
||||
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
|
||||
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
|
||||
0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
|
||||
0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
|
||||
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
||||
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
|
||||
0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
|
||||
0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
|
||||
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
|
||||
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
|
||||
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
|
||||
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
|
||||
0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
|
||||
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
|
||||
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
|
||||
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
|
||||
0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
|
||||
0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
|
||||
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
||||
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
|
||||
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
|
||||
0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
|
||||
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
|
||||
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
|
||||
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
|
||||
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
|
||||
0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
|
||||
0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
|
||||
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
|
||||
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
|
||||
0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
|
||||
0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
|
||||
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||
]
|
||||
|
||||
|
||||
def crc32(data):
|
||||
"""
|
||||
Calculate CRC32 from given data bytes
|
||||
"""
|
||||
stream = BytesIO(data)
|
||||
crc_value = 0x00000000
|
||||
while True:
|
||||
char = stream.read(1)
|
||||
if not char:
|
||||
break
|
||||
table_pos = (crc_value ^ ord(char)) & 0xff
|
||||
intermediate_value = crc_value >> 8
|
||||
crc_value = table[table_pos] ^ intermediate_value
|
||||
return crc_value
|
||||
810
outlookmsgfile.py
Normal file
810
outlookmsgfile.py
Normal file
@@ -0,0 +1,810 @@
|
||||
# This module converts a Microsoft Outlook .msg file into
|
||||
# a MIME message that can be loaded by most email programs
|
||||
# or inspected in a text editor.
|
||||
#
|
||||
# This script relies on the Python package compoundfiles
|
||||
# for reading the .msg container format.
|
||||
#
|
||||
# Referencecs:
|
||||
#
|
||||
# https://msdn.microsoft.com/en-us/library/cc463912.aspx
|
||||
# https://msdn.microsoft.com/en-us/library/cc463900(v=exchg.80).aspx
|
||||
# https://msdn.microsoft.com/en-us/library/ee157583(v=exchg.80).aspx
|
||||
# https://blogs.msdn.microsoft.com/openspecification/2009/11/06/msg-file-format-part-1/
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
from functools import reduce
|
||||
|
||||
import email.message, email.parser, email.policy
|
||||
from email.utils import parsedate_to_datetime, formatdate, formataddr
|
||||
|
||||
import compoundfiles
|
||||
|
||||
|
||||
# MAIN FUNCTIONS
|
||||
|
||||
|
||||
def load(filename_or_stream):
|
||||
with compoundfiles.CompoundFileReader(filename_or_stream) as doc:
|
||||
doc.rtf_attachments = 0
|
||||
return load_message_stream(doc.root, True, doc)
|
||||
|
||||
|
||||
def load_message_stream(entry, is_top_level, doc):
|
||||
# Load stream data.
|
||||
props = parse_properties(entry['__properties_version1.0'], is_top_level, entry, doc)
|
||||
|
||||
# Construct the MIME message....
|
||||
msg = email.message.EmailMessage()
|
||||
|
||||
# Add the raw headers, if known.
|
||||
if 'TRANSPORT_MESSAGE_HEADERS' in props:
|
||||
# Get the string holding all of the headers.
|
||||
headers = props['TRANSPORT_MESSAGE_HEADERS']
|
||||
if isinstance(headers, bytes):
|
||||
headers = headers.decode("utf-8")
|
||||
|
||||
# Remove content-type header because the body we can get this
|
||||
# way is just the plain-text portion of the email and whatever
|
||||
# Content-Type header was in the original is not valid for
|
||||
# reconstructing it this way.
|
||||
headers = re.sub("Content-Type: .*(\n\s.*)*\n", "", headers, re.I)
|
||||
|
||||
# Parse them.
|
||||
headers = email.parser.HeaderParser(policy=email.policy.default)\
|
||||
.parsestr(headers)
|
||||
|
||||
# Copy them into the message object.
|
||||
for header, value in headers.items():
|
||||
msg[header] = value
|
||||
|
||||
else:
|
||||
# Construct common headers from metadata.
|
||||
|
||||
msg['Date'] = formatdate(props['MESSAGE_DELIVERY_TIME'].timestamp())
|
||||
del props['MESSAGE_DELIVERY_TIME']
|
||||
|
||||
if props['SENDER_NAME'] != props['SENT_REPRESENTING_NAME']:
|
||||
props['SENDER_NAME'] += " (" + props['SENT_REPRESENTING_NAME'] + ")"
|
||||
del props['SENT_REPRESENTING_NAME']
|
||||
msg['From'] = formataddr((props['SENDER_NAME'], ""))
|
||||
del props['SENDER_NAME']
|
||||
|
||||
msg['To'] = props['DISPLAY_TO']
|
||||
del props['DISPLAY_TO']
|
||||
|
||||
msg['CC'] = props['DISPLAY_CC']
|
||||
del props['DISPLAY_CC']
|
||||
|
||||
msg['BCC'] = props['DISPLAY_BCC']
|
||||
del props['DISPLAY_BCC']
|
||||
|
||||
msg['Subject'] = props['SUBJECT']
|
||||
del props['SUBJECT']
|
||||
|
||||
# Add the plain-text body from the BODY field.
|
||||
if 'BODY' in props:
|
||||
body = props['BODY']
|
||||
if isinstance(body, str):
|
||||
msg.set_content(body, cte='quoted-printable')
|
||||
else:
|
||||
msg.set_content(body, maintype="text", subtype="plain", cte='8bit')
|
||||
|
||||
# Plain-text is not availabe. Use the rich text version.
|
||||
else:
|
||||
doc.rtf_attachments += 1
|
||||
fn = "messagebody_{}.rtf".format(doc.rtf_attachments)
|
||||
|
||||
msg.set_content(
|
||||
"<no plain text message body --- see attachment {}>".format(fn),
|
||||
cte='quoted-printable')
|
||||
|
||||
# Decompress the value to Rich Text Format.
|
||||
import compressed_rtf
|
||||
rtf = props['RTF_COMPRESSED']
|
||||
rtf = compressed_rtf.decompress(rtf)
|
||||
|
||||
# Add RTF file as an attachment.
|
||||
msg.add_attachment(
|
||||
rtf,
|
||||
maintype="text", subtype="rtf",
|
||||
filename=fn)
|
||||
|
||||
# # Copy over string values of remaining properties as headers
|
||||
# # so we don't lose any information.
|
||||
# for k, v in props.items():
|
||||
# if k == 'RTF_COMPRESSED': continue # not interested, save output
|
||||
# msg[k] = str(v)
|
||||
|
||||
# Add attachments.
|
||||
for stream in entry:
|
||||
if stream.name.startswith("__attach_version1.0_#"):
|
||||
process_attachment(msg, stream, doc)
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def process_attachment(msg, entry, doc):
|
||||
# Load attachment stream.
|
||||
props = parse_properties(entry['__properties_version1.0'], False, entry, doc)
|
||||
|
||||
# The attachment content...
|
||||
blob = props['ATTACH_DATA_BIN']
|
||||
|
||||
# Get the filename and MIME type of the attachment.
|
||||
filename = props.get("ATTACH_FILENAME") or props.get("DISPLAY_NAME")
|
||||
if isinstance(filename, bytes): filename = filename.decode("utf8")
|
||||
|
||||
mime_type = props.get('ATTACH_MIME_TAG', 'application/octet-stream')
|
||||
if isinstance(mime_type, bytes): mime_type = mime_type.decode("utf8")
|
||||
|
||||
# Python 3.6.
|
||||
if isinstance(blob, str):
|
||||
msg.add_attachment(
|
||||
blob,
|
||||
filename=filename)
|
||||
elif isinstance(blob, bytes):
|
||||
msg.add_attachment(
|
||||
blob,
|
||||
maintype=mime_type.split("/", 1)[0], subtype=mime_type.split("/", 1)[-1],
|
||||
filename=filename)
|
||||
else: # a Message instance
|
||||
msg.add_attachment(
|
||||
blob,
|
||||
filename=filename)
|
||||
|
||||
def parse_properties(properties, is_top_level, container, doc):
|
||||
# Read a properties stream and return a Python dictionary
|
||||
# of the fields and values, using human-readable field names
|
||||
# in the mapping at the top of this module.
|
||||
|
||||
# Load stream content.
|
||||
with doc.open(properties) as stream:
|
||||
stream = stream.read()
|
||||
|
||||
# Skip header.
|
||||
i = (32 if is_top_level else 24)
|
||||
|
||||
# Read 16-byte entries.
|
||||
ret = { }
|
||||
while i < len(stream):
|
||||
# Read the entry.
|
||||
property_type = stream[i+0:i+2]
|
||||
property_tag = stream[i+2:i+4]
|
||||
flags = stream[i+4:i+8]
|
||||
value = stream[i+8:i+16]
|
||||
i += 16
|
||||
|
||||
# Turn the byte strings into numbers and look up the property type.
|
||||
property_type = property_type[0] + (property_type[1]<<8)
|
||||
property_tag = property_tag[0] + (property_tag[1]<<8)
|
||||
if property_tag not in property_tags: continue # should not happen
|
||||
tag_name, _ = property_tags[property_tag]
|
||||
tag_type = property_types.get(property_type)
|
||||
|
||||
# Fixed Length Properties.
|
||||
if isinstance(tag_type, FixedLengthValueLoader):
|
||||
value = tag_type.load(value)
|
||||
|
||||
# Variable Length Properties.
|
||||
elif isinstance(tag_type, VariableLengthValueLoader):
|
||||
value_length = stream[i+8:i+12] # not used
|
||||
|
||||
# Look up the stream in the document that holds the value.
|
||||
streamname = "__substg1.0_{0:0{1}X}{2:0{3}X}".format(property_tag,4, property_type,4)
|
||||
try:
|
||||
with doc.open(container[streamname]) as innerstream:
|
||||
value = innerstream.read()
|
||||
except:
|
||||
# Stream isn't present!
|
||||
print("stream missing", streamname, file=sys.stderr)
|
||||
continue
|
||||
|
||||
value = tag_type.load(value)
|
||||
|
||||
elif isinstance(tag_type, EMBEDDED_MESSAGE):
|
||||
# Look up the stream in the document that holds the attachment.
|
||||
streamname = "__substg1.0_{0:0{1}X}{2:0{3}X}".format(property_tag,4, property_type,4)
|
||||
try:
|
||||
value = container[streamname]
|
||||
except:
|
||||
# Stream isn't present!
|
||||
print("stream missing", streamname, file=sys.stderr)
|
||||
continue
|
||||
value = tag_type.load(value, doc)
|
||||
|
||||
else:
|
||||
# unrecognized type
|
||||
print("unhandled property type", hex(property_type), file=sys.stderr)
|
||||
continue
|
||||
|
||||
ret[tag_name] = value
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# PROPERTY VALUE LOADERS
|
||||
|
||||
class FixedLengthValueLoader(object):
|
||||
pass
|
||||
|
||||
class NULL(FixedLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is an eight-byte long bytestring with unused content.
|
||||
return None
|
||||
|
||||
class BOOLEAN(FixedLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is an eight-byte long bytestring holding a two-byte integer.
|
||||
return value[0] == 1
|
||||
|
||||
class INTEGER16(FixedLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is an eight-byte long bytestring holding a two-byte integer.
|
||||
return reduce(lambda a, b : (a<<8)+b, reversed(value[0:2]))
|
||||
|
||||
class INTEGER32(FixedLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is an eight-byte long bytestring holding a four-byte integer.
|
||||
return reduce(lambda a, b : (a<<8)+b, reversed(value[0:4]))
|
||||
|
||||
class INTEGER64(FixedLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is an eight-byte long bytestring holding an eight-byte integer.
|
||||
return reduce(lambda a, b : (a<<8)+b, reversed(value))
|
||||
|
||||
class INTTIME(FixedLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is an eight-byte long bytestring encoding the integer number of
|
||||
# 100-nanosecond intervals since January 1, 1601.
|
||||
from datetime import datetime, timedelta
|
||||
value = reduce(lambda a, b : (a<<8)+b, reversed(value)) # bytestring to integer
|
||||
value = datetime(1601, 1, 1) + timedelta(seconds=value/10000000)
|
||||
return value
|
||||
|
||||
# TODO: The other fixed-length data types:
|
||||
# "FLOAT", "DOUBLE", "CURRENCY", "APPTIME", "ERROR"
|
||||
|
||||
class VariableLengthValueLoader(object):
|
||||
pass
|
||||
|
||||
class BINARY(VariableLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is a bytestring. Just return it.
|
||||
return value
|
||||
|
||||
class STRING8(VariableLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is a bytestring. I haven't seen specified what character encoding
|
||||
# is used when the Unicode storage type is not used, so we'll assume it's
|
||||
# ASCII or Latin-1 like but we'll use UTF-8 to cover the bases.
|
||||
return value.decode("utf8")
|
||||
|
||||
class UNICODE(VariableLengthValueLoader):
|
||||
@staticmethod
|
||||
def load(value):
|
||||
# value is a bytestring. I haven't seen specified what character encoding
|
||||
# is used when the Unicode storage type is not used, so we'll assume it's
|
||||
# ASCII or Latin-1 like but we'll use UTF-8 to cover the bases.
|
||||
return value.decode("utf16")
|
||||
|
||||
# TODO: The other variable-length tag types are "CLSID", "OBJECT".
|
||||
|
||||
class EMBEDDED_MESSAGE(object):
|
||||
@staticmethod
|
||||
def load(entry, doc):
|
||||
return load_message_stream(entry, False, doc)
|
||||
|
||||
|
||||
# CONSTANTS
|
||||
|
||||
# These constants are defined by the Microsoft Outlook file format
|
||||
# and identify the data types and data fields in the .msg file.
|
||||
|
||||
# from mapidefs.h via https://github.com/inverse-inc/openchange.old/blob/master/libmapi/mapidefs.h
|
||||
property_types = {
|
||||
0x1: NULL(),
|
||||
0x2: INTEGER16(),
|
||||
0x3: INTEGER32(),
|
||||
0x4: "FLOAT",
|
||||
0x5: "DOUBLE",
|
||||
0x6: "CURRENCY",
|
||||
0x7: "APPTIME",
|
||||
0xa: "ERROR",
|
||||
0xb: BOOLEAN(),
|
||||
0xd: EMBEDDED_MESSAGE(),
|
||||
0x14: INTEGER64(),
|
||||
0x1e: STRING8(),
|
||||
0x1f: UNICODE(),
|
||||
0x40: INTTIME(),
|
||||
0x48: "CLSID",
|
||||
0xFB: "SVREID",
|
||||
0xFD: "SRESTRICT",
|
||||
0xFE: "ACTIONS",
|
||||
0x102: BINARY(),
|
||||
}
|
||||
|
||||
# from mapitags.h via https://github.com/mvz/email-outlook-message-perl/blob/master/mapitags.h
|
||||
property_tags = {
|
||||
0x01: ('ACKNOWLEDGEMENT_MODE', 'I4'),
|
||||
0x02: ('ALTERNATE_RECIPIENT_ALLOWED', 'BOOLEAN'),
|
||||
0x03: ('AUTHORIZING_USERS', 'BINARY'),
|
||||
# Comment on an automatically forwarded message
|
||||
0x04: ('AUTO_FORWARD_COMMENT', 'STRING'),
|
||||
# Whether a message has been automatically forwarded
|
||||
0x05: ('AUTO_FORWARDED', 'BOOLEAN'),
|
||||
0x06: ('CONTENT_CONFIDENTIALITY_ALGORITHM_ID', 'BINARY'),
|
||||
0x07: ('CONTENT_CORRELATOR', 'BINARY'),
|
||||
0x08: ('CONTENT_IDENTIFIER', 'STRING'),
|
||||
# MIME content length
|
||||
0x09: ('CONTENT_LENGTH', 'I4'),
|
||||
0x0A: ('CONTENT_RETURN_REQUESTED', 'BOOLEAN'),
|
||||
0x0B: ('CONVERSATION_KEY', 'BINARY'),
|
||||
0x0C: ('CONVERSION_EITS', 'BINARY'),
|
||||
0x0D: ('CONVERSION_WITH_LOSS_PROHIBITED', 'BOOLEAN'),
|
||||
0x0E: ('CONVERTED_EITS', 'BINARY'),
|
||||
# Time to deliver for delayed delivery messages
|
||||
0x0F: ('DEFERRED_DELIVERY_TIME', 'SYSTIME'),
|
||||
0x10: ('DELIVER_TIME', 'SYSTIME'),
|
||||
# Reason a message was discarded
|
||||
0x11: ('DISCARD_REASON', 'I4'),
|
||||
0x12: ('DISCLOSURE_OF_RECIPIENTS', 'BOOLEAN'),
|
||||
0x13: ('DL_EXPANSION_HISTORY', 'BINARY'),
|
||||
0x14: ('DL_EXPANSION_PROHIBITED', 'BOOLEAN'),
|
||||
0x15: ('EXPIRY_TIME', 'SYSTIME'),
|
||||
0x16: ('IMPLICIT_CONVERSION_PROHIBITED', 'BOOLEAN'),
|
||||
# Message importance
|
||||
0x17: ('IMPORTANCE', 'I4'),
|
||||
0x18: ('IPM_ID', 'BINARY'),
|
||||
0x19: ('LATEST_DELIVERY_TIME', 'SYSTIME'),
|
||||
0x1A: ('MESSAGE_CLASS', 'STRING'),
|
||||
0x1B: ('MESSAGE_DELIVERY_ID', 'BINARY'),
|
||||
0x1E: ('MESSAGE_SECURITY_LABEL', 'BINARY'),
|
||||
0x1F: ('OBSOLETED_IPMS', 'BINARY'),
|
||||
# Person a message was originally for
|
||||
0x20: ('ORIGINALLY_INTENDED_RECIPIENT_NAME', 'BINARY'),
|
||||
0x21: ('ORIGINAL_EITS', 'BINARY'),
|
||||
0x22: ('ORIGINATOR_CERTIFICATE', 'BINARY'),
|
||||
0x23: ('ORIGINATOR_DELIVERY_REPORT_REQUESTED', 'BOOLEAN'),
|
||||
# Address of the message sender
|
||||
0x24: ('ORIGINATOR_RETURN_ADDRESS', 'BINARY'),
|
||||
0x25: ('PARENT_KEY', 'BINARY'),
|
||||
0x26: ('PRIORITY', 'I4'),
|
||||
0x27: ('ORIGIN_CHECK', 'BINARY'),
|
||||
0x28: ('PROOF_OF_SUBMISSION_REQUESTED', 'BOOLEAN'),
|
||||
# Whether a read receipt is desired
|
||||
0x29: ('READ_RECEIPT_REQUESTED', 'BOOLEAN'),
|
||||
# Time a message was received
|
||||
0x2A: ('RECEIPT_TIME', 'SYSTIME'),
|
||||
0x2B: ('RECIPIENT_REASSIGNMENT_PROHIBITED', 'BOOLEAN'),
|
||||
0x2C: ('REDIRECTION_HISTORY', 'BINARY'),
|
||||
0x2D: ('RELATED_IPMS', 'BINARY'),
|
||||
# Sensitivity of the original message
|
||||
0x2E: ('ORIGINAL_SENSITIVITY', 'I4'),
|
||||
0x2F: ('LANGUAGES', 'STRING'),
|
||||
0x30: ('REPLY_TIME', 'SYSTIME'),
|
||||
0x31: ('REPORT_TAG', 'BINARY'),
|
||||
0x32: ('REPORT_TIME', 'SYSTIME'),
|
||||
0x33: ('RETURNED_IPM', 'BOOLEAN'),
|
||||
0x34: ('SECURITY', 'I4'),
|
||||
0x35: ('INCOMPLETE_COPY', 'BOOLEAN'),
|
||||
0x36: ('SENSITIVITY', 'I4'),
|
||||
# The message subject
|
||||
0x37: ('SUBJECT', 'STRING'),
|
||||
0x38: ('SUBJECT_IPM', 'BINARY'),
|
||||
0x39: ('CLIENT_SUBMIT_TIME', 'SYSTIME'),
|
||||
0x3A: ('REPORT_NAME', 'STRING'),
|
||||
0x3B: ('SENT_REPRESENTING_SEARCH_KEY', 'BINARY'),
|
||||
0x3C: ('X400_CONTENT_TYPE', 'BINARY'),
|
||||
0x3D: ('SUBJECT_PREFIX', 'STRING'),
|
||||
0x3E: ('NON_RECEIPT_REASON', 'I4'),
|
||||
0x3F: ('RECEIVED_BY_ENTRYID', 'BINARY'),
|
||||
# Received by: entry
|
||||
0x40: ('RECEIVED_BY_NAME', 'STRING'),
|
||||
0x41: ('SENT_REPRESENTING_ENTRYID', 'BINARY'),
|
||||
0x42: ('SENT_REPRESENTING_NAME', 'STRING'),
|
||||
0x43: ('RCVD_REPRESENTING_ENTRYID', 'BINARY'),
|
||||
0x44: ('RCVD_REPRESENTING_NAME', 'STRING'),
|
||||
0x45: ('REPORT_ENTRYID', 'BINARY'),
|
||||
0x46: ('READ_RECEIPT_ENTRYID', 'BINARY'),
|
||||
0x47: ('MESSAGE_SUBMISSION_ID', 'BINARY'),
|
||||
0x48: ('PROVIDER_SUBMIT_TIME', 'SYSTIME'),
|
||||
# Subject of the original message
|
||||
0x49: ('ORIGINAL_SUBJECT', 'STRING'),
|
||||
0x4A: ('DISC_VAL', 'BOOLEAN'),
|
||||
0x4B: ('ORIG_MESSAGE_CLASS', 'STRING'),
|
||||
0x4C: ('ORIGINAL_AUTHOR_ENTRYID', 'BINARY'),
|
||||
# Author of the original message
|
||||
0x4D: ('ORIGINAL_AUTHOR_NAME', 'STRING'),
|
||||
# Time the original message was submitted
|
||||
0x4E: ('ORIGINAL_SUBMIT_TIME', 'SYSTIME'),
|
||||
0x4F: ('REPLY_RECIPIENT_ENTRIES', 'BINARY'),
|
||||
0x50: ('REPLY_RECIPIENT_NAMES', 'STRING'),
|
||||
0x51: ('RECEIVED_BY_SEARCH_KEY', 'BINARY'),
|
||||
0x52: ('RCVD_REPRESENTING_SEARCH_KEY', 'BINARY'),
|
||||
0x53: ('READ_RECEIPT_SEARCH_KEY', 'BINARY'),
|
||||
0x54: ('REPORT_SEARCH_KEY', 'BINARY'),
|
||||
0x55: ('ORIGINAL_DELIVERY_TIME', 'SYSTIME'),
|
||||
0x56: ('ORIGINAL_AUTHOR_SEARCH_KEY', 'BINARY'),
|
||||
0x57: ('MESSAGE_TO_ME', 'BOOLEAN'),
|
||||
0x58: ('MESSAGE_CC_ME', 'BOOLEAN'),
|
||||
0x59: ('MESSAGE_RECIP_ME', 'BOOLEAN'),
|
||||
# Sender of the original message
|
||||
0x5A: ('ORIGINAL_SENDER_NAME', 'STRING'),
|
||||
0x5B: ('ORIGINAL_SENDER_ENTRYID', 'BINARY'),
|
||||
0x5C: ('ORIGINAL_SENDER_SEARCH_KEY', 'BINARY'),
|
||||
0x5D: ('ORIGINAL_SENT_REPRESENTING_NAME', 'STRING'),
|
||||
0x5E: ('ORIGINAL_SENT_REPRESENTING_ENTRYID', 'BINARY'),
|
||||
0x5F: ('ORIGINAL_SENT_REPRESENTING_SEARCH_KEY', 'BINARY'),
|
||||
0x60: ('START_DATE', 'SYSTIME'),
|
||||
0x61: ('END_DATE', 'SYSTIME'),
|
||||
0x62: ('OWNER_APPT_ID', 'I4'),
|
||||
# Whether a response to the message is desired
|
||||
0x63: ('RESPONSE_REQUESTED', 'BOOLEAN'),
|
||||
0x64: ('SENT_REPRESENTING_ADDRTYPE', 'STRING'),
|
||||
0x65: ('SENT_REPRESENTING_EMAIL_ADDRESS', 'STRING'),
|
||||
0x66: ('ORIGINAL_SENDER_ADDRTYPE', 'STRING'),
|
||||
# Email of the original message sender
|
||||
0x67: ('ORIGINAL_SENDER_EMAIL_ADDRESS', 'STRING'),
|
||||
0x68: ('ORIGINAL_SENT_REPRESENTING_ADDRTYPE', 'STRING'),
|
||||
0x69: ('ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS', 'STRING'),
|
||||
0x70: ('CONVERSATION_TOPIC', 'STRING'),
|
||||
0x71: ('CONVERSATION_INDEX', 'BINARY'),
|
||||
0x72: ('ORIGINAL_DISPLAY_BCC', 'STRING'),
|
||||
0x73: ('ORIGINAL_DISPLAY_CC', 'STRING'),
|
||||
0x74: ('ORIGINAL_DISPLAY_TO', 'STRING'),
|
||||
0x75: ('RECEIVED_BY_ADDRTYPE', 'STRING'),
|
||||
0x76: ('RECEIVED_BY_EMAIL_ADDRESS', 'STRING'),
|
||||
0x77: ('RCVD_REPRESENTING_ADDRTYPE', 'STRING'),
|
||||
0x78: ('RCVD_REPRESENTING_EMAIL_ADDRESS', 'STRING'),
|
||||
0x79: ('ORIGINAL_AUTHOR_ADDRTYPE', 'STRING'),
|
||||
0x7A: ('ORIGINAL_AUTHOR_EMAIL_ADDRESS', 'STRING'),
|
||||
0x7B: ('ORIGINALLY_INTENDED_RECIP_ADDRTYPE', 'STRING'),
|
||||
0x7C: ('ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS', 'STRING'),
|
||||
0x7D: ('TRANSPORT_MESSAGE_HEADERS', 'STRING'),
|
||||
0x7E: ('DELEGATION', 'BINARY'),
|
||||
0x7F: ('TNEF_CORRELATION_KEY', 'BINARY'),
|
||||
0x1000: ('BODY', 'STRING'),
|
||||
0x1001: ('REPORT_TEXT', 'STRING'),
|
||||
0x1002: ('ORIGINATOR_AND_DL_EXPANSION_HISTORY', 'BINARY'),
|
||||
0x1003: ('REPORTING_DL_NAME', 'BINARY'),
|
||||
0x1004: ('REPORTING_MTA_CERTIFICATE', 'BINARY'),
|
||||
0x1006: ('RTF_SYNC_BODY_CRC', 'I4'),
|
||||
0x1007: ('RTF_SYNC_BODY_COUNT', 'I4'),
|
||||
0x1008: ('RTF_SYNC_BODY_TAG', 'STRING'),
|
||||
0x1009: ('RTF_COMPRESSED', 'BINARY'),
|
||||
0x1010: ('RTF_SYNC_PREFIX_COUNT', 'I4'),
|
||||
0x1011: ('RTF_SYNC_TRAILING_COUNT', 'I4'),
|
||||
0x1012: ('ORIGINALLY_INTENDED_RECIP_ENTRYID', 'BINARY'),
|
||||
0x0C00: ('CONTENT_INTEGRITY_CHECK', 'BINARY'),
|
||||
0x0C01: ('EXPLICIT_CONVERSION', 'I4'),
|
||||
0x0C02: ('IPM_RETURN_REQUESTED', 'BOOLEAN'),
|
||||
0x0C03: ('MESSAGE_TOKEN', 'BINARY'),
|
||||
0x0C04: ('NDR_REASON_CODE', 'I4'),
|
||||
0x0C05: ('NDR_DIAG_CODE', 'I4'),
|
||||
0x0C06: ('NON_RECEIPT_NOTIFICATION_REQUESTED', 'BOOLEAN'),
|
||||
0x0C07: ('DELIVERY_POINT', 'I4'),
|
||||
0x0C08: ('ORIGINATOR_NON_DELIVERY_REPORT_REQUESTED', 'BOOLEAN'),
|
||||
0x0C09: ('ORIGINATOR_REQUESTED_ALTERNATE_RECIPIENT', 'BINARY'),
|
||||
0x0C0A: ('PHYSICAL_DELIVERY_BUREAU_FAX_DELIVERY', 'BOOLEAN'),
|
||||
0x0C0B: ('PHYSICAL_DELIVERY_MODE', 'I4'),
|
||||
0x0C0C: ('PHYSICAL_DELIVERY_REPORT_REQUEST', 'I4'),
|
||||
0x0C0D: ('PHYSICAL_FORWARDING_ADDRESS', 'BINARY'),
|
||||
0x0C0E: ('PHYSICAL_FORWARDING_ADDRESS_REQUESTED', 'BOOLEAN'),
|
||||
0x0C0F: ('PHYSICAL_FORWARDING_PROHIBITED', 'BOOLEAN'),
|
||||
0x0C10: ('PHYSICAL_RENDITION_ATTRIBUTES', 'BINARY'),
|
||||
0x0C11: ('PROOF_OF_DELIVERY', 'BINARY'),
|
||||
0x0C12: ('PROOF_OF_DELIVERY_REQUESTED', 'BOOLEAN'),
|
||||
0x0C13: ('RECIPIENT_CERTIFICATE', 'BINARY'),
|
||||
0x0C14: ('RECIPIENT_NUMBER_FOR_ADVICE', 'STRING'),
|
||||
0x0C15: ('RECIPIENT_TYPE', 'I4'),
|
||||
0x0C16: ('REGISTERED_MAIL_TYPE', 'I4'),
|
||||
0x0C17: ('REPLY_REQUESTED', 'BOOLEAN'),
|
||||
0x0C18: ('REQUESTED_DELIVERY_METHOD', 'I4'),
|
||||
0x0C19: ('SENDER_ENTRYID', 'BINARY'),
|
||||
0x0C1A: ('SENDER_NAME', 'STRING'),
|
||||
0x0C1B: ('SUPPLEMENTARY_INFO', 'STRING'),
|
||||
0x0C1C: ('TYPE_OF_MTS_USER', 'I4'),
|
||||
0x0C1D: ('SENDER_SEARCH_KEY', 'BINARY'),
|
||||
0x0C1E: ('SENDER_ADDRTYPE', 'STRING'),
|
||||
0x0C1F: ('SENDER_EMAIL_ADDRESS', 'STRING'),
|
||||
0x0E00: ('CURRENT_VERSION', 'I8'),
|
||||
0x0E01: ('DELETE_AFTER_SUBMIT', 'BOOLEAN'),
|
||||
0x0E02: ('DISPLAY_BCC', 'STRING'),
|
||||
0x0E03: ('DISPLAY_CC', 'STRING'),
|
||||
0x0E04: ('DISPLAY_TO', 'STRING'),
|
||||
0x0E05: ('PARENT_DISPLAY', 'STRING'),
|
||||
0x0E06: ('MESSAGE_DELIVERY_TIME', 'SYSTIME'),
|
||||
0x0E07: ('MESSAGE_FLAGS', 'I4'),
|
||||
0x0E08: ('MESSAGE_SIZE', 'I4'),
|
||||
0x0E09: ('PARENT_ENTRYID', 'BINARY'),
|
||||
0x0E0A: ('SENTMAIL_ENTRYID', 'BINARY'),
|
||||
0x0E0C: ('CORRELATE', 'BOOLEAN'),
|
||||
0x0E0D: ('CORRELATE_MTSID', 'BINARY'),
|
||||
0x0E0E: ('DISCRETE_VALUES', 'BOOLEAN'),
|
||||
0x0E0F: ('RESPONSIBILITY', 'BOOLEAN'),
|
||||
0x0E10: ('SPOOLER_STATUS', 'I4'),
|
||||
0x0E11: ('TRANSPORT_STATUS', 'I4'),
|
||||
0x0E12: ('MESSAGE_RECIPIENTS', 'OBJECT'),
|
||||
0x0E13: ('MESSAGE_ATTACHMENTS', 'OBJECT'),
|
||||
0x0E14: ('SUBMIT_FLAGS', 'I4'),
|
||||
0x0E15: ('RECIPIENT_STATUS', 'I4'),
|
||||
0x0E16: ('TRANSPORT_KEY', 'I4'),
|
||||
0x0E17: ('MSG_STATUS', 'I4'),
|
||||
0x0E18: ('MESSAGE_DOWNLOAD_TIME', 'I4'),
|
||||
0x0E19: ('CREATION_VERSION', 'I8'),
|
||||
0x0E1A: ('MODIFY_VERSION', 'I8'),
|
||||
0x0E1B: ('HASATTACH', 'BOOLEAN'),
|
||||
0x0E1D: ('NORMALIZED_SUBJECT', 'STRING'),
|
||||
0x0E1F: ('RTF_IN_SYNC', 'BOOLEAN'),
|
||||
0x0E20: ('ATTACH_SIZE', 'I4'),
|
||||
0x0E21: ('ATTACH_NUM', 'I4'),
|
||||
0x0E22: ('PREPROCESS', 'BOOLEAN'),
|
||||
0x0E25: ('ORIGINATING_MTA_CERTIFICATE', 'BINARY'),
|
||||
0x0E26: ('PROOF_OF_SUBMISSION', 'BINARY'),
|
||||
# A unique identifier for editing the properties of a MAPI object
|
||||
0x0FFF: ('ENTRYID', 'BINARY'),
|
||||
# The type of an object
|
||||
0x0FFE: ('OBJECT_TYPE', 'I4'),
|
||||
0x0FFD: ('ICON', 'BINARY'),
|
||||
0x0FFC: ('MINI_ICON', 'BINARY'),
|
||||
0x0FFB: ('STORE_ENTRYID', 'BINARY'),
|
||||
0x0FFA: ('STORE_RECORD_KEY', 'BINARY'),
|
||||
# Binary identifer for an individual object
|
||||
0x0FF9: ('RECORD_KEY', 'BINARY'),
|
||||
0x0FF8: ('MAPPING_SIGNATURE', 'BINARY'),
|
||||
0x0FF7: ('ACCESS_LEVEL', 'I4'),
|
||||
# The primary key of a column in a table
|
||||
0x0FF6: ('INSTANCE_KEY', 'BINARY'),
|
||||
0x0FF5: ('ROW_TYPE', 'I4'),
|
||||
0x0FF4: ('ACCESS', 'I4'),
|
||||
0x3000: ('ROWID', 'I4'),
|
||||
# The name to display for a given MAPI object
|
||||
0x3001: ('DISPLAY_NAME', 'STRING'),
|
||||
0x3002: ('ADDRTYPE', 'STRING'),
|
||||
# An email address
|
||||
0x3003: ('EMAIL_ADDRESS', 'STRING'),
|
||||
# A comment field
|
||||
0x3004: ('COMMENT', 'STRING'),
|
||||
0x3005: ('DEPTH', 'I4'),
|
||||
# Provider-defined display name for a service provider
|
||||
0x3006: ('PROVIDER_DISPLAY', 'STRING'),
|
||||
# The time an object was created
|
||||
0x3007: ('CREATION_TIME', 'SYSTIME'),
|
||||
# The time an object was last modified
|
||||
0x3008: ('LAST_MODIFICATION_TIME', 'SYSTIME'),
|
||||
# Flags describing a service provider, message service, or status object
|
||||
0x3009: ('RESOURCE_FLAGS', 'I4'),
|
||||
# The name of a provider dll, minus any "32" suffix and ".dll"
|
||||
0x300A: ('PROVIDER_DLL_NAME', 'STRING'),
|
||||
0x300B: ('SEARCH_KEY', 'BINARY'),
|
||||
0x300C: ('PROVIDER_UID', 'BINARY'),
|
||||
0x300D: ('PROVIDER_ORDINAL', 'I4'),
|
||||
0x3301: ('FORM_VERSION', 'STRING'),
|
||||
0x3302: ('FORM_CLSID', 'CLSID'),
|
||||
0x3303: ('FORM_CONTACT_NAME', 'STRING'),
|
||||
0x3304: ('FORM_CATEGORY', 'STRING'),
|
||||
0x3305: ('FORM_CATEGORY_SUB', 'STRING'),
|
||||
0x3306: ('FORM_HOST_MAP', 'MV_LONG'),
|
||||
0x3307: ('FORM_HIDDEN', 'BOOLEAN'),
|
||||
0x3308: ('FORM_DESIGNER_NAME', 'STRING'),
|
||||
0x3309: ('FORM_DESIGNER_GUID', 'CLSID'),
|
||||
0x330A: ('FORM_MESSAGE_BEHAVIOR', 'I4'),
|
||||
# Is this row the default message store?
|
||||
0x3400: ('DEFAULT_STORE', 'BOOLEAN'),
|
||||
0x340D: ('STORE_SUPPORT_MASK', 'I4'),
|
||||
0x340E: ('STORE_STATE', 'I4'),
|
||||
0x3410: ('IPM_SUBTREE_SEARCH_KEY', 'BINARY'),
|
||||
0x3411: ('IPM_OUTBOX_SEARCH_KEY', 'BINARY'),
|
||||
0x3412: ('IPM_WASTEBASKET_SEARCH_KEY', 'BINARY'),
|
||||
0x3413: ('IPM_SENTMAIL_SEARCH_KEY', 'BINARY'),
|
||||
# Provder-defined message store type
|
||||
0x3414: ('MDB_PROVIDER', 'BINARY'),
|
||||
0x3415: ('RECEIVE_FOLDER_SETTINGS', 'OBJECT'),
|
||||
0x35DF: ('VALID_FOLDER_MASK', 'I4'),
|
||||
0x35E0: ('IPM_SUBTREE_ENTRYID', 'BINARY'),
|
||||
0x35E2: ('IPM_OUTBOX_ENTRYID', 'BINARY'),
|
||||
0x35E3: ('IPM_WASTEBASKET_ENTRYID', 'BINARY'),
|
||||
0x35E4: ('IPM_SENTMAIL_ENTRYID', 'BINARY'),
|
||||
0x35E5: ('VIEWS_ENTRYID', 'BINARY'),
|
||||
0x35E6: ('COMMON_VIEWS_ENTRYID', 'BINARY'),
|
||||
0x35E7: ('FINDER_ENTRYID', 'BINARY'),
|
||||
0x3600: ('CONTAINER_FLAGS', 'I4'),
|
||||
0x3601: ('FOLDER_TYPE', 'I4'),
|
||||
0x3602: ('CONTENT_COUNT', 'I4'),
|
||||
0x3603: ('CONTENT_UNREAD', 'I4'),
|
||||
0x3604: ('CREATE_TEMPLATES', 'OBJECT'),
|
||||
0x3605: ('DETAILS_TABLE', 'OBJECT'),
|
||||
0x3607: ('SEARCH', 'OBJECT'),
|
||||
0x3609: ('SELECTABLE', 'BOOLEAN'),
|
||||
0x360A: ('SUBFOLDERS', 'BOOLEAN'),
|
||||
0x360B: ('STATUS', 'I4'),
|
||||
0x360C: ('ANR', 'STRING'),
|
||||
0x360D: ('CONTENTS_SORT_ORDER', 'MV_LONG'),
|
||||
0x360E: ('CONTAINER_HIERARCHY', 'OBJECT'),
|
||||
0x360F: ('CONTAINER_CONTENTS', 'OBJECT'),
|
||||
0x3610: ('FOLDER_ASSOCIATED_CONTENTS', 'OBJECT'),
|
||||
0x3611: ('DEF_CREATE_DL', 'BINARY'),
|
||||
0x3612: ('DEF_CREATE_MAILUSER', 'BINARY'),
|
||||
0x3613: ('CONTAINER_CLASS', 'STRING'),
|
||||
0x3614: ('CONTAINER_MODIFY_VERSION', 'I8'),
|
||||
0x3615: ('AB_PROVIDER_ID', 'BINARY'),
|
||||
0x3616: ('DEFAULT_VIEW_ENTRYID', 'BINARY'),
|
||||
0x3617: ('ASSOC_CONTENT_COUNT', 'I4'),
|
||||
0x3700: ('ATTACHMENT_X400_PARAMETERS', 'BINARY'),
|
||||
0x3701: ('ATTACH_DATA_OBJ', 'OBJECT'),
|
||||
0x3701: ('ATTACH_DATA_BIN', 'BINARY'),
|
||||
0x3702: ('ATTACH_ENCODING', 'BINARY'),
|
||||
0x3703: ('ATTACH_EXTENSION', 'STRING'),
|
||||
0x3704: ('ATTACH_FILENAME', 'STRING'),
|
||||
0x3705: ('ATTACH_METHOD', 'I4'),
|
||||
0x3707: ('ATTACH_LONG_FILENAME', 'STRING'),
|
||||
0x3708: ('ATTACH_PATHNAME', 'STRING'),
|
||||
0x370A: ('ATTACH_TAG', 'BINARY'),
|
||||
0x370B: ('RENDERING_POSITION', 'I4'),
|
||||
0x370C: ('ATTACH_TRANSPORT_NAME', 'STRING'),
|
||||
0x370D: ('ATTACH_LONG_PATHNAME', 'STRING'),
|
||||
0x370E: ('ATTACH_MIME_TAG', 'STRING'),
|
||||
0x370F: ('ATTACH_ADDITIONAL_INFO', 'BINARY'),
|
||||
0x3900: ('DISPLAY_TYPE', 'I4'),
|
||||
0x3902: ('TEMPLATEID', 'BINARY'),
|
||||
0x3904: ('PRIMARY_CAPABILITY', 'BINARY'),
|
||||
0x39FF: ('7BIT_DISPLAY_NAME', 'STRING'),
|
||||
0x3A00: ('ACCOUNT', 'STRING'),
|
||||
0x3A01: ('ALTERNATE_RECIPIENT', 'BINARY'),
|
||||
0x3A02: ('CALLBACK_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A03: ('CONVERSION_PROHIBITED', 'BOOLEAN'),
|
||||
0x3A04: ('DISCLOSE_RECIPIENTS', 'BOOLEAN'),
|
||||
0x3A05: ('GENERATION', 'STRING'),
|
||||
0x3A06: ('GIVEN_NAME', 'STRING'),
|
||||
0x3A07: ('GOVERNMENT_ID_NUMBER', 'STRING'),
|
||||
0x3A08: ('BUSINESS_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A09: ('HOME_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A0A: ('INITIALS', 'STRING'),
|
||||
0x3A0B: ('KEYWORD', 'STRING'),
|
||||
0x3A0C: ('LANGUAGE', 'STRING'),
|
||||
0x3A0D: ('LOCATION', 'STRING'),
|
||||
0x3A0E: ('MAIL_PERMISSION', 'BOOLEAN'),
|
||||
0x3A0F: ('MHS_COMMON_NAME', 'STRING'),
|
||||
0x3A10: ('ORGANIZATIONAL_ID_NUMBER', 'STRING'),
|
||||
0x3A11: ('SURNAME', 'STRING'),
|
||||
0x3A12: ('ORIGINAL_ENTRYID', 'BINARY'),
|
||||
0x3A13: ('ORIGINAL_DISPLAY_NAME', 'STRING'),
|
||||
0x3A14: ('ORIGINAL_SEARCH_KEY', 'BINARY'),
|
||||
0x3A15: ('POSTAL_ADDRESS', 'STRING'),
|
||||
0x3A16: ('COMPANY_NAME', 'STRING'),
|
||||
0x3A17: ('TITLE', 'STRING'),
|
||||
0x3A18: ('DEPARTMENT_NAME', 'STRING'),
|
||||
0x3A19: ('OFFICE_LOCATION', 'STRING'),
|
||||
0x3A1A: ('PRIMARY_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A1B: ('BUSINESS2_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A1C: ('MOBILE_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A1D: ('RADIO_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A1E: ('CAR_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A1F: ('OTHER_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A20: ('TRANSMITABLE_DISPLAY_NAME', 'STRING'),
|
||||
0x3A21: ('PAGER_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A22: ('USER_CERTIFICATE', 'BINARY'),
|
||||
0x3A23: ('PRIMARY_FAX_NUMBER', 'STRING'),
|
||||
0x3A24: ('BUSINESS_FAX_NUMBER', 'STRING'),
|
||||
0x3A25: ('HOME_FAX_NUMBER', 'STRING'),
|
||||
0x3A26: ('COUNTRY', 'STRING'),
|
||||
0x3A27: ('LOCALITY', 'STRING'),
|
||||
0x3A28: ('STATE_OR_PROVINCE', 'STRING'),
|
||||
0x3A29: ('STREET_ADDRESS', 'STRING'),
|
||||
0x3A2A: ('POSTAL_CODE', 'STRING'),
|
||||
0x3A2B: ('POST_OFFICE_BOX', 'STRING'),
|
||||
0x3A2C: ('TELEX_NUMBER', 'STRING'),
|
||||
0x3A2D: ('ISDN_NUMBER', 'STRING'),
|
||||
0x3A2E: ('ASSISTANT_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A2F: ('HOME2_TELEPHONE_NUMBER', 'STRING'),
|
||||
0x3A30: ('ASSISTANT', 'STRING'),
|
||||
0x3A40: ('SEND_RICH_INFO', 'BOOLEAN'),
|
||||
0x3A41: ('WEDDING_ANNIVERSARY', 'SYSTIME'),
|
||||
0x3A42: ('BIRTHDAY', 'SYSTIME'),
|
||||
0x3A43: ('HOBBIES', 'STRING'),
|
||||
0x3A44: ('MIDDLE_NAME', 'STRING'),
|
||||
0x3A45: ('DISPLAY_NAME_PREFIX', 'STRING'),
|
||||
0x3A46: ('PROFESSION', 'STRING'),
|
||||
0x3A47: ('PREFERRED_BY_NAME', 'STRING'),
|
||||
0x3A48: ('SPOUSE_NAME', 'STRING'),
|
||||
0x3A49: ('COMPUTER_NETWORK_NAME', 'STRING'),
|
||||
0x3A4A: ('CUSTOMER_ID', 'STRING'),
|
||||
0x3A4B: ('TTYTDD_PHONE_NUMBER', 'STRING'),
|
||||
0x3A4C: ('FTP_SITE', 'STRING'),
|
||||
0x3A4D: ('GENDER', 'I2'),
|
||||
0x3A4E: ('MANAGER_NAME', 'STRING'),
|
||||
0x3A4F: ('NICKNAME', 'STRING'),
|
||||
0x3A50: ('PERSONAL_HOME_PAGE', 'STRING'),
|
||||
0x3A51: ('BUSINESS_HOME_PAGE', 'STRING'),
|
||||
0x3A52: ('CONTACT_VERSION', 'CLSID'),
|
||||
0x3A53: ('CONTACT_ENTRYIDS', 'MV_BINARY'),
|
||||
0x3A54: ('CONTACT_ADDRTYPES', 'MV_STRING'),
|
||||
0x3A55: ('CONTACT_DEFAULT_ADDRESS_INDEX', 'I4'),
|
||||
0x3A56: ('CONTACT_EMAIL_ADDRESSES', 'MV_STRING'),
|
||||
0x3A57: ('COMPANY_MAIN_PHONE_NUMBER', 'STRING'),
|
||||
0x3A58: ('CHILDRENS_NAMES', 'MV_STRING'),
|
||||
0x3A59: ('HOME_ADDRESS_CITY', 'STRING'),
|
||||
0x3A5A: ('HOME_ADDRESS_COUNTRY', 'STRING'),
|
||||
0x3A5B: ('HOME_ADDRESS_POSTAL_CODE', 'STRING'),
|
||||
0x3A5C: ('HOME_ADDRESS_STATE_OR_PROVINCE', 'STRING'),
|
||||
0x3A5D: ('HOME_ADDRESS_STREET', 'STRING'),
|
||||
0x3A5E: ('HOME_ADDRESS_POST_OFFICE_BOX', 'STRING'),
|
||||
0x3A5F: ('OTHER_ADDRESS_CITY', 'STRING'),
|
||||
0x3A60: ('OTHER_ADDRESS_COUNTRY', 'STRING'),
|
||||
0x3A61: ('OTHER_ADDRESS_POSTAL_CODE', 'STRING'),
|
||||
0x3A62: ('OTHER_ADDRESS_STATE_OR_PROVINCE', 'STRING'),
|
||||
0x3A63: ('OTHER_ADDRESS_STREET', 'STRING'),
|
||||
0x3A64: ('OTHER_ADDRESS_POST_OFFICE_BOX', 'STRING'),
|
||||
0x3D00: ('STORE_PROVIDERS', 'BINARY'),
|
||||
0x3D01: ('AB_PROVIDERS', 'BINARY'),
|
||||
0x3D02: ('TRANSPORT_PROVIDERS', 'BINARY'),
|
||||
0x3D04: ('DEFAULT_PROFILE', 'BOOLEAN'),
|
||||
0x3D05: ('AB_SEARCH_PATH', 'MV_BINARY'),
|
||||
0x3D06: ('AB_DEFAULT_DIR', 'BINARY'),
|
||||
0x3D07: ('AB_DEFAULT_PAB', 'BINARY'),
|
||||
0x3D09: ('SERVICE_NAME', 'STRING'),
|
||||
0x3D0A: ('SERVICE_DLL_NAME', 'STRING'),
|
||||
0x3D0B: ('SERVICE_ENTRY_NAME', 'STRING'),
|
||||
0x3D0C: ('SERVICE_UID', 'BINARY'),
|
||||
0x3D0D: ('SERVICE_EXTRA_UIDS', 'BINARY'),
|
||||
0x3D0E: ('SERVICES', 'BINARY'),
|
||||
0x3D0F: ('SERVICE_SUPPORT_FILES', 'MV_STRING'),
|
||||
0x3D10: ('SERVICE_DELETE_FILES', 'MV_STRING'),
|
||||
0x3D11: ('AB_SEARCH_PATH_UPDATE', 'BINARY'),
|
||||
0x3D12: ('PROFILE_NAME', 'STRING'),
|
||||
0x3E00: ('IDENTITY_DISPLAY', 'STRING'),
|
||||
0x3E01: ('IDENTITY_ENTRYID', 'BINARY'),
|
||||
0x3E02: ('RESOURCE_METHODS', 'I4'),
|
||||
# Service provider type
|
||||
0x3E03: ('RESOURCE_TYPE', 'I4'),
|
||||
0x3E04: ('STATUS_CODE', 'I4'),
|
||||
0x3E05: ('IDENTITY_SEARCH_KEY', 'BINARY'),
|
||||
0x3E06: ('OWN_STORE_ENTRYID', 'BINARY'),
|
||||
0x3E07: ('RESOURCE_PATH', 'STRING'),
|
||||
0x3E08: ('STATUS_STRING', 'STRING'),
|
||||
0x3E09: ('X400_DEFERRED_DELIVERY_CANCEL', 'BOOLEAN'),
|
||||
0x3E0A: ('HEADER_FOLDER_ENTRYID', 'BINARY'),
|
||||
0x3E0B: ('REMOTE_PROGRESS', 'I4'),
|
||||
0x3E0C: ('REMOTE_PROGRESS_TEXT', 'STRING'),
|
||||
0x3E0D: ('REMOTE_VALIDATE_OK', 'BOOLEAN'),
|
||||
0x3F00: ('CONTROL_FLAGS', 'I4'),
|
||||
0x3F01: ('CONTROL_STRUCTURE', 'BINARY'),
|
||||
0x3F02: ('CONTROL_TYPE', 'I4'),
|
||||
0x3F03: ('DELTAX', 'I4'),
|
||||
0x3F04: ('DELTAY', 'I4'),
|
||||
0x3F05: ('XPOS', 'I4'),
|
||||
0x3F06: ('YPOS', 'I4'),
|
||||
0x3F07: ('CONTROL_ID', 'BINARY'),
|
||||
0x3F08: ('INITIAL_DETAILS_PANE', 'I4'),
|
||||
}
|
||||
|
||||
|
||||
# COMMAND-LINE ENTRY POINT
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If no command-line arguments are given, convert the .msg
|
||||
# file on STDIN to .eml format on STDOUT.
|
||||
if len(sys.argv) <= 1:
|
||||
print(load(sys.stdin), file=sys.stdout)
|
||||
|
||||
# Otherwise, for each file mentioned on the command-line,
|
||||
# convert it and save it to a file with ".eml" appended
|
||||
# to the name.
|
||||
else:
|
||||
for fn in sys.argv[1:]:
|
||||
print(fn + "...")
|
||||
msg = load(fn)
|
||||
with open(fn + ".eml", "wb") as f:
|
||||
f.write(msg.as_bytes())
|
||||
Reference in New Issue
Block a user