from __future__ import annotations
__all__ = [
'OutlookImageDIB',
]
import struct
from typing import Optional, TYPE_CHECKING
from . import registerHandler
from .custom_handler import CustomAttachmentHandler
from ...enums import DVAspect, ErrorBehavior, InsecureFeatures
from ...exceptions import DependencyError, SecurityError
if TYPE_CHECKING:
from ..attachment_base import AttachmentBase
_ST_OLE = struct.Struct('<IIIII')
_ST_MAILSTREAM = struct.Struct('<III')
[docs]class OutlookImageDIB(CustomAttachmentHandler):
"""
Custom handler for a special attachment type, a Device Independent Bitmap
stored in a way special to Outlook.
"""
def __init__(self, attachment: AttachmentBase):
super().__init__(attachment)
# First, get the mandatory bitmap data.
self.__data = self.getStream('CONTENTS')
if not self.__data:
raise ValueError('Bitmap data could not be read for Outlook signature.')
# Next we need to get the mailstream.
stream = self.getStream('\x03MailStream')
if stream:
if len(stream) != 12:
raise ValueError('MailStream is the wrong length.')
# Unpack the mailstream.
vals = _ST_MAILSTREAM.unpack(stream)
self.__dvaspect = DVAspect(vals[0])
self.__x = vals[1]
self.__y = vals[2]
else:
#raise ValueError('MailStream could not be found.')
# Create default values.
self.__dvaspect = DVAspect.CONTENT
# TODO figure out what the default values for these should actually
# be.
self.__x = 0
self.__y = 0
# This is done regardless of default values or not.
# Convert to twips for RTF.
self.__xtwips = int(round(self.__x / 1.7639))
self.__ytwips = int(round(self.__y / 1.7639))
# Check the error behavior to see if we should even do this check.
if ErrorBehavior.CUSTOM_ATTACH_TOLERANT not in attachment.msg.errorBehavior:
# Get the OLE data.
oleStream = self.getStream('\x01Ole')
if oleStream:
# While I have only seen this stream be one length, it could in
# theory be more than one length. So long as it is *at least* 20
# bytes, we call it valid.
if len(oleStream) < 20:
raise ValueError('OLE stream is too short.')
# Unpack and verify the OLE stream.
vals = _ST_OLE.unpack(oleStream[:20])
# Check the version magic.
if vals[0] != 0x2000001:
raise ValueError('OLE stream has wrong version magic.')
# Check the reserved bytes.
if vals[3] != 0:
raise ValueError('OLE stream has non-zero reserved int.')
else:
#raise ValueError('OLE stream could not be found.')
# If the stream is there we validate it, so here we just leave
# it alone since nothing is actually stored.
pass
[docs] @classmethod
def isCorrectHandler(cls, attachment: AttachmentBase) -> bool:
if attachment.clsid != '00000316-0000-0000-C000-000000000046':
return False
# Check for the required streams.
if not attachment.exists('__substg1.0_3701000D/CONTENTS'):
return False
# These streams were previously considered mandatory, but are now
# tentatively optional.
#if not attachment.exists('__substg1.0_3701000D/\x01Ole'):
# return False
#if not attachment.exists('__substg1.0_3701000D/\x03MailStream'):
# return False
return True
[docs] def generateRtf(self) -> Optional[bytes]:
"""
Generates the RTF to inject in place of the \\objattph tag.
If this function should do nothing, returns ``None``.
:raises DependencyError: PIL or Pillow could not be found.
"""
if InsecureFeatures.PIL_IMAGE_PARSING not in self.attachment.msg.insecureFeatures:
raise SecurityError('Generating the RTF for a custom attachment requires the insecure feature PIL_IMAGE_PARSING.')
try:
import PIL.Image
except ImportError:
raise DependencyError('PIL or Pillow is required for inserting an Outlook Image into the body.')
# First, convert the bitmap into a PNG so we can insert it into the
# body.
import io
# Note, use self.data instead of self.__data to allow support for
# extensions.
with PIL.Image.open(io.BytesIO(self.data)) as img:
out = io.BytesIO()
img.save(out, 'PNG')
hexData = out.getvalue().hex()
inject = '{\\*\\shppict\n{\\pict\\picscalex100\\picscaley100'
inject += f'\\picw{img.width}\\pich{img.height}'
inject += f'\\picwgoal{self.__xtwips}\\pichgoal{self.__ytwips}\n'
inject += '\\pngblip ' + hexData + '}}'
return inject.encode()
@property
def data(self) -> bytes:
return self.__data
@property
def name(self) -> str:
# Try to get the name from the attachment. If that fails, name it based
# on the number.
if not (name := self.attachment.name):
name = f'attachment {int(self.attachment.dir[-8:], 16)}'
return name + '.bmp'
@property
def obj(self) -> bytes:
return self.data
registerHandler(OutlookImageDIB)