Module exchangelib.items

Expand source code
from .base import (
    AFFECTED_TASK_OCCURRENCES_CHOICES,
    ALL_OCCURRENCES,
    ALL_PROPERTIES,
    ALWAYS_OVERWRITE,
    AUTO_RESOLVE,
    CONFLICT_RESOLUTION_CHOICES,
    DEFAULT,
    DELETE_TYPE_CHOICES,
    HARD_DELETE,
    ID_ONLY,
    MESSAGE_DISPOSITION_CHOICES,
    MOVE_TO_DELETED_ITEMS,
    NEVER_OVERWRITE,
    SAVE_ONLY,
    SEND_AND_SAVE_COPY,
    SEND_MEETING_CANCELLATIONS_CHOICES,
    SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES,
    SEND_MEETING_INVITATIONS_CHOICES,
    SEND_ONLY,
    SEND_ONLY_TO_ALL,
    SEND_ONLY_TO_CHANGED,
    SEND_TO_ALL_AND_SAVE_COPY,
    SEND_TO_CHANGED_AND_SAVE_COPY,
    SEND_TO_NONE,
    SHAPE_CHOICES,
    SOFT_DELETE,
    SPECIFIED_OCCURRENCE_ONLY,
    BulkCreateResult,
    RegisterMixIn,
)
from .calendar_item import (
    CONFERENCE_TYPES,
    AcceptItem,
    CalendarItem,
    CancelCalendarItem,
    DeclineItem,
    MeetingCancellation,
    MeetingMessage,
    MeetingRequest,
    MeetingResponse,
    TentativelyAcceptItem,
)
from .contact import Contact, DistributionList, Persona
from .item import BaseItem, Item
from .message import ForwardItem, Message, ReplyAllToItem, ReplyToItem
from .post import PostItem, PostReplyItem
from .task import Task

# Traversal enums
SHALLOW = "Shallow"
SOFT_DELETED = "SoftDeleted"
ASSOCIATED = "Associated"
ITEM_TRAVERSAL_CHOICES = (SHALLOW, SOFT_DELETED, ASSOCIATED)

# Contacts search (ResolveNames) scope enums
ACTIVE_DIRECTORY = "ActiveDirectory"
ACTIVE_DIRECTORY_CONTACTS = "ActiveDirectoryContacts"
CONTACTS = "Contacts"
CONTACTS_ACTIVE_DIRECTORY = "ContactsActiveDirectory"
SEARCH_SCOPE_CHOICES = (ACTIVE_DIRECTORY, ACTIVE_DIRECTORY_CONTACTS, CONTACTS, CONTACTS_ACTIVE_DIRECTORY)


ITEM_CLASSES = (
    CalendarItem,
    Contact,
    DistributionList,
    Item,
    Message,
    MeetingMessage,
    MeetingRequest,
    MeetingResponse,
    MeetingCancellation,
    PostItem,
    Task,
)

__all__ = [
    "RegisterMixIn",
    "MESSAGE_DISPOSITION_CHOICES",
    "SAVE_ONLY",
    "SEND_ONLY",
    "SEND_AND_SAVE_COPY",
    "CalendarItem",
    "AcceptItem",
    "TentativelyAcceptItem",
    "DeclineItem",
    "CancelCalendarItem",
    "MeetingRequest",
    "MeetingResponse",
    "MeetingCancellation",
    "CONFERENCE_TYPES",
    "Contact",
    "Persona",
    "DistributionList",
    "SEND_MEETING_INVITATIONS_CHOICES",
    "SEND_TO_NONE",
    "SEND_ONLY_TO_ALL",
    "SEND_TO_ALL_AND_SAVE_COPY",
    "SEND_MEETING_INVITATIONS_AND_CANCELLATIONS_CHOICES",
    "SEND_ONLY_TO_CHANGED",
    "SEND_TO_CHANGED_AND_SAVE_COPY",
    "SEND_MEETING_CANCELLATIONS_CHOICES",
    "AFFECTED_TASK_OCCURRENCES_CHOICES",
    "ALL_OCCURRENCES",
    "SPECIFIED_OCCURRENCE_ONLY",
    "CONFLICT_RESOLUTION_CHOICES",
    "NEVER_OVERWRITE",
    "AUTO_RESOLVE",
    "ALWAYS_OVERWRITE",
    "DELETE_TYPE_CHOICES",
    "HARD_DELETE",
    "SOFT_DELETE",
    "MOVE_TO_DELETED_ITEMS",
    "BaseItem",
    "Item",
    "BulkCreateResult",
    "Message",
    "ReplyToItem",
    "ReplyAllToItem",
    "ForwardItem",
    "PostItem",
    "PostReplyItem",
    "Task",
    "ITEM_TRAVERSAL_CHOICES",
    "SHALLOW",
    "SOFT_DELETED",
    "ASSOCIATED",
    "SHAPE_CHOICES",
    "ID_ONLY",
    "DEFAULT",
    "ALL_PROPERTIES",
    "SEARCH_SCOPE_CHOICES",
    "ACTIVE_DIRECTORY",
    "ACTIVE_DIRECTORY_CONTACTS",
    "CONTACTS",
    "CONTACTS_ACTIVE_DIRECTORY",
    "ITEM_CLASSES",
]

Sub-modules

exchangelib.items.base
exchangelib.items.calendar_item
exchangelib.items.contact
exchangelib.items.item
exchangelib.items.message
exchangelib.items.post
exchangelib.items.task

Classes

class AcceptItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem

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.

