def __init__(self, *, parent=None, con=None, **kwargs): """ Create a contact API component :param parent: parent account for this folder :type parent: Account :param Connection con: connection to use if no parent specified :param Protocol protocol: protocol to use if no parent specified (kwargs) :param str main_resource: use this resource instead of parent resource (kwargs) """ assert parent or con, 'Need a parent or a connection' self.con = parent.con if parent else con # Choose the main_resource passed in kwargs over parent main_resource main_resource = kwargs.pop('main_resource', None) or getattr( parent, 'main_resource', None) if parent else None super().__init__( protocol=parent.protocol if parent else kwargs.get('protocol'), main_resource=main_resource) cloud_data = kwargs.get(self._cloud_data_key, {}) cc = self._cc # alias to shorten the code # internal to know which properties need to be updated on the server self._track_changes = TrackerSet(casing=cc) self.object_id = cloud_data.get(cc('id'), None) self.__created = cloud_data.get(cc('createdDateTime'), None) self.__modified = cloud_data.get(cc('lastModifiedDateTime'), None) local_tz = self.protocol.timezone self.__created = parse( self.created).astimezone(local_tz) if self.__created else None self.__modified = parse( self.modified).astimezone(local_tz) if self.__modified else None self.__display_name = cloud_data.get(cc('displayName'), '') self.__name = cloud_data.get(cc('givenName'), '') self.__surname = cloud_data.get(cc('surname'), '') self.__title = cloud_data.get(cc('title'), '') self.__job_title = cloud_data.get(cc('jobTitle'), '') self.__company_name = cloud_data.get(cc('companyName'), '') self.__department = cloud_data.get(cc('department'), '') self.__office_location = cloud_data.get(cc('officeLocation'), '') self.__business_phones = cloud_data.get(cc('businessPhones'), []) or [] self.__mobile_phone = cloud_data.get(cc('mobilePhone'), '') self.__home_phones = cloud_data.get(cc('homePhones'), []) or [] emails = cloud_data.get(cc('emailAddresses'), []) self.__emails = Recipients(recipients=[(rcp.get(cc('name'), ''), rcp.get(cc('address'), '')) for rcp in emails], parent=self, field=cc('emailAddresses')) email = cloud_data.get(cc('email')) self.__emails.untrack = True if email and email not in self.__emails: # a Contact from OneDrive? self.__emails.add(email) self.__business_address = cloud_data.get(cc('businessAddress'), {}) self.__home_address = cloud_data.get(cc('homesAddress'), {}) self.__other_address = cloud_data.get(cc('otherAddress'), {}) self.__preferred_language = cloud_data.get(cc('preferredLanguage'), None) self.__categories = cloud_data.get(cc('categories'), []) self.__folder_id = cloud_data.get(cc('parentFolderId'), None) # When using Users endpoints (GAL) # Missing keys: ['mail', 'userPrincipalName'] mail = cloud_data.get(cc('mail'), None) user_principal_name = cloud_data.get(cc('userPrincipalName'), None) if mail and mail not in self.emails: self.emails.add(mail) if user_principal_name and user_principal_name not in self.emails: self.emails.add(user_principal_name) self.__emails.untrack = False
class Contact(ApiComponent, AttachableMixin): """ Contact manages lists of events on associated contact on office365. """ _endpoints = { 'contact': '/contacts', 'root_contact': '/contacts/{id}', 'child_contact': '/contactFolders/{folder_id}/contacts' } message_constructor = Message def __init__(self, *, parent=None, con=None, **kwargs): """ Create a contact API component :param parent: parent account for this folder :type parent: Account :param Connection con: connection to use if no parent specified :param Protocol protocol: protocol to use if no parent specified (kwargs) :param str main_resource: use this resource instead of parent resource (kwargs) """ assert parent or con, 'Need a parent or a connection' self.con = parent.con if parent else con # Choose the main_resource passed in kwargs over parent main_resource main_resource = kwargs.pop('main_resource', None) or getattr( parent, 'main_resource', None) if parent else None super().__init__( protocol=parent.protocol if parent else kwargs.get('protocol'), main_resource=main_resource) cloud_data = kwargs.get(self._cloud_data_key, {}) cc = self._cc # alias to shorten the code # internal to know which properties need to be updated on the server self._track_changes = TrackerSet(casing=cc) self.object_id = cloud_data.get(cc('id'), None) self.__created = cloud_data.get(cc('createdDateTime'), None) self.__modified = cloud_data.get(cc('lastModifiedDateTime'), None) local_tz = self.protocol.timezone self.__created = parse( self.created).astimezone(local_tz) if self.__created else None self.__modified = parse( self.modified).astimezone(local_tz) if self.__modified else None self.__display_name = cloud_data.get(cc('displayName'), '') self.__name = cloud_data.get(cc('givenName'), '') self.__surname = cloud_data.get(cc('surname'), '') self.__title = cloud_data.get(cc('title'), '') self.__job_title = cloud_data.get(cc('jobTitle'), '') self.__company_name = cloud_data.get(cc('companyName'), '') self.__department = cloud_data.get(cc('department'), '') self.__office_location = cloud_data.get(cc('officeLocation'), '') self.__business_phones = cloud_data.get(cc('businessPhones'), []) or [] self.__mobile_phone = cloud_data.get(cc('mobilePhone'), '') self.__home_phones = cloud_data.get(cc('homePhones'), []) or [] emails = cloud_data.get(cc('emailAddresses'), []) self.__emails = Recipients(recipients=[(rcp.get(cc('name'), ''), rcp.get(cc('address'), '')) for rcp in emails], parent=self, field=cc('emailAddresses')) email = cloud_data.get(cc('email')) self.__emails.untrack = True if email and email not in self.__emails: # a Contact from OneDrive? self.__emails.add(email) self.__business_address = cloud_data.get(cc('businessAddress'), {}) self.__home_address = cloud_data.get(cc('homesAddress'), {}) self.__other_address = cloud_data.get(cc('otherAddress'), {}) self.__preferred_language = cloud_data.get(cc('preferredLanguage'), None) self.__categories = cloud_data.get(cc('categories'), []) self.__folder_id = cloud_data.get(cc('parentFolderId'), None) # When using Users endpoints (GAL) # Missing keys: ['mail', 'userPrincipalName'] mail = cloud_data.get(cc('mail'), None) user_principal_name = cloud_data.get(cc('userPrincipalName'), None) if mail and mail not in self.emails: self.emails.add(mail) if user_principal_name and user_principal_name not in self.emails: self.emails.add(user_principal_name) self.__emails.untrack = False @property def created(self): """ Created Time :rtype: datetime """ return self.__created @property def modified(self): """ Last Modified Time :rtype: datetime """ return self.__modified @property def display_name(self): """ Display Name :getter: Get the display name of the contact :setter: Update the display name :type: str """ return self.__display_name @display_name.setter def display_name(self, value): self.__display_name = value self._track_changes.add(self._cc('displayName')) @property def name(self): """ First Name :getter: Get the name of the contact :setter: Update the name :type: str """ return self.__name @name.setter def name(self, value): self.__name = value self._track_changes.add(self._cc('givenName')) @property def surname(self): """ Surname of Contact :getter: Get the surname of the contact :setter: Update the surname :type: str """ return self.__surname @surname.setter def surname(self, value): self.__surname = value self._track_changes.add(self._cc('surname')) @property def full_name(self): """ Full Name (Name + Surname) :rtype: str """ return '{} {}'.format(self.name, self.surname).strip() @property def title(self): """ Title (Mr., Ms., etc..) :getter: Get the title of the contact :setter: Update the title :type: str """ return self.__title @title.setter def title(self, value): self.__title = value self._track_changes.add(self._cc('title')) @property def job_title(self): """ Job Title :getter: Get the job title of contact :setter: Update the job title :type: str """ return self.__job_title @job_title.setter def job_title(self, value): self.__job_title = value self._track_changes.add(self._cc('jobTitle')) @property def company_name(self): """ Name of the company :getter: Get the company name of contact :setter: Update the company name :type: str """ return self.__company_name @company_name.setter def company_name(self, value): self.__company_name = value self._track_changes.add(self._cc('companyName')) @property def department(self): """ Department :getter: Get the department of contact :setter: Update the department :type: str """ return self.__department @department.setter def department(self, value): self.__department = value self._track_changes.add(self._cc('department')) @property def office_location(self): """ Office Location :getter: Get the office location of contact :setter: Update the office location :type: str """ return self.__office_location @office_location.setter def office_location(self, value): self.__office_location = value self._track_changes.add(self._cc('officeLocation')) @property def business_phones(self): """ Business Contact numbers :getter: Get the contact numbers of contact :setter: Update the contact numbers :type: list[str] """ return self.__business_phones @business_phones.setter def business_phones(self, value): if isinstance(value, tuple): value = list(value) if not isinstance(value, list): value = [value] self.__business_phones = value self._track_changes.add(self._cc('businessPhones')) @property def mobile_phone(self): """ Personal Contact numbers :getter: Get the contact numbers of contact :setter: Update the contact numbers :type: list[str] """ return self.__mobile_phone @mobile_phone.setter def mobile_phone(self, value): self.__mobile_phone = value self._track_changes.add(self._cc('mobilePhone')) @property def home_phones(self): """ Home Contact numbers :getter: Get the contact numbers of contact :setter: Update the contact numbers :type: list[str] """ return self.__home_phones @home_phones.setter def home_phones(self, value): if isinstance(value, list): self.__home_phones = value elif isinstance(value, str): self.__home_phones = [value] elif isinstance(value, tuple): self.__home_phones = list(value) else: raise ValueError('home_phones must be a list') self._track_changes.add(self._cc('homePhones')) @property def emails(self): """ List of email ids of the Contact :rtype: Recipients """ return self.__emails @property def main_email(self): """ Primary(First) email id of the Contact :rtype: str """ if not self.emails: return None return self.emails[0].address @property def business_address(self): """ Business Address :getter: Get the address of contact :setter: Update the address :type: dict """ return self.__business_address @business_address.setter def business_address(self, value): if not isinstance(value, dict): raise ValueError('"business_address" must be dict') self.__business_address = value self._track_changes.add(self._cc('businessAddress')) @property def home_address(self): """ Home Address :getter: Get the address of contact :setter: Update the address :type: dict """ return self.__home_address @home_address.setter def home_address(self, value): if not isinstance(value, dict): raise ValueError('"home_address" must be dict') self.__home_address = value self._track_changes.add(self._cc('homesAddress')) @property def other_address(self): """ Other Address :getter: Get the address of contact :setter: Update the address :type: dict """ return self.__other_address @other_address.setter def other_address(self, value): if not isinstance(value, dict): raise ValueError('"other_address" must be dict') self.__other_address = value self._track_changes.add(self._cc('otherAddress')) @property def preferred_language(self): """ Preferred Language :getter: Get the language of contact :setter: Update the language :type: str """ return self.__preferred_language @preferred_language.setter def preferred_language(self, value): self.__preferred_language = value self._track_changes.add(self._cc('preferredLanguage')) @property def categories(self): """ Assigned Categories :getter: Get the categories :setter: Update the categories :type: list[str] """ return self.__categories @categories.setter def categories(self, value): if isinstance(value, list): self.__categories = value elif isinstance(value, str): self.__categories = [value] elif isinstance(value, tuple): self.__categories = list(value) else: raise ValueError('categories must be a list') self._track_changes.add(self._cc('categories')) @property def folder_id(self): """ ID of the folder :rtype: str """ return self.__folder_id def __str__(self): return self.__repr__() def __repr__(self): return self.display_name or self.full_name or 'Unknown Name' def to_api_data(self, restrict_keys=None): """ Returns a dictionary in cloud format :param restrict_keys: a set of keys to restrict the returned data to. """ cc = self._cc # alias data = { cc('displayName'): self.__display_name, cc('givenName'): self.__name, cc('surname'): self.__surname, cc('title'): self.__title, cc('jobTitle'): self.__job_title, cc('companyName'): self.__company_name, cc('department'): self.__department, cc('officeLocation'): self.__office_location, cc('businessPhones'): self.__business_phones, cc('mobilePhone'): self.__mobile_phone, cc('homePhones'): self.__home_phones, cc('emailAddresses'): [{ self._cc('name'): recipient.name or '', self._cc('address'): recipient.address } for recipient in self.emails], cc('businessAddress'): self.__business_address, cc('homesAddress'): self.__home_address, cc('otherAddress'): self.__other_address, cc('categories'): self.__categories } if restrict_keys: restrict_keys.add(cc( 'givenName')) # GivenName is required by the api all the time. for key in list(data.keys()): if key not in restrict_keys: del data[key] return data def delete(self): """ Deletes this contact :return: Success or Failure :rtype: bool :raises RuntimeError: if contact is not yet saved to cloud """ if not self.object_id: raise RuntimeError('Attempting to delete an unsaved Contact') url = self.build_url( self._endpoints.get('root_contact').format(id=self.object_id)) response = self.con.delete(url) return bool(response) def save(self): """ Saves this contact to the cloud (create or update existing one based on what values have changed) :return: Saved or Not :rtype: bool """ if self.object_id: # Update Contact if not self._track_changes: return True # there's nothing to update url = self.build_url( self._endpoints.get('root_contact').format(id=self.object_id)) method = self.con.patch data = self.to_api_data(restrict_keys=self._track_changes) else: # Save new Contact if self.__folder_id: url = self.build_url( self._endpoints.get('child_contact').format( folder_id=self.__folder_id)) else: url = self.build_url(self._endpoints.get('contact')) method = self.con.post data = self.to_api_data(restrict_keys=self._track_changes) response = method(url, data=data) if not response: return False if not self.object_id: # New Contact contact = response.json() self.object_id = contact.get(self._cc('id'), None) self.__created = contact.get(self._cc('createdDateTime'), None) self.__modified = contact.get(self._cc('lastModifiedDateTime'), None) local_tz = self.protocol.timezone self.__created = parse( self.created).astimezone(local_tz) if self.__created else None self.__modified = parse(self.modified).astimezone( local_tz) if self.__modified else None else: self.__modified = self.protocol.timezone.localize( dt.datetime.now()) return True def new_message(self, recipient=None, *, recipient_type=RecipientType.TO): """ This method returns a new draft Message instance with contacts first email as a recipient :param Recipient recipient: a Recipient instance where to send this message. If None first email of this contact will be used :param RecipientType recipient_type: section to add recipient into :return: newly created message :rtype: Message or None """ if self.main_resource == GAL_MAIN_RESOURCE: # preventing the contact lookup to explode for big organizations.. raise RuntimeError('Sending a message to all users within an ' 'Organization is not allowed') if isinstance(recipient_type, str): recipient_type = RecipientType(recipient_type) recipient = recipient or self.emails.get_first_recipient_with_address() if not recipient: return None new_message = self.message_constructor(parent=self, is_draft=True) target_recipients = getattr(new_message, str(recipient_type.value)) target_recipients.add(recipient) return new_message
def __init__(self, *, parent=None, con=None, **kwargs): """ Makes a new message wrapper for sending and receiving messages. :param parent: the parent object :param con: the id of this message if it exists """ assert parent or con, 'Need a parent or a connection' self.con = parent.con if parent else con # Choose the main_resource passed in kwargs over the parent main_resource main_resource = kwargs.pop('main_resource', None) or getattr( parent, 'main_resource', None) if parent else None super().__init__( protocol=parent.protocol if parent else kwargs.get('protocol'), main_resource=main_resource, attachment_name_property='subject', attachment_type='message_type') download_attachments = kwargs.get('download_attachments') cloud_data = kwargs.get(self._cloud_data_key, {}) cc = self._cc # alias to shorten the code self._track_changes = TrackerSet( casing=cc ) # internal to know which properties need to be updated on the server self.object_id = cloud_data.get(cc('id'), None) self.__created = cloud_data.get(cc('createdDateTime'), None) self.__modified = cloud_data.get(cc('lastModifiedDateTime'), None) self.__received = cloud_data.get(cc('receivedDateTime'), None) self.__sent = cloud_data.get(cc('sentDateTime'), None) local_tz = self.protocol.timezone self.__created = parse( self.__created).astimezone(local_tz) if self.__created else None self.__modified = parse( self.__modified).astimezone(local_tz) if self.__modified else None self.__received = parse( self.__received).astimezone(local_tz) if self.__received else None self.__sent = parse( self.__sent).astimezone(local_tz) if self.__sent else None self.__attachments = MessageAttachments(parent=self, attachments=[]) self.has_attachments = cloud_data.get(cc('hasAttachments'), 0) if self.has_attachments and download_attachments: self.attachments.download_attachments() self.__subject = cloud_data.get(cc('subject'), '') body = cloud_data.get(cc('body'), {}) self.__body = body.get(cc('content'), '') self.body_type = body.get(cc('contentType'), 'HTML') # default to HTML for new messages self.__sender = self._recipient_from_cloud(cloud_data.get( cc('from'), None), field=cc('from')) self.__to = self._recipients_from_cloud(cloud_data.get( cc('toRecipients'), []), field=cc('toRecipients')) self.__cc = self._recipients_from_cloud(cloud_data.get( cc('ccRecipients'), []), field=cc('ccRecipients')) self.__bcc = self._recipients_from_cloud(cloud_data.get( cc('bccRecipients'), []), field=cc('bccRecipients')) self.__reply_to = self._recipients_from_cloud(cloud_data.get( cc('replyTo'), []), field=cc('replyTo')) self.__categories = cloud_data.get(cc('categories'), []) self.__importance = ImportanceLevel( (cloud_data.get(cc('importance'), 'normal') or 'normal').lower()) # lower because of office365 v1.0 self.__is_read = cloud_data.get(cc('isRead'), None) self.__is_draft = cloud_data.get( cc('isDraft'), kwargs.get('is_draft', True)) # a message is a draft by default self.conversation_id = cloud_data.get(cc('conversationId'), None) self.folder_id = cloud_data.get(cc('parentFolderId'), None)
def _clear_tracker(self): # reset the tracked changes. Usually after a server update self._track_changes = TrackerSet(casing=self._cc)
class Message(ApiComponent, AttachableMixin, HandleRecipientsMixin): """ Management of the process of sending, receiving, reading, and editing emails. """ _endpoints = { 'create_draft': '/messages', 'create_draft_folder': '/mailFolders/{id}/messages', 'send_mail': '/sendMail', 'send_draft': '/messages/{id}/send', 'get_message': '/messages/{id}', 'move_message': '/messages/{id}/move', 'copy_message': '/messages/{id}/copy', 'create_reply': '/messages/{id}/createReply', 'create_reply_all': '/messages/{id}/createReplyAll', 'forward_message': '/messages/{id}/createForward' } def __init__(self, *, parent=None, con=None, **kwargs): """ Makes a new message wrapper for sending and receiving messages. :param parent: the parent object :param con: the id of this message if it exists """ assert parent or con, 'Need a parent or a connection' self.con = parent.con if parent else con # Choose the main_resource passed in kwargs over the parent main_resource main_resource = kwargs.pop('main_resource', None) or getattr( parent, 'main_resource', None) if parent else None super().__init__( protocol=parent.protocol if parent else kwargs.get('protocol'), main_resource=main_resource, attachment_name_property='subject', attachment_type='message_type') download_attachments = kwargs.get('download_attachments') cloud_data = kwargs.get(self._cloud_data_key, {}) cc = self._cc # alias to shorten the code self._track_changes = TrackerSet( casing=cc ) # internal to know which properties need to be updated on the server self.object_id = cloud_data.get(cc('id'), None) self.__created = cloud_data.get(cc('createdDateTime'), None) self.__modified = cloud_data.get(cc('lastModifiedDateTime'), None) self.__received = cloud_data.get(cc('receivedDateTime'), None) self.__sent = cloud_data.get(cc('sentDateTime'), None) local_tz = self.protocol.timezone self.__created = parse( self.__created).astimezone(local_tz) if self.__created else None self.__modified = parse( self.__modified).astimezone(local_tz) if self.__modified else None self.__received = parse( self.__received).astimezone(local_tz) if self.__received else None self.__sent = parse( self.__sent).astimezone(local_tz) if self.__sent else None self.__attachments = MessageAttachments(parent=self, attachments=[]) self.has_attachments = cloud_data.get(cc('hasAttachments'), 0) if self.has_attachments and download_attachments: self.attachments.download_attachments() self.__subject = cloud_data.get(cc('subject'), '') body = cloud_data.get(cc('body'), {}) self.__body = body.get(cc('content'), '') self.body_type = body.get(cc('contentType'), 'HTML') # default to HTML for new messages self.__sender = self._recipient_from_cloud(cloud_data.get( cc('from'), None), field=cc('from')) self.__to = self._recipients_from_cloud(cloud_data.get( cc('toRecipients'), []), field=cc('toRecipients')) self.__cc = self._recipients_from_cloud(cloud_data.get( cc('ccRecipients'), []), field=cc('ccRecipients')) self.__bcc = self._recipients_from_cloud(cloud_data.get( cc('bccRecipients'), []), field=cc('bccRecipients')) self.__reply_to = self._recipients_from_cloud(cloud_data.get( cc('replyTo'), []), field=cc('replyTo')) self.__categories = cloud_data.get(cc('categories'), []) self.__importance = ImportanceLevel( (cloud_data.get(cc('importance'), 'normal') or 'normal').lower()) # lower because of office365 v1.0 self.__is_read = cloud_data.get(cc('isRead'), None) self.__is_draft = cloud_data.get( cc('isDraft'), kwargs.get('is_draft', True)) # a message is a draft by default self.conversation_id = cloud_data.get(cc('conversationId'), None) self.folder_id = cloud_data.get(cc('parentFolderId'), None) def _clear_tracker(self): # reset the tracked changes. Usually after a server update self._track_changes = TrackerSet(casing=self._cc) @property def is_read(self): return self.__is_read @is_read.setter def is_read(self, value): self.__is_read = value self._track_changes.add('isRead') @property def is_draft(self): return self.__is_draft @property def subject(self): return self.__subject @subject.setter def subject(self, value): self.__subject = value self._track_changes.add('subject') @property def body(self): return self.__body @body.setter def body(self, value): if self.__body: if not value: self.__body = '' else: soup = bs(self.__body, 'html.parser') soup.body.insert(0, bs(value, 'html.parser')) self.__body = str(soup) else: self.__body = value self._track_changes.add('body') @property def created(self): return self.__created @property def modified(self): return self.__modified @property def received(self): return self.__received @property def sent(self): return self.__sent @property def attachments(self): """ Just to avoid api misuse by assigning to 'attachments' """ return self.__attachments @property def sender(self): """ sender is a property to force to be allways a Recipient class """ return self.__sender @sender.setter def sender(self, value): """ sender is a property to force to be allways a Recipient class """ if isinstance(value, Recipient): if value._parent is None: value._parent = self value._field = 'from' self.__sender = value elif isinstance(value, str): self.__sender.address = value self.__sender.name = '' else: raise ValueError( 'sender must be an address string or a Recipient object') self._track_changes.add('from') @property def to(self): """ Just to avoid api misuse by assigning to 'to' """ return self.__to @property def cc(self): """ Just to avoid api misuse by assigning to 'cc' """ return self.__cc @property def bcc(self): """ Just to avoid api misuse by assigning to 'bcc' """ return self.__bcc @property def reply_to(self): """ Just to avoid api misuse by assigning to 'reply_to' """ return self.__reply_to @property def categories(self): return self.__categories @categories.setter def categories(self, value): if isinstance(value, list): self.__categories = value elif isinstance(value, str): self.__categories = [value] elif isinstance(value, tuple): self.__categories = list(value) else: raise ValueError('categories must be a list') self._track_changes.add('categories') @property def importance(self): return self.__importance @importance.setter def importance(self, value): self.__importance = value if isinstance( value, ImportanceLevel) else ImportanceLevel(value.lower()) self._track_changes.add('importance') def to_api_data(self, restrict_keys=None): """ Returns a dict representation of this message prepared to be send to the cloud :param restrict_keys: a set of keys to restrict the returned data to. """ cc = self._cc # alias to shorten the code message = { cc('subject'): self.subject, cc('body'): { cc('contentType'): self.body_type, cc('content'): self.body }, cc('importance'): self.importance.value } if self.to: message[cc('toRecipients')] = [ self._recipient_to_cloud(recipient) for recipient in self.to ] if self.cc: message[cc('ccRecipients')] = [ self._recipient_to_cloud(recipient) for recipient in self.cc ] if self.bcc: message[cc('bccRecipients')] = [ self._recipient_to_cloud(recipient) for recipient in self.bcc ] if self.reply_to: message[cc('replyTo')] = [ self._recipient_to_cloud(recipient) for recipient in self.reply_to ] if self.attachments: message[cc('attachments')] = self.attachments.to_api_data() if self.sender and self.sender.address: message[cc('from')] = self._recipient_to_cloud(self.sender) if self.object_id and not self.__is_draft: # return the whole signature of this message message[cc('id')] = self.object_id message[cc('createdDateTime')] = self.created.astimezone( pytz.utc).isoformat() message[cc('receivedDateTime')] = self.received.astimezone( pytz.utc).isoformat() message[cc('sentDateTime')] = self.sent.astimezone( pytz.utc).isoformat() message[cc('hasAttachments')] = len(self.attachments) > 0 message[cc('categories')] = self.categories message[cc('isRead')] = self.is_read message[cc('isDraft')] = self.__is_draft message[cc('conversationId')] = self.conversation_id message[cc( 'parentFolderId' )] = self.folder_id # this property does not form part of the message itself if restrict_keys: for key in list(message.keys()): if key not in restrict_keys: del message[key] return message def send(self, save_to_sent_folder=True): """ Sends this message. """ if self.object_id and not self.__is_draft: return RuntimeError( 'Not possible to send a message that is not new or a draft. Use Reply or Forward instead.' ) if self.__is_draft and self.object_id: url = self.build_url( self._endpoints.get('send_draft').format(id=self.object_id)) data = None else: url = self.build_url(self._endpoints.get('send_mail')) data = {self._cc('message'): self.to_api_data()} if save_to_sent_folder is False: data[self._cc('saveToSentItems')] = False response = self.con.post(url, data=data) if not response: # response evaluates to false if 4XX or 5XX status codes are returned return False self.object_id = 'sent_message' if not self.object_id else self.object_id self.__is_draft = False return True def reply(self, to_all=True): """ Creates a new message that is a reply to this message. :param to_all: replies to all the recipients instead to just the sender """ if not self.object_id or self.__is_draft: raise RuntimeError("Can't reply to this message") if to_all: url = self.build_url( self._endpoints.get('create_reply_all').format( id=self.object_id)) else: url = self.build_url( self._endpoints.get('create_reply').format(id=self.object_id)) response = self.con.post(url) if not response: return None message = response.json() # Everything received from the cloud must be passed with self._cloud_data_key return self.__class__(parent=self, **{self._cloud_data_key: message}) def forward(self): """ Creates a new message that is a forward of this message. """ if not self.object_id or self.__is_draft: raise RuntimeError("Can't forward this message") url = self.build_url( self._endpoints.get('forward_message').format(id=self.object_id)) response = self.con.post(url) if not response: return None message = response.json() # Everything received from the cloud must be passed with self._cloud_data_key return self.__class__(parent=self, **{self._cloud_data_key: message}) def delete(self): """ Deletes a stored message """ if self.object_id is None: raise RuntimeError('Attempting to delete an unsaved Message') url = self.build_url( self._endpoints.get('get_message').format(id=self.object_id)) response = self.con.delete(url) return bool(response) def mark_as_read(self): """ Marks this message as read in the cloud.""" if self.object_id is None or self.__is_draft: raise RuntimeError('Attempting to mark as read an unsaved Message') data = {self._cc('isRead'): True} url = self.build_url( self._endpoints.get('get_message').format(id=self.object_id)) response = self.con.patch(url, data=data) if not response: return False self.__is_read = True return True def move(self, folder): """ Move the message to a given folder :param folder: Folder object or Folder id or Well-known name to move this message to :returns: True on success """ if self.object_id is None: raise RuntimeError('Attempting to move an unsaved Message') url = self.build_url( self._endpoints.get('move_message').format(id=self.object_id)) if isinstance(folder, str): folder_id = folder else: folder_id = getattr(folder, 'folder_id', None) if not folder_id: raise RuntimeError('Must Provide a valid folder_id') data = {self._cc('destinationId'): folder_id} response = self.con.post(url, data=data) if not response: return False self.folder_id = folder_id return True def copy(self, folder): """ Copy the message to a given folder :param folder: Folder object or Folder id or Well-known name to move this message to :returns: the copied message """ if self.object_id is None: raise RuntimeError('Attempting to move an unsaved Message') url = self.build_url( self._endpoints.get('copy_message').format(id=self.object_id)) if isinstance(folder, str): folder_id = folder else: folder_id = getattr(folder, 'folder_id', None) if not folder_id: raise RuntimeError('Must Provide a valid folder_id') data = {self._cc('destinationId'): folder_id} response = self.con.post(url, data=data) if not response: return None message = response.json() # Everything received from the cloud must be passed with self._cloud_data_key return self.__class__(parent=self, **{self._cloud_data_key: message}) def save_draft(self, target_folder=OutlookWellKnowFolderNames.DRAFTS): """ Save this message as a draft on the cloud """ if self.object_id: # update message. Attachments are NOT included nor saved. if not self.__is_draft: raise RuntimeError('Only draft messages can be updated') if not self._track_changes: return True # there's nothing to update url = self.build_url( self._endpoints.get('get_message').format(id=self.object_id)) method = self.con.patch data = self.to_api_data(restrict_keys=self._track_changes) data.pop(self._cc('attachments'), None) # attachments are handled by the next method call self.attachments._update_attachments_to_cloud() else: # new message. Attachments are included and saved. if not self.__is_draft: raise RuntimeError( 'Only draft messages can be saved as drafts') target_folder = target_folder or OutlookWellKnowFolderNames.DRAFTS if isinstance(target_folder, OutlookWellKnowFolderNames): target_folder = target_folder.value elif not isinstance(target_folder, str): # a Folder instance target_folder = getattr( target_folder, 'folder_id', OutlookWellKnowFolderNames.DRAFTS.value) url = self.build_url( self._endpoints.get('create_draft_folder').format( id=target_folder)) method = self.con.post data = self.to_api_data() self._clear_tracker( ) # reset the tracked changes as they are all saved. if not data: return True response = method(url, data=data) if not response: return False if not self.object_id: # new message message = response.json() self.object_id = message.get(self._cc('id'), None) self.folder_id = message.get(self._cc('parentFolderId'), None) self.__created = message.get( self._cc('createdDateTime'), message.get(self._cc('dateTimeCreated'), None)) # fallback to office365 v1.0 self.__modified = message.get( self._cc('lastModifiedDateTime'), message.get(self._cc('dateTimeModified'), None)) # fallback to office365 v1.0 self.__created = parse(self.__created).astimezone( self.protocol.timezone) if self.__created else None self.__modified = parse(self.__modified).astimezone( self.protocol.timezone) if self.__modified else None else: self.__modified = self.protocol.timezone.localize( dt.datetime.now()) return True def get_body_text(self): """ Parse the body html and returns the body text using bs4 """ if self.body_type != 'HTML': return self.body try: soup = bs(self.body, 'html.parser') except Exception as e: return self.body else: return soup.body.text def get_body_soup(self): """ Returns the beautifulsoup4 of the html body""" if self.body_type != 'HTML': return None else: return bs(self.body, 'html.parser') def __str__(self): return self.__repr__() def __repr__(self): return 'Subject: {}'.format(self.subject)