1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
|
import logging
from ..errors import InvalidTypeError
from ..extended_properties import ExtendedProperty
from ..fields import (
AttachmentField,
BodyField,
BooleanField,
CharField,
EWSElementField,
ExtendedPropertyField,
ExtendedPropertyListField,
IdElementField,
MailboxField,
MailboxListField,
)
from ..properties import EWSElement, EWSMeta, IdChangeKeyMixIn, InvalidField, ItemId, ReferenceItemId
from ..util import require_account
from ..version import EXCHANGE_2007_SP1
log = logging.getLogger(__name__)
# Shape enums
ID_ONLY = "IdOnly"
DEFAULT = "Default"
# AllProperties doesn't actually get all properties in FindItem, just the "first-class" ones. See
# https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/email-properties-and-elements-in-ews-in-exchange
ALL_PROPERTIES = "AllProperties"
SHAPE_CHOICES = (ID_ONLY, DEFAULT, ALL_PROPERTIES)
# MessageDisposition values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem
SAVE_ONLY = "SaveOnly"
SEND_ONLY = "SendOnly"
SEND_AND_SAVE_COPY = "SendAndSaveCopy"
MESSAGE_DISPOSITION_CHOICES = (SAVE_ONLY, SEND_ONLY, SEND_AND_SAVE_COPY)
# SendMeetingInvitations values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem
# SendMeetingInvitationsOrCancellations. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem
# SendMeetingCancellations values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem
SEND_TO_NONE = "SendToNone"
SEND_ONLY_TO_ALL = "SendOnlyToAll"
SEND_ONLY_TO_CHANGED = "SendOnlyToChanged"
SEND_TO_ALL_AND_SAVE_COPY = "SendToAllAndSaveCopy"
SEND_TO_CHANGED_AND_SAVE_COPY = "SendToChangedAndSaveCopy"
SEND_MEETING_INVITATIONS_CHOICES = (SEND_TO_NONE, SEND_ONLY_TO_ALL, SEND_TO_ALL_AND_SAVE_COPY)
SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES = (
SEND_TO_NONE,
SEND_ONLY_TO_ALL,
SEND_ONLY_TO_CHANGED,
SEND_TO_ALL_AND_SAVE_COPY,
SEND_TO_CHANGED_AND_SAVE_COPY,
)
SEND_MEETING_CANCELLATIONS_CHOICES = (SEND_TO_NONE, SEND_ONLY_TO_ALL, SEND_TO_ALL_AND_SAVE_COPY)
# AffectedTaskOccurrences values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem
ALL_OCCURRENCES = "AllOccurrences"
SPECIFIED_OCCURRENCE_ONLY = "SpecifiedOccurrenceOnly"
AFFECTED_TASK_OCCURRENCES_CHOICES = (ALL_OCCURRENCES, SPECIFIED_OCCURRENCE_ONLY)
# ConflictResolution values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem
NEVER_OVERWRITE = "NeverOverwrite"
AUTO_RESOLVE = "AutoResolve"
ALWAYS_OVERWRITE = "AlwaysOverwrite"
CONFLICT_RESOLUTION_CHOICES = (NEVER_OVERWRITE, AUTO_RESOLVE, ALWAYS_OVERWRITE)
# DeleteType values. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/deleteitem
HARD_DELETE = "HardDelete"
SOFT_DELETE = "SoftDelete"
MOVE_TO_DELETED_ITEMS = "MoveToDeletedItems"
DELETE_TYPE_CHOICES = (HARD_DELETE, SOFT_DELETE, MOVE_TO_DELETED_ITEMS)
class RegisterMixIn(IdChangeKeyMixIn, metaclass=EWSMeta):
"""Base class for classes that can change their list of supported fields dynamically."""
# This class implements dynamic fields on an element class, so we need to include __dict__ in __slots__
__slots__ = ("__dict__",)
INSERT_AFTER_FIELD = None
@classmethod
def register(cls, attr_name, attr_cls):
"""Register a custom extended property in this item class so they can be accessed just like any other attribute
:param attr_name:
:param attr_cls:
:return:
"""
if not cls.INSERT_AFTER_FIELD:
raise ValueError(f"Class {cls} is missing INSERT_AFTER_FIELD value")
try:
cls.get_field_by_fieldname(attr_name)
except InvalidField:
pass
else:
raise ValueError(f"'attr_name' {attr_name!r} is already registered")
if not issubclass(attr_cls, ExtendedProperty):
raise TypeError(f"'attr_cls' {attr_cls!r} must be a subclass of type {ExtendedProperty}")
# Check if class attributes are properly defined
attr_cls.validate_cls()
# ExtendedProperty is not a real field, but a placeholder in the fields list. See
# https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item
#
# Find the correct index for the new extended property, and insert.
if attr_cls.is_array_type():
field = ExtendedPropertyListField(attr_name, value_cls=attr_cls)
else:
field = ExtendedPropertyField(attr_name, value_cls=attr_cls)
cls.add_field(field, insert_after=cls.INSERT_AFTER_FIELD)
@classmethod
def deregister(cls, attr_name):
"""De-register an extended property that has been registered with register().
:param attr_name:
:return:
"""
try:
field = cls.get_field_by_fieldname(attr_name)
except InvalidField:
raise ValueError(f"{attr_name!r} is not registered")
if not isinstance(field, ExtendedPropertyField):
raise ValueError(f"{attr_name} is not registered as an ExtendedProperty")
cls.remove_field(field)
class BaseItem(RegisterMixIn, metaclass=EWSMeta):
"""Base class for all other classes that implement EWS items."""
ID_ELEMENT_CLS = ItemId
_id = IdElementField(field_uri="item:ItemId", value_cls=ID_ELEMENT_CLS)
__slots__ = "account", "folder"
def __init__(self, **kwargs):
"""Pick out optional 'account' and 'folder' kwargs, and pass the rest to the parent class.
:param kwargs:
'account' is optional but allows calling 'send()' and 'delete()'
'folder' is optional but allows calling 'save()'. If 'folder' has an account, and 'account' is not set,
we use folder.account.
"""
from ..account import Account
from ..folders import BaseFolder
self.account = kwargs.pop("account", None)
if self.account is not None and not isinstance(self.account, Account):
raise InvalidTypeError("account", self.account, Account)
self.folder = kwargs.pop("folder", None)
if self.folder is not None:
if not isinstance(self.folder, BaseFolder):
raise InvalidTypeError("folder", self.folder, BaseFolder)
if self.folder.account is not None:
if self.account is not None:
# Make sure the account from kwargs matches the folder account
if self.account != self.folder.account:
raise ValueError("'account' does not match 'folder.account'")
self.account = self.folder.account
super().__init__(**kwargs)
@classmethod
def from_xml(cls, elem, account):
item = super().from_xml(elem=elem, account=account)
item.account = account
return item
class BaseReplyItem(EWSElement, metaclass=EWSMeta):
"""Base class for reply/forward elements that share the same fields."""
subject = CharField(field_uri="Subject")
body = BodyField(field_uri="Body") # Accepts and returns Body or HTMLBody instances
to_recipients = MailboxListField(field_uri="ToRecipients")
cc_recipients = MailboxListField(field_uri="CcRecipients")
bcc_recipients = MailboxListField(field_uri="BccRecipients")
is_read_receipt_requested = BooleanField(field_uri="IsReadReceiptRequested")
is_delivery_receipt_requested = BooleanField(field_uri="IsDeliveryReceiptRequested")
author = MailboxField(field_uri="From")
reference_item_id = EWSElementField(value_cls=ReferenceItemId)
new_body = BodyField(field_uri="NewBodyContent") # Accepts and returns Body or HTMLBody instances
received_by = MailboxField(field_uri="ReceivedBy", supported_from=EXCHANGE_2007_SP1)
received_representing = MailboxField(field_uri="ReceivedRepresenting", supported_from=EXCHANGE_2007_SP1)
__slots__ = ("account",)
def __init__(self, **kwargs):
# 'account' is optional but allows calling 'send()' and 'save()'
from ..account import Account
self.account = kwargs.pop("account", None)
if self.account is not None and not isinstance(self.account, Account):
raise InvalidTypeError("account", self.account, Account)
super().__init__(**kwargs)
@require_account
def send(self, save_copy=True, copy_to_folder=None):
from ..services import CreateItem
if copy_to_folder and not save_copy:
raise AttributeError("'save_copy' must be True when 'copy_to_folder' is set")
message_disposition = SEND_AND_SAVE_COPY if save_copy else SEND_ONLY
return CreateItem(account=self.account).get(
items=[self],
folder=copy_to_folder,
message_disposition=message_disposition,
send_meeting_invitations=SEND_TO_NONE,
)
@require_account
def save(self, folder):
"""Save the item for later modification. You may want to use account.drafts as the folder.
:param folder:
:return:
"""
from ..services import CreateItem
return CreateItem(account=self.account).get(
items=[self],
folder=folder,
message_disposition=SAVE_ONLY,
send_meeting_invitations=SEND_TO_NONE,
)
class BulkCreateResult(BaseItem):
"""A dummy class to store return values from a CreateItem service call."""
attachments = AttachmentField(field_uri="item:Attachments") # ItemAttachment or FileAttachment
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.attachments is None:
self.attachments = []
|