Expand source code
class AcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/acceptitem"""

    ELEMENT_NAME = "AcceptItem"

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class BaseItem (**kwargs)

Base class for all other classes that implement EWS items.

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.

Expand source code
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

Ancestors

Subclasses

Class variables

var FIELDS
var ID_ELEMENT_CLS

'id' and 'changekey' are UUIDs generated by Exchange.

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/itemid

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    item = super().from_xml(elem=elem, account=account)
    item.account = account
    return item

Instance variables

var account

Return an attribute of instance, which is of type owner.

var folder

Return an attribute of instance, which is of type owner.

Inherited members

class BulkCreateResult (**kwargs)

A dummy class to store return values from a CreateItem service call.

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.

Expand source code
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 = []

Ancestors

Class variables

var FIELDS

Instance variables

var attachments

Inherited members

class CalendarItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem

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.

Expand source code
class CalendarItem(Item, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/calendaritem"""

    ELEMENT_NAME = "CalendarItem"

    uid = TextField(field_uri="calendar:UID", is_required_after_save=True, is_searchable=False)
    recurrence_id = DateTimeField(field_uri="calendar:RecurrenceId", is_read_only=True)
    start = DateOrDateTimeField(field_uri="calendar:Start", is_required=True)
    end = DateOrDateTimeField(field_uri="calendar:End", is_required=True)
    original_start = DateTimeField(field_uri="calendar:OriginalStart", is_read_only=True)
    is_all_day = BooleanField(field_uri="calendar:IsAllDayEvent", is_required=True, default=False)
    legacy_free_busy_status = FreeBusyStatusField(
        field_uri="calendar:LegacyFreeBusyStatus", is_required=True, default="Busy"
    )
    location = TextField(field_uri="calendar:Location")
    when = TextField(field_uri="calendar:When")
    is_meeting = BooleanField(field_uri="calendar:IsMeeting", is_read_only=True)
    is_cancelled = BooleanField(field_uri="calendar:IsCancelled", is_read_only=True)
    is_recurring = BooleanField(field_uri="calendar:IsRecurring", is_read_only=True)
    meeting_request_was_sent = BooleanField(field_uri="calendar:MeetingRequestWasSent", is_read_only=True)
    is_response_requested = BooleanField(
        field_uri="calendar:IsResponseRequested", default=None, is_required_after_save=True, is_searchable=False
    )
    type = ChoiceField(
        field_uri="calendar:CalendarItemType", choices={Choice(c) for c in CALENDAR_ITEM_CHOICES}, is_read_only=True
    )
    my_response_type = ChoiceField(
        field_uri="calendar:MyResponseType", choices={Choice(c) for c in Attendee.RESPONSE_TYPES}, is_read_only=True
    )
    organizer = MailboxField(field_uri="calendar:Organizer", is_read_only=True)
    required_attendees = AttendeesField(field_uri="calendar:RequiredAttendees", is_searchable=False)
    optional_attendees = AttendeesField(field_uri="calendar:OptionalAttendees", is_searchable=False)
    resources = AttendeesField(field_uri="calendar:Resources", is_searchable=False)
    conflicting_meeting_count = IntegerField(field_uri="calendar:ConflictingMeetingCount", is_read_only=True)
    adjacent_meeting_count = IntegerField(field_uri="calendar:AdjacentMeetingCount", is_read_only=True)
    conflicting_meetings = EWSElementListField(
        field_uri="calendar:ConflictingMeetings", value_cls="CalendarItem", namespace=Item.NAMESPACE, is_read_only=True
    )
    adjacent_meetings = EWSElementListField(
        field_uri="calendar:AdjacentMeetings", value_cls="CalendarItem", namespace=Item.NAMESPACE, is_read_only=True
    )
    duration = CharField(field_uri="calendar:Duration", is_read_only=True)
    appointment_reply_time = DateTimeField(field_uri="calendar:AppointmentReplyTime", is_read_only=True)
    appointment_sequence_number = IntegerField(field_uri="calendar:AppointmentSequenceNumber", is_read_only=True)
    appointment_state = AppointmentStateField(field_uri="calendar:AppointmentState", is_read_only=True)
    recurrence = RecurrenceField(field_uri="calendar:Recurrence", is_searchable=False)
    first_occurrence = OccurrenceField(
        field_uri="calendar:FirstOccurrence", value_cls=FirstOccurrence, is_read_only=True
    )
    last_occurrence = OccurrenceField(field_uri="calendar:LastOccurrence", value_cls=LastOccurrence, is_read_only=True)
    modified_occurrences = OccurrenceListField(
        field_uri="calendar:ModifiedOccurrences", value_cls=Occurrence, is_read_only=True
    )
    deleted_occurrences = OccurrenceListField(
        field_uri="calendar:DeletedOccurrences", value_cls=DeletedOccurrence, is_read_only=True
    )
    _meeting_timezone = TimeZoneField(
        field_uri="calendar:MeetingTimeZone", deprecated_from=EXCHANGE_2010, is_searchable=False
    )
    _start_timezone = TimeZoneField(
        field_uri="calendar:StartTimeZone", supported_from=EXCHANGE_2010, is_searchable=False
    )
    _end_timezone = TimeZoneField(field_uri="calendar:EndTimeZone", supported_from=EXCHANGE_2010, is_searchable=False)
    conference_type = EnumAsIntField(
        field_uri="calendar:ConferenceType", enum=CONFERENCE_TYPES, min=0, default=None, is_required_after_save=True
    )
    allow_new_time_proposal = BooleanField(
        field_uri="calendar:AllowNewTimeProposal", default=None, is_required_after_save=True, is_searchable=False
    )
    is_online_meeting = BooleanField(field_uri="calendar:IsOnlineMeeting", default=None, is_read_only=True)
    meeting_workspace_url = URIField(field_uri="calendar:MeetingWorkspaceUrl")
    net_show_url = URIField(field_uri="calendar:NetShowUrl")

    def occurrence(self, index):
        """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on a recurring master.

        :param index: The index, which is 1-based

        :return The occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
        )

    def recurring_master(self):
        """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
        Call refresh() on the item do do so.

        Only call this method on an occurrence of a recurring master.

        :return: The master occurrence
        """
        return self.__class__(
            account=self.account,
            folder=self.folder,
            _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
        )

    @classmethod
    def timezone_fields(cls):
        return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))

    def clean_timezone_fields(self, version):
        # Sets proper values on the timezone fields if they are not already set
        if self.start is None:
            start_tz = None
        elif type(self.start) in (EWSDate, datetime.date):
            start_tz = self.account.default_timezone
        else:
            start_tz = self.start.tzinfo
        if self.end is None:
            end_tz = None
        elif type(self.end) in (EWSDate, datetime.date):
            end_tz = self.account.default_timezone
        else:
            end_tz = self.end.tzinfo
        if version.build < EXCHANGE_2010:
            if self._meeting_timezone is None:
                self._meeting_timezone = start_tz
            self._start_timezone = None
            self._end_timezone = None
        else:
            self._meeting_timezone = None
            if self._start_timezone is None:
                self._start_timezone = start_tz
            if self._end_timezone is None:
                self._end_timezone = end_tz

    def clean(self, version=None):
        super().clean(version=version)
        if self.start and self.end and self.end < self.start:
            raise ValueError(f"'end' must be greater than 'start' ({self.start} -> {self.end})")
        if version:
            self.clean_timezone_fields(version=version)

    def cancel(self, **kwargs):
        return CancelCalendarItem(
            account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), **kwargs
        ).send()

    def _update_fieldnames(self):
        update_fields = super()._update_fieldnames()
        if self.type == OCCURRENCE:
            # Some CalendarItem fields cannot be updated when the item is an occurrence. The values are empty when we
            # receive them so would have been updated because they are set to None.
            update_fields.remove("recurrence")
            update_fields.remove("uid")
        return update_fields

    @classmethod
    def from_xml(cls, elem, account):
        item = super().from_xml(elem=elem, account=account)
        # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
        # applicable.
        if not item.is_all_day:
            return item
        for field_name in ("start", "end"):
            val = getattr(item, field_name)
            if val is None:
                continue
            # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
            # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
            if field_name == "end":
                val -= datetime.timedelta(days=1)
            tz = getattr(item, f"_{field_name}_timezone")
            setattr(item, field_name, val.astimezone(tz).date())
        return item

    def tz_field_for_field_name(self, field_name):
        meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
        if self.account.version.build < EXCHANGE_2010:
            return meeting_tz_field
        if field_name == "start":
            return start_tz_field
        if field_name == "end":
            return end_tz_field
        raise ValueError("Unsupported field_name")

    def date_to_datetime(self, field_name):
        # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
        # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
        # start and end values and let EWS apply its logic, but that seems hacky.
        value = getattr(self, field_name)
        tz = getattr(self, self.tz_field_for_field_name(field_name).name)
        value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
        if field_name == "end":
            value += datetime.timedelta(days=1)
        return value

    def to_xml(self, version):
        # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
        # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
        # refers to midnight in the local timezone. See
        #
        # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
        #
        elem = super().to_xml(version=version)
        if not self.is_all_day:
            return elem
        for field_name in ("start", "end"):
            value = getattr(self, field_name)
            if value is None:
                continue
            if type(value) in (EWSDate, datetime.date):
                # EWS always expects a datetime
                value = self.date_to_datetime(field_name=field_name)
                # We already generated an XML element for this field, but it contains a plain date at this point, which
                # is invalid. Replace the value.
                field = self.get_field_by_fieldname(field_name)
                set_xml_value(elem.find(field.response_tag()), value)
        return elem

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Static methods

def from_xml(elem, account)
Expand source code
@classmethod
def from_xml(cls, elem, account):
    item = super().from_xml(elem=elem, account=account)
    # EWS returns the start and end values as a datetime regardless of the is_all_day status. Convert to date if
    # applicable.
    if not item.is_all_day:
        return item
    for field_name in ("start", "end"):
        val = getattr(item, field_name)
        if val is None:
            continue
        # Return just the date part of the value. Subtract 1 day from the date if this is the end field. This is
        # the inverse of what we do in .to_xml(). Convert to the local timezone before getting the date.
        if field_name == "end":
            val -= datetime.timedelta(days=1)
        tz = getattr(item, f"_{field_name}_timezone")
        setattr(item, field_name, val.astimezone(tz).date())
    return item
def timezone_fields()
Expand source code
@classmethod
def timezone_fields(cls):
    return tuple(f for f in cls.FIELDS if isinstance(f, TimeZoneField))

Instance variables

var adjacent_meeting_count
var adjacent_meetings
var allow_new_time_proposal
var appointment_reply_time
var appointment_sequence_number
var appointment_state
var conference_type
var conflicting_meeting_count
var conflicting_meetings
var deleted_occurrences
var duration
var end
var first_occurrence
var is_all_day
var is_cancelled
var is_meeting
var is_online_meeting
var is_recurring
var is_response_requested
var last_occurrence
var legacy_free_busy_status
var location
var meeting_request_was_sent
var meeting_workspace_url
var modified_occurrences
var my_response_type
var net_show_url
var optional_attendees
var organizer
var original_start
var recurrence
var recurrence_id
var required_attendees
var resources
var start
var type
var uid
var when

Methods

def cancel(self, **kwargs)
Expand source code
def cancel(self, **kwargs):
    return CancelCalendarItem(
        account=self.account, reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey), **kwargs
    ).send()
def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.start and self.end and self.end < self.start:
        raise ValueError(f"'end' must be greater than 'start' ({self.start} -> {self.end})")
    if version:
        self.clean_timezone_fields(version=version)
def clean_timezone_fields(self, version)
Expand source code
def clean_timezone_fields(self, version):
    # Sets proper values on the timezone fields if they are not already set
    if self.start is None:
        start_tz = None
    elif type(self.start) in (EWSDate, datetime.date):
        start_tz = self.account.default_timezone
    else:
        start_tz = self.start.tzinfo
    if self.end is None:
        end_tz = None
    elif type(self.end) in (EWSDate, datetime.date):
        end_tz = self.account.default_timezone
    else:
        end_tz = self.end.tzinfo
    if version.build < EXCHANGE_2010:
        if self._meeting_timezone is None:
            self._meeting_timezone = start_tz
        self._start_timezone = None
        self._end_timezone = None
    else:
        self._meeting_timezone = None
        if self._start_timezone is None:
            self._start_timezone = start_tz
        if self._end_timezone is None:
            self._end_timezone = end_tz
def date_to_datetime(self, field_name)
Expand source code
def date_to_datetime(self, field_name):
    # EWS always expects a datetime. If we have a date value, then convert it to datetime in the local
    # timezone. Additionally, if this the end field, add 1 day to the date. We could add 12 hours to both
    # start and end values and let EWS apply its logic, but that seems hacky.
    value = getattr(self, field_name)
    tz = getattr(self, self.tz_field_for_field_name(field_name).name)
    value = EWSDateTime.combine(value, datetime.time(0, 0)).replace(tzinfo=tz)
    if field_name == "end":
        value += datetime.timedelta(days=1)
    return value
def occurrence(self, index)

Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so.

Only call this method on a recurring master.

:param index: The index, which is 1-based

:return The occurrence

Expand source code
def occurrence(self, index):
    """Get an occurrence of a recurring master by index. No query is sent to the server to actually fetch the item.
    Call refresh() on the item do do so.

    Only call this method on a recurring master.

    :param index: The index, which is 1-based

    :return The occurrence
    """
    return self.__class__(
        account=self.account,
        folder=self.folder,
        _id=OccurrenceItemId(id=self.id, changekey=self.changekey, instance_index=index),
    )
def recurring_master(self)

Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item. Call refresh() on the item do do so.

Only call this method on an occurrence of a recurring master.

:return: The master occurrence

Expand source code
def recurring_master(self):
    """Get the recurring master of an occurrence. No query is sent to the server to actually fetch the item.
    Call refresh() on the item do do so.

    Only call this method on an occurrence of a recurring master.

    :return: The master occurrence
    """
    return self.__class__(
        account=self.account,
        folder=self.folder,
        _id=RecurringMasterItemId(id=self.id, changekey=self.changekey),
    )
def to_xml(self, version)
Expand source code
def to_xml(self, version):
    # EWS has some special logic related to all-day start and end values. Non-midnight start values are pushed to
    # the previous midnight. Non-midnight end values are pushed to the following midnight. Midnight in this context
    # refers to midnight in the local timezone. See
    #
    # https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-create-all-day-events-by-using-ews-in-exchange
    #
    elem = super().to_xml(version=version)
    if not self.is_all_day:
        return elem
    for field_name in ("start", "end"):
        value = getattr(self, field_name)
        if value is None:
            continue
        if type(value) in (EWSDate, datetime.date):
            # EWS always expects a datetime
            value = self.date_to_datetime(field_name=field_name)
            # We already generated an XML element for this field, but it contains a plain date at this point, which
            # is invalid. Replace the value.
            field = self.get_field_by_fieldname(field_name)
            set_xml_value(elem.find(field.response_tag()), value)
    return elem
def tz_field_for_field_name(self, field_name)
Expand source code
def tz_field_for_field_name(self, field_name):
    meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
    if self.account.version.build < EXCHANGE_2010:
        return meeting_tz_field
    if field_name == "start":
        return start_tz_field
    if field_name == "end":
        return end_tz_field
    raise ValueError("Unsupported field_name")

Inherited members

class CancelCalendarItem (**kwargs)
Expand source code
class CancelCalendarItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/cancelcalendaritem"""

    ELEMENT_NAME = "CancelCalendarItem"
    author_idx = BaseReplyItem.FIELDS.index_by_name("author")
    FIELDS = BaseReplyItem.FIELDS[:author_idx] + BaseReplyItem.FIELDS[author_idx + 1 :]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var author_idx

Inherited members

class Contact (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact

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.

Expand source code
class Contact(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/contact"""

    ELEMENT_NAME = "Contact"

    file_as = TextField(field_uri="contacts:FileAs")
    file_as_mapping = ChoiceField(
        field_uri="contacts:FileAsMapping",
        choices={
            Choice("None"),
            Choice("LastCommaFirst"),
            Choice("FirstSpaceLast"),
            Choice("Company"),
            Choice("LastCommaFirstCompany"),
            Choice("CompanyLastFirst"),
            Choice("LastFirst"),
            Choice("LastFirstCompany"),
            Choice("CompanyLastCommaFirst"),
            Choice("LastFirstSuffix"),
            Choice("LastSpaceFirstCompany"),
            Choice("CompanyLastSpaceFirst"),
            Choice("LastSpaceFirst"),
            Choice("DisplayName"),
            Choice("FirstName"),
            Choice("LastFirstMiddleSuffix"),
            Choice("LastName"),
            Choice("Empty"),
        },
    )
    display_name = TextField(field_uri="contacts:DisplayName", is_required=True)
    given_name = CharField(field_uri="contacts:GivenName")
    initials = TextField(field_uri="contacts:Initials")
    middle_name = CharField(field_uri="contacts:MiddleName")
    nickname = TextField(field_uri="contacts:Nickname")
    complete_name = EWSElementField(field_uri="contacts:CompleteName", value_cls=CompleteName, is_read_only=True)
    company_name = TextField(field_uri="contacts:CompanyName")
    email_addresses = EmailAddressesField(field_uri="contacts:EmailAddress")
    physical_addresses = PhysicalAddressField(field_uri="contacts:PhysicalAddress")
    phone_numbers = PhoneNumberField(field_uri="contacts:PhoneNumber")
    assistant_name = TextField(field_uri="contacts:AssistantName")
    birthday = DateTimeBackedDateField(field_uri="contacts:Birthday", default_time=datetime.time(11, 59))
    business_homepage = URIField(field_uri="contacts:BusinessHomePage")
    children = TextListField(field_uri="contacts:Children")
    companies = TextListField(field_uri="contacts:Companies", is_searchable=False)
    contact_source = ChoiceField(
        field_uri="contacts:ContactSource", choices={Choice("Store"), Choice("ActiveDirectory")}, is_read_only=True
    )
    department = TextField(field_uri="contacts:Department")
    generation = TextField(field_uri="contacts:Generation")
    im_addresses = CharField(field_uri="contacts:ImAddresses", is_read_only=True)
    job_title = TextField(field_uri="contacts:JobTitle")
    manager = TextField(field_uri="contacts:Manager")
    mileage = TextField(field_uri="contacts:Mileage")
    office = TextField(field_uri="contacts:OfficeLocation")
    postal_address_index = ChoiceField(
        field_uri="contacts:PostalAddressIndex",
        choices={Choice("Business"), Choice("Home"), Choice("Other"), Choice("None")},
        default="None",
        is_required_after_save=True,
    )
    profession = TextField(field_uri="contacts:Profession")
    spouse_name = TextField(field_uri="contacts:SpouseName")
    surname = CharField(field_uri="contacts:Surname")
    wedding_anniversary = DateTimeBackedDateField(
        field_uri="contacts:WeddingAnniversary", default_time=datetime.time(11, 59)
    )
    has_picture = BooleanField(field_uri="contacts:HasPicture", supported_from=EXCHANGE_2010, is_read_only=True)
    phonetic_full_name = TextField(
        field_uri="contacts:PhoneticFullName", supported_from=EXCHANGE_2010_SP2, is_read_only=True
    )
    phonetic_first_name = TextField(
        field_uri="contacts:PhoneticFirstName", supported_from=EXCHANGE_2010_SP2, is_read_only=True
    )
    phonetic_last_name = TextField(
        field_uri="contacts:PhoneticLastName", supported_from=EXCHANGE_2010_SP2, is_read_only=True
    )
    email_alias = EmailAddressField(field_uri="contacts:Alias", is_read_only=True, supported_from=EXCHANGE_2010_SP2)
    # 'notes' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # put entries into the 'notes' form field into the 'body' field.
    notes = CharField(field_uri="contacts:Notes", supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    # 'photo' is documented in MSDN but apparently unused. Writing to it raises ErrorInvalidPropertyRequest. OWA
    # adds photos as FileAttachments on the contact item (with 'is_contact_photo=True'), which automatically flips
    # the 'has_picture' field.
    photo = Base64Field(field_uri="contacts:Photo", supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    user_smime_certificate = Base64Field(
        field_uri="contacts:UserSMIMECertificate", supported_from=EXCHANGE_2010_SP2, is_read_only=True
    )
    ms_exchange_certificate = Base64Field(
        field_uri="contacts:MSExchangeCertificate", supported_from=EXCHANGE_2010_SP2, is_read_only=True
    )
    directory_id = TextField(field_uri="contacts:DirectoryId", supported_from=EXCHANGE_2010_SP2, is_read_only=True)
    manager_mailbox = MailboxField(
        field_uri="contacts:ManagerMailbox", supported_from=EXCHANGE_2010_SP2, is_read_only=True
    )
    direct_reports = MailboxListField(
        field_uri="contacts:DirectReports", supported_from=EXCHANGE_2010_SP2, is_read_only=True
    )

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var assistant_name
var birthday
var business_homepage
var children
var companies
var company_name
var complete_name
var contact_source
var department
var direct_reports
var directory_id
var display_name
var email_addresses
var email_alias
var file_as
var file_as_mapping
var generation
var given_name
var has_picture
var im_addresses
var initials
var job_title
var manager
var manager_mailbox
var middle_name
var mileage
var ms_exchange_certificate
var nickname
var notes
var office
var phone_numbers
var phonetic_first_name
var phonetic_full_name
var phonetic_last_name
var photo
var physical_addresses
var postal_address_index
var profession
var spouse_name
var surname
var user_smime_certificate
var wedding_anniversary

Inherited members

class DeclineItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem

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.

Expand source code
class DeclineItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/declineitem"""

    ELEMENT_NAME = "DeclineItem"

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class DistributionList (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist

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.

Expand source code
class DistributionList(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distributionlist"""

    ELEMENT_NAME = "DistributionList"

    display_name = CharField(field_uri="contacts:DisplayName", is_required=True)
    file_as = CharField(field_uri="contacts:FileAs", is_read_only=True)
    contact_source = ChoiceField(
        field_uri="contacts:ContactSource", choices={Choice("Store"), Choice("ActiveDirectory")}, is_read_only=True
    )
    members = MemberListField(field_uri="distributionlist:Members")

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var contact_source
var display_name
var file_as
var members

Inherited members

class ForwardItem (**kwargs)
Expand source code
class ForwardItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/forwarditem"""

    ELEMENT_NAME = "ForwardItem"

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Item (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item

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.

Expand source code
class Item(BaseItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/item"""

    ELEMENT_NAME = "Item"

    mime_content = MimeContentField(field_uri="item:MimeContent", is_read_only_after_send=True)
    _id = BaseItem.FIELDS["_id"]
    parent_folder_id = EWSElementField(field_uri="item:ParentFolderId", value_cls=ParentFolderId, is_read_only=True)
    item_class = CharField(field_uri="item:ItemClass", is_read_only=True)
    subject = CharField(field_uri="item:Subject")
    sensitivity = ChoiceField(
        field_uri="item:Sensitivity",
        choices={Choice("Normal"), Choice("Personal"), Choice("Private"), Choice("Confidential")},
        is_required=True,
        default="Normal",
    )
    text_body = TextField(field_uri="item:TextBody", is_read_only=True, supported_from=EXCHANGE_2013)
    body = BodyField(field_uri="item:Body")  # Accepts and returns Body or HTMLBody instances
    attachments = AttachmentField(field_uri="item:Attachments")  # ItemAttachment or FileAttachment
    datetime_received = DateTimeField(field_uri="item:DateTimeReceived", is_read_only=True)
    size = IntegerField(field_uri="item:Size", is_read_only=True)  # Item size in bytes
    categories = CharListField(field_uri="item:Categories")
    importance = ChoiceField(
        field_uri="item:Importance",
        choices={Choice("Low"), Choice("Normal"), Choice("High")},
        is_required=True,
        default="Normal",
    )
    in_reply_to = TextField(field_uri="item:InReplyTo")
    is_submitted = BooleanField(field_uri="item:IsSubmitted", is_read_only=True)
    is_draft = BooleanField(field_uri="item:IsDraft", is_read_only=True)
    is_from_me = BooleanField(field_uri="item:IsFromMe", is_read_only=True)
    is_resend = BooleanField(field_uri="item:IsResend", is_read_only=True)
    is_unmodified = BooleanField(field_uri="item:IsUnmodified", is_read_only=True)
    headers = MessageHeaderField(field_uri="item:InternetMessageHeaders", is_read_only=True)
    datetime_sent = DateTimeField(field_uri="item:DateTimeSent", is_read_only=True)
    datetime_created = DateTimeField(field_uri="item:DateTimeCreated", is_read_only=True)
    response_objects = EWSElementField(
        field_uri="item:ResponseObjects",
        value_cls=ResponseObjects,
        is_read_only=True,
    )
    # Placeholder for ResponseObjects
    reminder_due_by = DateTimeField(field_uri="item:ReminderDueBy", is_required_after_save=True, is_searchable=False)
    reminder_is_set = BooleanField(field_uri="item:ReminderIsSet", is_required=True, default=False)
    reminder_minutes_before_start = IntegerField(
        field_uri="item:ReminderMinutesBeforeStart", is_required_after_save=True, min=0, default=0
    )
    display_cc = TextField(field_uri="item:DisplayCc", is_read_only=True)
    display_to = TextField(field_uri="item:DisplayTo", is_read_only=True)
    has_attachments = BooleanField(field_uri="item:HasAttachments", is_read_only=True)
    # ExtendedProperty fields go here
    culture = CultureField(field_uri="item:Culture", is_required_after_save=True, is_searchable=False)
    effective_rights = EffectiveRightsField(field_uri="item:EffectiveRights", is_read_only=True)
    last_modified_name = CharField(field_uri="item:LastModifiedName", is_read_only=True)
    last_modified_time = DateTimeField(field_uri="item:LastModifiedTime", is_read_only=True)
    is_associated = BooleanField(field_uri="item:IsAssociated", is_read_only=True, supported_from=EXCHANGE_2010)
    web_client_read_form_query_string = URIField(
        field_uri="item:WebClientReadFormQueryString", is_read_only=True, supported_from=EXCHANGE_2010
    )
    web_client_edit_form_query_string = URIField(
        field_uri="item:WebClientEditFormQueryString", is_read_only=True, supported_from=EXCHANGE_2010
    )
    conversation_id = EWSElementField(
        field_uri="item:ConversationId", value_cls=ConversationId, is_read_only=True, supported_from=EXCHANGE_2010
    )
    unique_body = BodyField(field_uri="item:UniqueBody", is_read_only=True, supported_from=EXCHANGE_2010)

    FIELDS = Fields()

    # Used to register extended properties
    INSERT_AFTER_FIELD = "has_attachments"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.attachments:
            for a in self.attachments:
                if a.parent_item:
                    if a.parent_item is not self:
                        raise ValueError(f"'parent_item' of attachment {a} must point to this item")
                else:
                    a.parent_item = self
                self.attach(self.attachments)
        else:
            self.attachments = []

    def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
        from .task import Task

        if self.id:
            item_id, changekey = self._update(
                update_fieldnames=update_fields,
                message_disposition=SAVE_ONLY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations,
            )
            if (
                self.id != item_id
                and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId))
                and not isinstance(self, Task)
            ):
                # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
                # the ID of this item changes.
                #
                # When we update certain fields on a task, the ID may change. A full description is available at
                # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task
                raise ValueError("'id' mismatch in returned update response")
            # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact
            self._id = self.ID_ELEMENT_CLS(item_id, changekey)
        else:
            if update_fields:
                raise ValueError("'update_fields' is only valid for updates")
            tmp_attachments = None
            if self.account and self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't save attachments immediately. You need to first
                # save, then attach. Store the attachment of this item temporarily and attach later.
                tmp_attachments, self.attachments = self.attachments, []
            item = self._create(message_disposition=SAVE_ONLY, send_meeting_invitations=send_meeting_invitations)
            self._id = self.ID_ELEMENT_CLS(item.id, item.changekey)
            for old_att, new_att in zip(self.attachments, item.attachments):
                if old_att.attachment_id is not None:
                    raise ValueError("Old 'attachment_id' is not empty")
                if new_att.attachment_id is None:
                    raise ValueError("New 'attachment_id' is empty")
                old_att.attachment_id = new_att.attachment_id
            if tmp_attachments:
                # Exchange 2007 workaround. See above
                self.attach(tmp_attachments)
        return self

    @require_account
    def _create(self, message_disposition, send_meeting_invitations):
        # Return a BulkCreateResult because we want to return the ID of both the main item *and* attachments. In send
        # and send-and-save-copy mode, the server does not return an ID, so we just return True.
        from ..services import CreateItem

        return CreateItem(account=self.account).get(
            items=[self],
            folder=self.folder,
            message_disposition=message_disposition,
            send_meeting_invitations=send_meeting_invitations,
        )

    def _update_fieldnames(self):
        from .contact import Contact, DistributionList

        # Return the list of fields we are allowed to update
        update_fieldnames = []
        for f in self.supported_fields(version=self.account.version):
            if f.name == "attachments":
                # Attachments are handled separately after item creation
                continue
            if f.is_read_only:
                # These cannot be changed
                continue
            if (f.is_required or f.is_required_after_save) and (
                getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
            ):
                # These are required and cannot be deleted
                continue
            if not self.is_draft and f.is_read_only_after_send:
                # These cannot be changed when the item is no longer a draft
                continue
            if f.name == "message_id" and f.is_read_only_after_send:
                # 'message_id' doesn't support updating, no matter the draft status
                continue
            if f.name == "mime_content" and isinstance(self, (Contact, DistributionList)):
                # Contact and DistributionList don't support updating mime_content, no matter the draft status
                continue
            update_fieldnames.append(f.name)
        return update_fieldnames

    @require_account
    def _update(self, update_fieldnames, message_disposition, conflict_resolution, send_meeting_invitations):
        from ..services import UpdateItem

        if not self.changekey:
            raise ValueError(f"{self.__class__.__name__} must have changekey")
        if not update_fieldnames:
            # The fields to update was not specified explicitly. Update all fields where update is possible
            update_fieldnames = self._update_fieldnames()
        return UpdateItem(account=self.account).get(
            items=[(self, update_fieldnames)],
            message_disposition=message_disposition,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations_or_cancellations=send_meeting_invitations,
            suppress_read_receipts=True,
            expect_result=message_disposition != SEND_AND_SAVE_COPY,
        )

    @require_id
    def refresh(self):
        # Updates the item based on fresh data from EWS
        from ..folders import Folder
        from ..services import GetItem

        additional_fields = {
            FieldPath(field=f) for f in Folder(root=self.account.root).allowed_item_fields(version=self.account.version)
        }
        res = GetItem(account=self.account).get(items=[self], additional_fields=additional_fields, shape=ID_ONLY)
        if self.id != res.id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)):
            # When we refresh an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
            # the ID of this item changes.
            raise ValueError("'id' mismatch in returned update response")
        for f in self.FIELDS:
            setattr(self, f.name, getattr(res, f.name))
        # 'parent_item' should point to 'self', not 'fresh_item'. That way, 'fresh_item' can be garbage collected.
        for a in self.attachments:
            a.parent_item = self
        return self

    @require_id
    def copy(self, to_folder):
        from ..services import CopyItem

        # If 'to_folder' is a public folder or a folder in a different mailbox then None is returned
        return CopyItem(account=self.account).get(
            items=[self],
            to_folder=to_folder,
            expect_result=None,
        )

    @require_id
    def move(self, to_folder):
        from ..services import MoveItem

        res = MoveItem(account=self.account).get(
            items=[self],
            to_folder=to_folder,
            expect_result=None,
        )
        if res is None:
            # Assume 'to_folder' is a public folder or a folder in a different mailbox
            self._id = None
            return
        self._id = self.ID_ELEMENT_CLS(*res)
        self.folder = to_folder

    def move_to_trash(
        self,
        send_meeting_cancellations=SEND_TO_NONE,
        affected_task_occurrences=ALL_OCCURRENCES,
        suppress_read_receipts=True,
    ):
        # Delete and move to the trash folder.
        self._delete(
            delete_type=MOVE_TO_DELETED_ITEMS,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )
        self._id = None
        self.folder = self.account.trash

    def soft_delete(
        self,
        send_meeting_cancellations=SEND_TO_NONE,
        affected_task_occurrences=ALL_OCCURRENCES,
        suppress_read_receipts=True,
    ):
        # Delete and move to the dumpster, if it is enabled.
        self._delete(
            delete_type=SOFT_DELETE,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )
        self._id = None
        self.folder = self.account.recoverable_items_deletions

    def delete(
        self,
        send_meeting_cancellations=SEND_TO_NONE,
        affected_task_occurrences=ALL_OCCURRENCES,
        suppress_read_receipts=True,
    ):
        # Remove the item permanently. No copies are stored anywhere.
        self._delete(
            delete_type=HARD_DELETE,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )
        self._id, self.folder = None, None

    @require_id
    def _delete(self, delete_type, send_meeting_cancellations, affected_task_occurrences, suppress_read_receipts):
        from ..services import DeleteItem

        DeleteItem(account=self.account).get(
            items=[self],
            delete_type=delete_type,
            send_meeting_cancellations=send_meeting_cancellations,
            affected_task_occurrences=affected_task_occurrences,
            suppress_read_receipts=suppress_read_receipts,
        )

    @require_id
    def archive(self, to_folder):
        from ..services import ArchiveItem

        return ArchiveItem(account=self.account).get(items=[self], to_folder=to_folder, expect_result=True)

    def attach(self, attachments):
        """Add an attachment, or a list of attachments, to this item. If the item has already been saved, the
        attachments will be created on the server immediately. If the item has not yet been saved, the attachments will
        be created on the server when the item is saved.

        Adding attachments to an existing item will update the changekey of the item.

        :param attachments:
        """
        if not is_iterable(attachments, generators_allowed=True):
            attachments = [attachments]
        for a in attachments:
            if not a.parent_item:
                a.parent_item = self
            if self.id and not a.attachment_id:
                # Already saved object. Attach the attachment server-side now
                a.attach()
            if a not in self.attachments:
                self.attachments.append(a)

    def detach(self, attachments):
        """Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the
        attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will
        simply not be created on the server the item is saved.

        Removing attachments from an existing item will update the changekey of the item.

        :param attachments:
        """
        if not is_iterable(attachments, generators_allowed=True):
            attachments = [attachments]
        if attachments is self.attachments:
            # Don't remove from the same list we are iterating
            attachments = list(attachments)
        for a in attachments:
            if a.parent_item is not self:
                raise ValueError("Attachment does not belong to this item")
            if self.id:
                # Item is already created. Detach  the attachment server-side now
                a.detach()
            if a in self.attachments:
                self.attachments.remove(a)

    @require_id
    def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
        from .message import ForwardItem

        return ForwardItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
        self.create_forward(
            subject,
            body,
            to_recipients,
            cc_recipients,
            bcc_recipients,
        ).send()

Ancestors

Subclasses

Class variables

var ELEMENT_NAME
var FIELDS
var INSERT_AFTER_FIELD

Instance variables

var attachments
var body
var categories
var conversation_id
var culture
var datetime_created
var datetime_received
var datetime_sent
var display_cc
var display_to
var effective_rights
var has_attachments
var headers
var importance
var in_reply_to
var is_associated
var is_draft
var is_from_me
var is_resend
var is_submitted
var is_unmodified
var item_class
var last_modified_name
var last_modified_time
var mime_content
var parent_folder_id
var reminder_due_by
var reminder_is_set
var reminder_minutes_before_start
var response_objects
var sensitivity
var size
var subject
var text_body
var unique_body
var web_client_edit_form_query_string
var web_client_read_form_query_string

Methods

def archive(self, to_folder)
Expand source code
@require_id
def archive(self, to_folder):
    from ..services import ArchiveItem

    return ArchiveItem(account=self.account).get(items=[self], to_folder=to_folder, expect_result=True)
def attach(self, attachments)

Add an attachment, or a list of attachments, to this item. If the item has already been saved, the attachments will be created on the server immediately. If the item has not yet been saved, the attachments will be created on the server when the item is saved.

Adding attachments to an existing item will update the changekey of the item.

:param attachments:

Expand source code
def attach(self, attachments):
    """Add an attachment, or a list of attachments, to this item. If the item has already been saved, the
    attachments will be created on the server immediately. If the item has not yet been saved, the attachments will
    be created on the server when the item is saved.

    Adding attachments to an existing item will update the changekey of the item.

    :param attachments:
    """
    if not is_iterable(attachments, generators_allowed=True):
        attachments = [attachments]
    for a in attachments:
        if not a.parent_item:
            a.parent_item = self
        if self.id and not a.attachment_id:
            # Already saved object. Attach the attachment server-side now
            a.attach()
        if a not in self.attachments:
            self.attachments.append(a)
def copy(self, to_folder)
Expand source code
@require_id
def copy(self, to_folder):
    from ..services import CopyItem

    # If 'to_folder' is a public folder or a folder in a different mailbox then None is returned
    return CopyItem(account=self.account).get(
        items=[self],
        to_folder=to_folder,
        expect_result=None,
    )
def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None)
Expand source code
@require_id
def create_forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
    from .message import ForwardItem

    return ForwardItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=cc_recipients,
        bcc_recipients=bcc_recipients,
    )
def delete(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def delete(
    self,
    send_meeting_cancellations=SEND_TO_NONE,
    affected_task_occurrences=ALL_OCCURRENCES,
    suppress_read_receipts=True,
):
    # Remove the item permanently. No copies are stored anywhere.
    self._delete(
        delete_type=HARD_DELETE,
        send_meeting_cancellations=send_meeting_cancellations,
        affected_task_occurrences=affected_task_occurrences,
        suppress_read_receipts=suppress_read_receipts,
    )
    self._id, self.folder = None, None
def detach(self, attachments)

Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will simply not be created on the server the item is saved.

Removing attachments from an existing item will update the changekey of the item.

:param attachments:

Expand source code
def detach(self, attachments):
    """Remove an attachment, or a list of attachments, from this item. If the item has already been saved, the
    attachments will be deleted on the server immediately. If the item has not yet been saved, the attachments will
    simply not be created on the server the item is saved.

    Removing attachments from an existing item will update the changekey of the item.

    :param attachments:
    """
    if not is_iterable(attachments, generators_allowed=True):
        attachments = [attachments]
    if attachments is self.attachments:
        # Don't remove from the same list we are iterating
        attachments = list(attachments)
    for a in attachments:
        if a.parent_item is not self:
            raise ValueError("Attachment does not belong to this item")
        if self.id:
            # Item is already created. Detach  the attachment server-side now
            a.detach()
        if a in self.attachments:
            self.attachments.remove(a)
def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None)
Expand source code
def forward(self, subject, body, to_recipients, cc_recipients=None, bcc_recipients=None):
    self.create_forward(
        subject,
        body,
        to_recipients,
        cc_recipients,
        bcc_recipients,
    ).send()
def move(self, to_folder)
Expand source code
@require_id
def move(self, to_folder):
    from ..services import MoveItem

    res = MoveItem(account=self.account).get(
        items=[self],
        to_folder=to_folder,
        expect_result=None,
    )
    if res is None:
        # Assume 'to_folder' is a public folder or a folder in a different mailbox
        self._id = None
        return
    self._id = self.ID_ELEMENT_CLS(*res)
    self.folder = to_folder
def move_to_trash(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def move_to_trash(
    self,
    send_meeting_cancellations=SEND_TO_NONE,
    affected_task_occurrences=ALL_OCCURRENCES,
    suppress_read_receipts=True,
):
    # Delete and move to the trash folder.
    self._delete(
        delete_type=MOVE_TO_DELETED_ITEMS,
        send_meeting_cancellations=send_meeting_cancellations,
        affected_task_occurrences=affected_task_occurrences,
        suppress_read_receipts=suppress_read_receipts,
    )
    self._id = None
    self.folder = self.account.trash
def refresh(self)
Expand source code
@require_id
def refresh(self):
    # Updates the item based on fresh data from EWS
    from ..folders import Folder
    from ..services import GetItem

    additional_fields = {
        FieldPath(field=f) for f in Folder(root=self.account.root).allowed_item_fields(version=self.account.version)
    }
    res = GetItem(account=self.account).get(items=[self], additional_fields=additional_fields, shape=ID_ONLY)
    if self.id != res.id and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId)):
        # When we refresh an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
        # the ID of this item changes.
        raise ValueError("'id' mismatch in returned update response")
    for f in self.FIELDS:
        setattr(self, f.name, getattr(res, f.name))
    # 'parent_item' should point to 'self', not 'fresh_item'. That way, 'fresh_item' can be garbage collected.
    for a in self.attachments:
        a.parent_item = self
    return self
def save(self, update_fields=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
def save(self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE):
    from .task import Task

    if self.id:
        item_id, changekey = self._update(
            update_fieldnames=update_fields,
            message_disposition=SAVE_ONLY,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations=send_meeting_invitations,
        )
        if (
            self.id != item_id
            and not isinstance(self._id, (OccurrenceItemId, RecurringMasterItemId))
            and not isinstance(self, Task)
        ):
            # When we update an item with an OccurrenceItemId as ID, EWS returns the ID of the occurrence, so
            # the ID of this item changes.
            #
            # When we update certain fields on a task, the ID may change. A full description is available at
            # https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateitem-operation-task
            raise ValueError("'id' mismatch in returned update response")
        # Don't check that changekeys are different. No-op saves will sometimes leave the changekey intact
        self._id = self.ID_ELEMENT_CLS(item_id, changekey)
    else:
        if update_fields:
            raise ValueError("'update_fields' is only valid for updates")
        tmp_attachments = None
        if self.account and self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't save attachments immediately. You need to first
            # save, then attach. Store the attachment of this item temporarily and attach later.
            tmp_attachments, self.attachments = self.attachments, []
        item = self._create(message_disposition=SAVE_ONLY, send_meeting_invitations=send_meeting_invitations)
        self._id = self.ID_ELEMENT_CLS(item.id, item.changekey)
        for old_att, new_att in zip(self.attachments, item.attachments):
            if old_att.attachment_id is not None:
                raise ValueError("Old 'attachment_id' is not empty")
            if new_att.attachment_id is None:
                raise ValueError("New 'attachment_id' is empty")
            old_att.attachment_id = new_att.attachment_id
        if tmp_attachments:
            # Exchange 2007 workaround. See above
            self.attach(tmp_attachments)
    return self
def soft_delete(self, send_meeting_cancellations='SendToNone', affected_task_occurrences='AllOccurrences', suppress_read_receipts=True)
Expand source code
def soft_delete(
    self,
    send_meeting_cancellations=SEND_TO_NONE,
    affected_task_occurrences=ALL_OCCURRENCES,
    suppress_read_receipts=True,
):
    # Delete and move to the dumpster, if it is enabled.
    self._delete(
        delete_type=SOFT_DELETE,
        send_meeting_cancellations=send_meeting_cancellations,
        affected_task_occurrences=affected_task_occurrences,
        suppress_read_receipts=suppress_read_receipts,
    )
    self._id = None
    self.folder = self.account.recoverable_items_deletions

Inherited members

class MeetingCancellation (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingcancellation

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.

Expand source code
class MeetingCancellation(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingcancellation"""

    ELEMENT_NAME = "MeetingCancellation"

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class MeetingRequest (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingrequest

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.

Expand source code
class MeetingRequest(BaseMeetingItem, AcceptDeclineMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingrequest"""

    ELEMENT_NAME = "MeetingRequest"

    meeting_request_type = ChoiceField(
        field_uri="meetingRequest:MeetingRequestType",
        choices={
            Choice("FullUpdate"),
            Choice("InformationalUpdate"),
            Choice("NewMeetingRequest"),
            Choice("None"),
            Choice("Outdated"),
            Choice("PrincipalWantsCopy"),
            Choice("SilentUpdate"),
        },
        default="None",
    )
    intended_free_busy_status = ChoiceField(
        field_uri="meetingRequest:IntendedFreeBusyStatus",
        choices={Choice("Free"), Choice("Tentative"), Choice("Busy"), Choice("OOF"), Choice("NoData")},
        is_required=True,
        default="Busy",
    )

    # This element also has some fields from CalendarItem
    start_idx = CalendarItem.FIELDS.index_by_name("start")
    is_response_requested_idx = CalendarItem.FIELDS.index_by_name("is_response_requested")
    FIELDS = (
        BaseMeetingItem.FIELDS
        + CalendarItem.FIELDS[start_idx:is_response_requested_idx]
        + CalendarItem.FIELDS[is_response_requested_idx + 1 :]
    )

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var is_response_requested_idx
var start_idx

Instance variables

var adjacent_meeting_count
var adjacent_meetings
var allow_new_time_proposal
var appointment_reply_time
var appointment_sequence_number
var appointment_state
var conference_type
var conflicting_meeting_count
var conflicting_meetings
var deleted_occurrences
var duration
var end
var first_occurrence
var intended_free_busy_status
var is_all_day
var is_cancelled
var is_meeting
var is_online_meeting
var is_recurring
var last_occurrence
var legacy_free_busy_status
var location
var meeting_request_type
var meeting_request_was_sent
var meeting_workspace_url
var modified_occurrences
var my_response_type
var net_show_url
var optional_attendees
var organizer
var original_start
var recurrence
var required_attendees
var resources
var start
var type
var when

Inherited members

class MeetingResponse (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingresponse

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.

Expand source code
class MeetingResponse(BaseMeetingItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/meetingresponse"""

    ELEMENT_NAME = "MeetingResponse"

    proposed_start = DateTimeField(field_uri="meeting:ProposedStart", supported_from=EXCHANGE_2013)
    proposed_end = DateTimeField(field_uri="meeting:ProposedEnd", supported_from=EXCHANGE_2013)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var proposed_end
var proposed_start

Inherited members

class Message (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref

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.

Expand source code
class Message(Item):
    """MSDN:
    https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/message-ex15websvcsotherref
    """

    ELEMENT_NAME = "Message"

    sender = MailboxField(field_uri="message:Sender", is_read_only=True, is_read_only_after_send=True)
    to_recipients = MailboxListField(
        field_uri="message:ToRecipients", is_read_only_after_send=True, is_searchable=False
    )
    cc_recipients = MailboxListField(
        field_uri="message:CcRecipients", is_read_only_after_send=True, is_searchable=False
    )
    bcc_recipients = MailboxListField(
        field_uri="message:BccRecipients", is_read_only_after_send=True, is_searchable=False
    )
    is_read_receipt_requested = BooleanField(
        field_uri="message:IsReadReceiptRequested", is_required=True, default=False, is_read_only_after_send=True
    )
    is_delivery_receipt_requested = BooleanField(
        field_uri="message:IsDeliveryReceiptRequested", is_required=True, default=False, is_read_only_after_send=True
    )
    conversation_index = Base64Field(field_uri="message:ConversationIndex", is_read_only=True)
    conversation_topic = CharField(field_uri="message:ConversationTopic", is_read_only=True)
    # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword.
    author = MailboxField(field_uri="message:From", is_read_only_after_send=True)
    message_id = TextField(field_uri="message:InternetMessageId", is_read_only_after_send=True)
    is_read = BooleanField(field_uri="message:IsRead", is_required=True, default=False)
    is_response_requested = BooleanField(field_uri="message:IsResponseRequested", default=False, is_required=True)
    references = TextField(field_uri="message:References")
    reply_to = MailboxListField(field_uri="message:ReplyTo", is_read_only_after_send=True, is_searchable=False)
    received_by = MailboxField(field_uri="message:ReceivedBy", is_read_only=True)
    received_representing = MailboxField(field_uri="message:ReceivedRepresenting", is_read_only=True)
    reminder_message_data = EWSElementField(
        field_uri="message:ReminderMessageData",
        value_cls=ReminderMessageData,
        supported_from=EXCHANGE_2013_SP1,
        is_read_only=True,
    )

    @require_account
    def send(
        self,
        save_copy=True,
        copy_to_folder=None,
        conflict_resolution=AUTO_RESOLVE,
        send_meeting_invitations=SEND_TO_NONE,
    ):
        from ..services import SendItem

        # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
        # not yet exist in EWS.
        if copy_to_folder and not save_copy:
            raise AttributeError("'save_copy' must be True when 'copy_to_folder' is set")
        if save_copy and not copy_to_folder:
            copy_to_folder = self.account.sent  # 'Sent' is default EWS behaviour
        if self.id:
            SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
            # The item will be deleted from the original folder
            self._id = None
            self.folder = copy_to_folder
            return None

        # New message
        if copy_to_folder:
            # This would better be done via send_and_save() but lets just support it here
            self.folder = copy_to_folder
            return self.send_and_save(
                conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations
            )

        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
            # then attach, then send. This is done in send_and_save(). send() will delete the item again.
            self.send_and_save(
                conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations
            )
            return None

        self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
        return None

    def send_and_save(
        self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE
    ):
        # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
        if self.id:
            self._update(
                update_fieldnames=update_fields,
                message_disposition=SEND_AND_SAVE_COPY,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations,
            )
        else:
            if self.account.version.build < EXCHANGE_2013 and self.attachments:
                # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
                # to first save, then attach, then send. This is done in save().
                self.save(
                    update_fields=update_fields,
                    conflict_resolution=conflict_resolution,
                    send_meeting_invitations=send_meeting_invitations,
                )
                self.send(
                    save_copy=False,
                    conflict_resolution=conflict_resolution,
                    send_meeting_invitations=send_meeting_invitations,
                )
            else:
                self._create(message_disposition=SEND_AND_SAVE_COPY, send_meeting_invitations=send_meeting_invitations)

    @require_id
    def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        if to_recipients is None:
            if not self.author:
                raise ValueError("'to_recipients' must be set when message has no 'author'")
            to_recipients = [self.author]
        return ReplyToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=cc_recipients,
            bcc_recipients=bcc_recipients,
        )

    def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
        self.create_reply(subject, body, to_recipients, cc_recipients, bcc_recipients).send()

    @require_id
    def create_reply_all(self, subject, body):
        to_recipients = list(self.to_recipients) if self.to_recipients else []
        if self.author:
            to_recipients.append(self.author)
        return ReplyAllToItem(
            account=self.account,
            reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
            subject=subject,
            new_body=body,
            to_recipients=to_recipients,
            cc_recipients=self.cc_recipients,
            bcc_recipients=self.bcc_recipients,
        )

    def reply_all(self, subject, body):
        self.create_reply_all(subject, body).send()

    def mark_as_junk(self, is_junk=True, move_item=True):
        """Mark or un-marks items as junk email.

        :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
        removed.
        :param move_item: If true, the item will be moved to the junk folder.
        :return:
        """
        from ..services import MarkAsJunk

        res = MarkAsJunk(account=self.account).get(
            items=[self], is_junk=is_junk, move_item=move_item, expect_result=None
        )
        if res is None:
            return
        self.folder = self.account.junk if is_junk else self.account.inbox
        self.id, self.changekey = res

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var author
var bcc_recipients
var cc_recipients
var conversation_index
var conversation_topic
var is_delivery_receipt_requested
var is_read
var is_read_receipt_requested
var is_response_requested
var message_id
var received_by
var received_representing
var references
var reminder_message_data
var reply_to
var sender
var to_recipients

Methods

def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None)
Expand source code
@require_id
def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
    if to_recipients is None:
        if not self.author:
            raise ValueError("'to_recipients' must be set when message has no 'author'")
        to_recipients = [self.author]
    return ReplyToItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=cc_recipients,
        bcc_recipients=bcc_recipients,
    )
def create_reply_all(self, subject, body)
Expand source code
@require_id
def create_reply_all(self, subject, body):
    to_recipients = list(self.to_recipients) if self.to_recipients else []
    if self.author:
        to_recipients.append(self.author)
    return ReplyAllToItem(
        account=self.account,
        reference_item_id=ReferenceItemId(id=self.id, changekey=self.changekey),
        subject=subject,
        new_body=body,
        to_recipients=to_recipients,
        cc_recipients=self.cc_recipients,
        bcc_recipients=self.bcc_recipients,
    )
def mark_as_junk(self, is_junk=True, move_item=True)

Mark or un-marks items as junk email.

:param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be removed. :param move_item: If true, the item will be moved to the junk folder. :return:

Expand source code
def mark_as_junk(self, is_junk=True, move_item=True):
    """Mark or un-marks items as junk email.

    :param is_junk: If True, the sender will be added from the blocked sender list. Otherwise, the sender will be
    removed.
    :param move_item: If true, the item will be moved to the junk folder.
    :return:
    """
    from ..services import MarkAsJunk

    res = MarkAsJunk(account=self.account).get(
        items=[self], is_junk=is_junk, move_item=move_item, expect_result=None
    )
    if res is None:
        return
    self.folder = self.account.junk if is_junk else self.account.inbox
    self.id, self.changekey = res
def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None)
Expand source code
def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None):
    self.create_reply(subject, body, to_recipients, cc_recipients, bcc_recipients).send()
def reply_all(self, subject, body)
Expand source code
def reply_all(self, subject, body):
    self.create_reply_all(subject, body).send()
def send(self, save_copy=True, copy_to_folder=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
@require_account
def send(
    self,
    save_copy=True,
    copy_to_folder=None,
    conflict_resolution=AUTO_RESOLVE,
    send_meeting_invitations=SEND_TO_NONE,
):
    from ..services import SendItem

    # Only sends a message. The message can either be an existing draft stored in EWS or a new message that does
    # not yet exist in EWS.
    if copy_to_folder and not save_copy:
        raise AttributeError("'save_copy' must be True when 'copy_to_folder' is set")
    if save_copy and not copy_to_folder:
        copy_to_folder = self.account.sent  # 'Sent' is default EWS behaviour
    if self.id:
        SendItem(account=self.account).get(items=[self], saved_item_folder=copy_to_folder)
        # The item will be deleted from the original folder
        self._id = None
        self.folder = copy_to_folder
        return None

    # New message
    if copy_to_folder:
        # This would better be done via send_and_save() but lets just support it here
        self.folder = copy_to_folder
        return self.send_and_save(
            conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations
        )

    if self.account.version.build < EXCHANGE_2013 and self.attachments:
        # At least some versions prior to Exchange 2013 can't send attachments immediately. You need to first save,
        # then attach, then send. This is done in send_and_save(). send() will delete the item again.
        self.send_and_save(
            conflict_resolution=conflict_resolution, send_meeting_invitations=send_meeting_invitations
        )
        return None

    self._create(message_disposition=SEND_ONLY, send_meeting_invitations=send_meeting_invitations)
    return None
def send_and_save(self, update_fields=None, conflict_resolution='AutoResolve', send_meeting_invitations='SendToNone')
Expand source code
def send_and_save(
    self, update_fields=None, conflict_resolution=AUTO_RESOLVE, send_meeting_invitations=SEND_TO_NONE
):
    # Sends Message and saves a copy in the parent folder. Does not return an ItemId.
    if self.id:
        self._update(
            update_fieldnames=update_fields,
            message_disposition=SEND_AND_SAVE_COPY,
            conflict_resolution=conflict_resolution,
            send_meeting_invitations=send_meeting_invitations,
        )
    else:
        if self.account.version.build < EXCHANGE_2013 and self.attachments:
            # At least some versions prior to Exchange 2013 can't send-and-save attachments immediately. You need
            # to first save, then attach, then send. This is done in save().
            self.save(
                update_fields=update_fields,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations,
            )
            self.send(
                save_copy=False,
                conflict_resolution=conflict_resolution,
                send_meeting_invitations=send_meeting_invitations,
            )
        else:
            self._create(message_disposition=SEND_AND_SAVE_COPY, send_meeting_invitations=send_meeting_invitations)

Inherited members

class Persona (**kwargs)
Expand source code
class Persona(IdChangeKeyMixIn):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/persona"""

    ELEMENT_NAME = "Persona"
    ID_ELEMENT_CLS = PersonaId

    _id = IdElementField(field_uri="persona:PersonaId", value_cls=ID_ELEMENT_CLS, namespace=TNS)
    persona_type = CharField(field_uri="persona:PersonaType")
    persona_object_type = TextField(field_uri="persona:PersonaObjectStatus")
    creation_time = DateTimeField(field_uri="persona:CreationTime")
    bodies = BodyContentAttributedValueField(field_uri="persona:Bodies")
    display_name_first_last_sort_key = TextField(field_uri="persona:DisplayNameFirstLastSortKey")
    display_name_last_first_sort_key = TextField(field_uri="persona:DisplayNameLastFirstSortKey")
    company_sort_key = TextField(field_uri="persona:CompanyNameSortKey")
    home_sort_key = TextField(field_uri="persona:HomeCitySortKey")
    work_city_sort_key = TextField(field_uri="persona:WorkCitySortKey")
    display_name_first_last_header = CharField(field_uri="persona:DisplayNameFirstLastHeader")
    display_name_last_first_header = CharField(field_uri="persona:DisplayNameLastFirstHeader")
    file_as_header = TextField(field_uri="persona:FileAsHeader")
    display_name = CharField(field_uri="persona:DisplayName")
    display_name_first_last = CharField(field_uri="persona:DisplayNameFirstLast")
    display_name_last_first = CharField(field_uri="persona:DisplayNameLastFirst")
    file_as = CharField(field_uri="persona:FileAs")
    file_as_id = TextField(field_uri="persona:FileAsId")
    display_name_prefix = CharField(field_uri="persona:DisplayNamePrefix")
    given_name = CharField(field_uri="persona:GivenName")
    middle_name = CharField(field_uri="persona:MiddleName")
    surname = CharField(field_uri="persona:Surname")
    generation = CharField(field_uri="persona:Generation")
    nickname = TextField(field_uri="persona:Nickname")
    yomi_company_name = TextField(field_uri="persona:YomiCompanyName")
    yomi_first_name = TextField(field_uri="persona:YomiFirstName")
    yomi_last_name = TextField(field_uri="persona:YomiLastName")
    title = CharField(field_uri="persona:Title")
    department = TextField(field_uri="persona:Department")
    company_name = CharField(field_uri="persona:CompanyName")
    email_address = EWSElementField(field_uri="persona:EmailAddress", value_cls=EmailAddress)
    email_addresses = EWSElementListField(field_uri="persona:EmailAddresses", value_cls=Address)
    PhoneNumber = PersonaPhoneNumberField(field_uri="persona:PhoneNumber")
    im_address = CharField(field_uri="persona:ImAddress")
    home_city = CharField(field_uri="persona:HomeCity")
    work_city = CharField(field_uri="persona:WorkCity")
    relevance_score = CharField(field_uri="persona:RelevanceScore")
    folder_ids = EWSElementListField(field_uri="persona:FolderIds", value_cls=FolderId)
    attributions = EWSElementListField(field_uri="persona:Attributions", value_cls=Attribution)
    display_names = StringAttributedValueField(field_uri="persona:DisplayNames")
    file_ases = StringAttributedValueField(field_uri="persona:FileAses")
    file_as_ids = StringAttributedValueField(field_uri="persona:FileAsIds")
    display_name_prefixes = StringAttributedValueField(field_uri="persona:DisplayNamePrefixes")
    given_names = StringAttributedValueField(field_uri="persona:GivenNames")
    middle_names = StringAttributedValueField(field_uri="persona:MiddleNames")
    surnames = StringAttributedValueField(field_uri="persona:Surnames")
    generations = StringAttributedValueField(field_uri="persona:Generations")
    nicknames = StringAttributedValueField(field_uri="persona:Nicknames")
    initials = StringAttributedValueField(field_uri="persona:Initials")
    yomi_company_names = StringAttributedValueField(field_uri="persona:YomiCompanyNames")
    yomi_first_names = StringAttributedValueField(field_uri="persona:YomiFirstNames")
    yomi_last_names = StringAttributedValueField(field_uri="persona:YomiLastNames")
    business_phone_numbers = PhoneNumberAttributedValueField(field_uri="persona:BusinessPhoneNumbers")
    business_phone_numbers2 = PhoneNumberAttributedValueField(field_uri="persona:BusinessPhoneNumbers2")
    home_phones = PhoneNumberAttributedValueField(field_uri="persona:HomePhones")
    home_phones2 = PhoneNumberAttributedValueField(field_uri="persona:HomePhones2")
    mobile_phones = PhoneNumberAttributedValueField(field_uri="persona:MobilePhones")
    mobile_phones2 = PhoneNumberAttributedValueField(field_uri="persona:MobilePhones2")
    assistant_phone_numbers = PhoneNumberAttributedValueField(field_uri="persona:AssistantPhoneNumbers")
    callback_phones = PhoneNumberAttributedValueField(field_uri="persona:CallbackPhones")
    car_phones = PhoneNumberAttributedValueField(field_uri="persona:CarPhones")
    home_faxes = PhoneNumberAttributedValueField(field_uri="persona:HomeFaxes")
    organization_main_phones = PhoneNumberAttributedValueField(field_uri="persona:OrganizationMainPhones")
    other_faxes = PhoneNumberAttributedValueField(field_uri="persona:OtherFaxes")
    other_telephones = PhoneNumberAttributedValueField(field_uri="persona:OtherTelephones")
    other_phones2 = PhoneNumberAttributedValueField(field_uri="persona:OtherPhones2")
    pagers = PhoneNumberAttributedValueField(field_uri="persona:Pagers")
    radio_phones = PhoneNumberAttributedValueField(field_uri="persona:RadioPhones")
    telex_numbers = PhoneNumberAttributedValueField(field_uri="persona:TelexNumbers")
    tty_tdd_phone_numbers = PhoneNumberAttributedValueField(field_uri="persona:TTYTDDPhoneNumbers")
    work_faxes = PhoneNumberAttributedValueField(field_uri="persona:WorkFaxes")
    emails1 = EmailAddressAttributedValueField(field_uri="persona:Emails1")
    emails2 = EmailAddressAttributedValueField(field_uri="persona:Emails2")
    emails3 = EmailAddressAttributedValueField(field_uri="persona:Emails3")
    business_home_pages = StringAttributedValueField(field_uri="persona:BusinessHomePages")
    personal_home_pages = StringAttributedValueField(field_uri="persona:PersonalHomePages")
    office_locations = StringAttributedValueField(field_uri="persona:OfficeLocations")
    im_addresses = StringAttributedValueField(field_uri="persona:ImAddresses")
    im_addresses2 = StringAttributedValueField(field_uri="persona:ImAddresses2")
    im_addresses3 = StringAttributedValueField(field_uri="persona:ImAddresses3")
    business_addresses = PostalAddressAttributedValueField(field_uri="persona:BusinessAddresses")
    home_addresses = PostalAddressAttributedValueField(field_uri="persona:HomeAddresses")
    other_addresses = PostalAddressAttributedValueField(field_uri="persona:OtherAddresses")
    titles = StringAttributedValueField(field_uri="persona:Titles")
    departments = StringAttributedValueField(field_uri="persona:Departments")
    company_names = StringAttributedValueField(field_uri="persona:CompanyNames")
    managers = StringAttributedValueField(field_uri="persona:Managers")
    assistant_names = StringAttributedValueField(field_uri="persona:AssistantNames")
    professions = StringAttributedValueField(field_uri="persona:Professions")
    spouse_names = StringAttributedValueField(field_uri="persona:SpouseNames")
    children = StringAttributedValueField(field_uri="persona:Children")
    schools = StringAttributedValueField(field_uri="persona:Schools")
    hobbies = StringAttributedValueField(field_uri="persona:Hobbies")
    wedding_anniversaries = StringAttributedValueField(field_uri="persona:WeddingAnniversaries")
    birthdays = StringAttributedValueField(field_uri="persona:Birthdays")
    locations = StringAttributedValueField(field_uri="persona:Locations")
    # This class has an additional field of type "ExtendedPropertyAttributedValueField" and
    # field_uri 'persona:ExtendedProperties'

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var ID_ELEMENT_CLS

Instance variables

var PhoneNumber
var assistant_names
var assistant_phone_numbers
var attributions
var birthdays
var bodies
var business_addresses
var business_home_pages
var business_phone_numbers
var business_phone_numbers2
var callback_phones
var car_phones
var children
var company_name
var company_names
var company_sort_key
var creation_time
var department
var departments
var display_name
var display_name_first_last
var display_name_first_last_header
var display_name_first_last_sort_key
var display_name_last_first
var display_name_last_first_header
var display_name_last_first_sort_key
var display_name_prefix
var display_name_prefixes
var display_names
var email_address
var email_addresses
var emails1
var emails2
var emails3
var file_as
var file_as_header
var file_as_id
var file_as_ids
var file_ases
var folder_ids
var generation
var generations
var given_name
var given_names
var hobbies
var home_addresses
var home_city
var home_faxes
var home_phones
var home_phones2
var home_sort_key
var im_address
var im_addresses
var im_addresses2
var im_addresses3
var initials
var locations
var managers
var middle_name
var middle_names
var mobile_phones
var mobile_phones2
var nickname
var nicknames
var office_locations
var organization_main_phones
var other_addresses
var other_faxes
var other_phones2
var other_telephones
var pagers
var persona_object_type
var persona_type
var personal_home_pages
var professions
var radio_phones
var relevance_score
var schools
var spouse_names
var surname
var surnames
var telex_numbers
var title
var titles
var tty_tdd_phone_numbers
var wedding_anniversaries
var work_city
var work_city_sort_key
var work_faxes
var yomi_company_name
var yomi_company_names
var yomi_first_name
var yomi_first_names
var yomi_last_name
var yomi_last_names

Inherited members

class PostItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem

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.

Expand source code
class PostItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postitem"""

    ELEMENT_NAME = "PostItem"

    conversation_index = Message.FIELDS["conversation_index"]
    conversation_topic = Message.FIELDS["conversation_topic"]

    author = Message.FIELDS["author"]
    message_id = Message.FIELDS["message_id"]
    is_read = Message.FIELDS["is_read"]

    posted_time = DateTimeField(field_uri="postitem:PostedTime", is_read_only=True)
    references = TextField(field_uri="message:References")
    sender = MailboxField(field_uri="message:Sender", is_read_only=True, is_read_only_after_send=True)

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS

Instance variables

var author
var conversation_index
var conversation_topic
var is_read
var message_id
var posted_time
var references
var sender

Inherited members

class PostReplyItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postreplyitem

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.

Expand source code
class PostReplyItem(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/postreplyitem"""

    ELEMENT_NAME = "PostReplyItem"

    # This element only has Item fields up to, and including, 'culture'
    # TDO: Plus all message fields
    new_body = BodyField(field_uri="NewBodyContent")  # Accepts and returns Body or HTMLBody instances

    culture_idx = Item.FIELDS.index_by_name("culture")
    sender_idx = Message.FIELDS.index_by_name("sender")
    FIELDS = Item.FIELDS[: culture_idx + 1] + Message.FIELDS[sender_idx:]

Ancestors

Class variables

var ELEMENT_NAME
var FIELDS
var culture_idx
var sender_idx

Instance variables

var author
var bcc_recipients
var cc_recipients
var conversation_index
var conversation_topic
var is_delivery_receipt_requested
var is_read
var is_read_receipt_requested
var is_response_requested
var message_id
var new_body
var received_by
var received_representing
var references
var reminder_message_data
var reply_to
var sender
var to_recipients

Inherited members

class RegisterMixIn (**kwargs)

Base class for classes that can change their list of supported fields dynamically.

Expand source code
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)

Ancestors

Subclasses

Class variables

var INSERT_AFTER_FIELD

Static methods

def deregister(attr_name)

De-register an extended property that has been registered with register().

:param attr_name: :return:

Expand source code
@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)
def register(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:

Expand source code
@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)

Inherited members

class ReplyAllToItem (**kwargs)
Expand source code
class ReplyAllToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replyalltoitem"""

    ELEMENT_NAME = "ReplyAllToItem"

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class ReplyToItem (**kwargs)
Expand source code
class ReplyToItem(BaseReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/replytoitem"""

    ELEMENT_NAME = "ReplyToItem"

Ancestors

Class variables

var ELEMENT_NAME

Inherited members

class Task (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task

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.

Expand source code
class Task(Item):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/task"""

    ELEMENT_NAME = "Task"
    NOT_STARTED = "NotStarted"
    COMPLETED = "Completed"

    actual_work = IntegerField(field_uri="task:ActualWork", min=0)
    assigned_time = DateTimeField(field_uri="task:AssignedTime", is_read_only=True)
    billing_information = TextField(field_uri="task:BillingInformation")
    change_count = IntegerField(field_uri="task:ChangeCount", is_read_only=True, min=0)
    companies = TextListField(field_uri="task:Companies")
    # 'complete_date' can be set, but is ignored by the server, which sets it to now()
    complete_date = DateTimeField(field_uri="task:CompleteDate", is_read_only=True)
    contacts = TextListField(field_uri="task:Contacts")
    delegation_state = ChoiceField(
        field_uri="task:DelegationState",
        choices={
            Choice("NoMatch"),
            Choice("OwnNew"),
            Choice("Owned"),
            Choice("Accepted"),
            Choice("Declined"),
            Choice("Max"),
        },
        is_read_only=True,
    )
    delegator = CharField(field_uri="task:Delegator", is_read_only=True)
    due_date = DateTimeBackedDateField(field_uri="task:DueDate")
    is_editable = BooleanField(field_uri="task:IsAssignmentEditable", is_read_only=True)
    is_complete = BooleanField(field_uri="task:IsComplete", is_read_only=True)
    is_recurring = BooleanField(field_uri="task:IsRecurring", is_read_only=True)
    is_team_task = BooleanField(field_uri="task:IsTeamTask", is_read_only=True)
    mileage = TextField(field_uri="task:Mileage")
    owner = CharField(field_uri="task:Owner", is_read_only=True)
    percent_complete = DecimalField(
        field_uri="task:PercentComplete",
        is_required=True,
        default=Decimal(0.0),
        min=Decimal(0),
        max=Decimal(100),
        is_searchable=False,
    )
    recurrence = TaskRecurrenceField(field_uri="task:Recurrence", is_searchable=False)
    start_date = DateTimeBackedDateField(field_uri="task:StartDate")
    status = ChoiceField(
        field_uri="task:Status",
        choices={
            Choice(NOT_STARTED),
            Choice("InProgress"),
            Choice(COMPLETED),
            Choice("WaitingOnOthers"),
            Choice("Deferred"),
        },
        is_required=True,
        is_searchable=False,
        default=NOT_STARTED,
    )
    status_description = CharField(field_uri="task:StatusDescription", is_read_only=True)
    total_work = IntegerField(field_uri="task:TotalWork", min=0)

    def clean(self, version=None):
        super().clean(version=version)
        if self.due_date and self.start_date and self.due_date < self.start_date:
            log.warning(
                "'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
                self.due_date,
                self.start_date,
            )
            self.due_date = self.start_date
        if self.complete_date:
            if self.status != self.COMPLETED:
                log.warning(
                    "'status' must be '%s' when 'complete_date' is set (%s). Resetting", self.COMPLETED, self.status
                )
                self.status = self.COMPLETED
            now = datetime.datetime.now(tz=UTC)
            if (self.complete_date - now).total_seconds() > 120:
                # Reset complete_date values that are in the future
                # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
                log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
                self.complete_date = now
            if self.start_date and self.complete_date.date() < self.start_date:
                log.warning(
                    "'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                    self.complete_date,
                    self.start_date,
                )
                self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
        if self.percent_complete is not None:
            if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
                # percent_complete must be 100% if task is complete
                log.warning(
                    "'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                    self.COMPLETED,
                    self.percent_complete,
                )
                self.percent_complete = Decimal(100)
            elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
                # percent_complete must be 0% if task is not started
                log.warning(
                    "'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                    self.NOT_STARTED,
                    self.percent_complete,
                )
                self.percent_complete = Decimal(0)

    def complete(self):
        # A helper method to mark a task as complete on the server
        self.status = Task.COMPLETED
        self.percent_complete = Decimal(100)
        self.save()

Ancestors

Class variables

var COMPLETED
var ELEMENT_NAME
var FIELDS
var NOT_STARTED

Instance variables

var actual_work
var assigned_time
var billing_information
var change_count
var companies
var complete_date
var contacts
var delegation_state
var delegator
var due_date
var is_complete
var is_editable
var is_recurring
var is_team_task
var mileage
var owner
var percent_complete
var recurrence
var start_date
var status
var status_description
var total_work

Methods

def clean(self, version=None)
Expand source code
def clean(self, version=None):
    super().clean(version=version)
    if self.due_date and self.start_date and self.due_date < self.start_date:
        log.warning(
            "'due_date' must be greater than 'start_date' (%s vs %s). Resetting 'due_date'",
            self.due_date,
            self.start_date,
        )
        self.due_date = self.start_date
    if self.complete_date:
        if self.status != self.COMPLETED:
            log.warning(
                "'status' must be '%s' when 'complete_date' is set (%s). Resetting", self.COMPLETED, self.status
            )
            self.status = self.COMPLETED
        now = datetime.datetime.now(tz=UTC)
        if (self.complete_date - now).total_seconds() > 120:
            # Reset complete_date values that are in the future
            # 'complete_date' can be set automatically by the server. Allow some grace between local and server time
            log.warning("'complete_date' must be in the past (%s vs %s). Resetting", self.complete_date, now)
            self.complete_date = now
        if self.start_date and self.complete_date.date() < self.start_date:
            log.warning(
                "'complete_date' must be greater than 'start_date' (%s vs %s). Resetting",
                self.complete_date,
                self.start_date,
            )
            self.complete_date = EWSDateTime.combine(self.start_date, datetime.time(0, 0)).replace(tzinfo=UTC)
    if self.percent_complete is not None:
        if self.status == self.COMPLETED and self.percent_complete != Decimal(100):
            # percent_complete must be 100% if task is complete
            log.warning(
                "'percent_complete' must be 100 when 'status' is '%s' (%s). Resetting",
                self.COMPLETED,
                self.percent_complete,
            )
            self.percent_complete = Decimal(100)
        elif self.status == self.NOT_STARTED and self.percent_complete != Decimal(0):
            # percent_complete must be 0% if task is not started
            log.warning(
                "'percent_complete' must be 0 when 'status' is '%s' (%s). Resetting",
                self.NOT_STARTED,
                self.percent_complete,
            )
            self.percent_complete = Decimal(0)
def complete(self)
Expand source code
def complete(self):
    # A helper method to mark a task as complete on the server
    self.status = Task.COMPLETED
    self.percent_complete = Decimal(100)
    self.save()

Inherited members

class TentativelyAcceptItem (**kwargs)

MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem

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.

Expand source code
class TentativelyAcceptItem(BaseMeetingReplyItem):
    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/tentativelyacceptitem"""

    ELEMENT_NAME = "TentativelyAcceptItem"

Ancestors

Class variables

var ELEMENT_NAME

Inherited members