From 3f7ead2adb395f01e36e1096ffab7516707e0dc0 Mon Sep 17 00:00:00 2001 From: ecederstrand Date: Sun, 3 Mar 2024 21:43:12 +0100 Subject: [PATCH] fix: Rename Rule.rule_id to Rule.id. Update API to require full rule objects, and to set and unset Rule.id automatically. Blacken. --- exchangelib/account.py | 59 +++++++++++--------------- exchangelib/properties.py | 61 ++++++++++----------------- exchangelib/services/__init__.py | 2 +- exchangelib/services/inbox_rules.py | 65 ++++++++++++----------------- tests/test_account.py | 39 ++++++++++++++++- 5 files changed, 112 insertions(+), 114 deletions(-) diff --git a/exchangelib/account.py b/exchangelib/account.py index c234e20d..966db5cb 100644 --- a/exchangelib/account.py +++ b/exchangelib/account.py @@ -6,14 +6,7 @@ from .autodiscover import Autodiscovery from .configuration import Configuration from .credentials import ACCESS_TYPES, DELEGATE, IMPERSONATION -from .errors import ( - ErrorInvalidArgument, - ErrorItemNotFound, - InvalidEnumValue, - InvalidTypeError, - ResponseMessageError, - UnknownTimeZone, -) +from .errors import ErrorItemNotFound, InvalidEnumValue, InvalidTypeError, ResponseMessageError, UnknownTimeZone from .ewsdatetime import UTC, EWSTimeZone from .fields import FieldPath, TextField from .folders import ( @@ -764,44 +757,42 @@ def rules(self): return list(GetInboxRules(account=self).call()) def create_rule(self, rule: Rule): - """Create an Inbox rule in a user's mailbox in the Exchange store. + """Create an Inbox rule. - :param rule: The rule to create with display_name as the key at least. - :return: The rule ID of the created rule. If failed, raise an error. + :param rule: The rule to create. Must have at least 'display_name' set. + :return: None if success, else raises an error. """ - create_rule_service = CreateInboxRule(account=self) - create_rule_service.get(rule=rule, remove_outlook_rule_blob=True) + CreateInboxRule(account=self).get(rule=rule, remove_outlook_rule_blob=True) # After creating the rule, query all rules, # find the rule that was just created, and return its ID. - rules = GetInboxRules(account=self).call() - for i in rules: - if i.display_name == rule.display_name: - return i.rule_id - raise ResponseMessageError(f"Failed to create rule ({rule.display_name})!") + try: + rule.id = {i.display_name: i for i in GetInboxRules(account=self).call()}[rule.display_name].id + except KeyError: + raise ResponseMessageError(f"Failed to create rule ({rule.display_name})!") def set_rule(self, rule: Rule): - """Modify an Inbox rule in a user's mailbox in the Exchange store. + """Modify an Inbox rule. - :param rule: The rule to set with rule_id as the key at least. - :return: None if success, else raise an error. + :param rule: The rule to set. Must have an ID. + :return: None if success, else raises an error. """ - return SetInboxRule(account=self).get(rule=rule) + SetInboxRule(account=self).get(rule=rule) def delete_rule(self, rule: Rule): - """Delete an Inbox rule in a user's mailbox in the Exchange store. + """Delete an Inbox rule. - :param rule: The rule to delete with rule_id or display_name as the key at least. - :return: None if success, else raise an error. + :param rule: The rule to delete. Must have ID or 'display_name'. + :return: None if success, else raises an error. """ - if rule.rule_id: - return DeleteInboxRule(account=self).get(rule_id=rule.rule_id) - if rule.display_name: - rules = GetInboxRules(account=self).call() - for _rule in rules: - if _rule.display_name == rule.display_name: - return DeleteInboxRule(account=self).get(rule_id=_rule.rule_id) - raise ErrorItemNotFound(f"rule ({rule.display_name}) not found in the mailbox!") - raise ErrorInvalidArgument("rule.rule_id or rule.display_name is required!") + if not rule.id: + if not rule.display_name: + raise ValueError("Rule must have ID or display_name") + try: + rule = {i.display_name: i for i in GetInboxRules(account=self).call()}[rule.display_name] + except KeyError: + raise ErrorItemNotFound(f"No rule with name {rule.display_name!r}") + DeleteInboxRule(account=self).get(rule=rule) + rule.id = None def subscribe_to_pull(self, event_types=None, watermark=None, timeout=60): """Create a pull subscription. diff --git a/exchangelib/properties.py b/exchangelib/properties.py index dd50bf03..bcfd3081 100644 --- a/exchangelib/properties.py +++ b/exchangelib/properties.py @@ -2153,8 +2153,7 @@ class WithinDateRange(EWSElement): ELEMENT_NAME = "DateRange" NAMESPACE = MNS - start_date_time = DateTimeField( - field_uri="StartDateTime", is_required=True) + start_date_time = DateTimeField(field_uri="StartDateTime", is_required=True) end_date_time = DateTimeField(field_uri="EndDateTime", is_required=True) @@ -2179,16 +2178,12 @@ class Conditions(EWSElement): categories = CharListField(field_uri="Categories") contains_body_strings = CharListField(field_uri="ContainsBodyStrings") contains_header_strings = CharListField(field_uri="ContainsHeaderStrings") - contains_recipient_strings = CharListField( - field_uri="ContainsRecipientStrings") + contains_recipient_strings = CharListField(field_uri="ContainsRecipientStrings") contains_sender_strings = CharListField(field_uri="ContainsSenderStrings") - contains_subject_or_body_strings = CharListField( - field_uri="ContainsSubjectOrBodyStrings") - contains_subject_strings = CharListField( - field_uri="ContainsSubjectStrings") + contains_subject_or_body_strings = CharListField(field_uri="ContainsSubjectOrBodyStrings") + contains_subject_strings = CharListField(field_uri="ContainsSubjectStrings") flagged_for_action = FlaggedForActionField(field_uri="FlaggedForAction") - from_addresses = EWSElementField( - value_cls=Mailbox, field_uri="FromAddresses") + from_addresses = EWSElementField(value_cls=Mailbox, field_uri="FromAddresses") from_connected_accounts = CharListField(field_uri="FromConnectedAccounts") has_attachments = BooleanField(field_uri="HasAttachments") importance = ImportanceField(field_uri="Importance") @@ -2208,15 +2203,12 @@ class Conditions(EWSElement): not_sent_to_me = BooleanField(field_uri="NotSentToMe") sent_cc_me = BooleanField(field_uri="SentCcMe") sent_only_to_me = BooleanField(field_uri="SentOnlyToMe") - sent_to_addresses = EWSElementField( - value_cls=Mailbox, field_uri="SentToAddresses") + sent_to_addresses = EWSElementField(value_cls=Mailbox, field_uri="SentToAddresses") sent_to_me = BooleanField(field_uri="SentToMe") sent_to_or_cc_me = BooleanField(field_uri="SentToOrCcMe") sensitivity = SensitivityField(field_uri="Sensitivity") - within_date_range = EWSElementField( - value_cls=WithinDateRange, field_uri="WithinDateRange") - within_size_range = EWSElementField( - value_cls=WithinSizeRange, field_uri="WithinSizeRange") + within_date_range = EWSElementField(value_cls=WithinDateRange, field_uri="WithinDateRange") + within_size_range = EWSElementField(value_cls=WithinSizeRange, field_uri="WithinSizeRange") class Exceptions(Conditions): @@ -2233,8 +2225,7 @@ class CopyToFolder(EWSElement): NAMESPACE = MNS folder_id = EWSElementField(value_cls=FolderId, field_uri="FolderId") - distinguished_folder_id = EWSElementField( - value_cls=DistinguishedFolderId, field_uri="DistinguishedFolderId") + distinguished_folder_id = EWSElementField(value_cls=DistinguishedFolderId, field_uri="DistinguishedFolderId") class MoveToFolder(CopyToFolder): @@ -2250,24 +2241,19 @@ class Actions(EWSElement): NAMESPACE = TNS assign_categories = CharListField(field_uri="AssignCategories") - copy_to_folder = EWSElementField( - value_cls=CopyToFolder, field_uri="CopyToFolder") + copy_to_folder = EWSElementField(value_cls=CopyToFolder, field_uri="CopyToFolder") delete = BooleanField(field_uri="Delete") forward_as_attachment_to_recipients = EWSElementField( - value_cls=Mailbox, field_uri="ForwardAsAttachmentToRecipients") - forward_to_recipients = EWSElementField( - value_cls=Mailbox, field_uri="ForwardToRecipients") + value_cls=Mailbox, field_uri="ForwardAsAttachmentToRecipients" + ) + forward_to_recipients = EWSElementField(value_cls=Mailbox, field_uri="ForwardToRecipients") mark_importance = ImportanceField(field_uri="MarkImportance") mark_as_read = BooleanField(field_uri="MarkAsRead") - move_to_folder = EWSElementField( - value_cls=MoveToFolder, field_uri="MoveToFolder") + move_to_folder = EWSElementField(value_cls=MoveToFolder, field_uri="MoveToFolder") permanent_delete = BooleanField(field_uri="PermanentDelete") - redirect_to_recipients = EWSElementField( - value_cls=Mailbox, field_uri="RedirectToRecipients") - send_sms_alert_to_recipients = EWSElementField( - value_cls=Mailbox, field_uri="SendSMSAlertToRecipients") - server_reply_with_message = EWSElementField( - value_cls=ItemId, field_uri="ServerReplyWithMessage") + redirect_to_recipients = EWSElementField(value_cls=Mailbox, field_uri="RedirectToRecipients") + send_sms_alert_to_recipients = EWSElementField(value_cls=Mailbox, field_uri="SendSMSAlertToRecipients") + server_reply_with_message = EWSElementField(value_cls=ItemId, field_uri="ServerReplyWithMessage") stop_processing_rules = BooleanField(field_uri="StopProcessingRules") @@ -2277,7 +2263,7 @@ class Rule(EWSElement): ELEMENT_NAME = "Rule" NAMESPACE = TNS - rule_id = CharField(field_uri="RuleId") + id = CharField(field_uri="RuleId") display_name = CharField(field_uri="DisplayName") priority = IntegerField(field_uri="Priority") is_enabled = BooleanField(field_uri="IsEnabled") @@ -2321,7 +2307,7 @@ class DeleteRuleOperation(EWSElement): ELEMENT_NAME = "DeleteRuleOperation" NAMESPACE = TNS - rule_id = CharField(field_uri="RuleId") + id = CharField(field_uri="RuleId") class Operations(EWSElement): @@ -2330,9 +2316,6 @@ class Operations(EWSElement): ELEMENT_NAME = "Operations" NAMESPACE = MNS - create_rule_operation = EWSElementField( - value_cls=CreateRuleOperation) - set_rule_operation = EWSElementField( - value_cls=SetRuleOperation) - delete_rule_operation = EWSElementField( - value_cls=DeleteRuleOperation) + create_rule_operation = EWSElementField(value_cls=CreateRuleOperation) + set_rule_operation = EWSElementField(value_cls=SetRuleOperation) + delete_rule_operation = EWSElementField(value_cls=DeleteRuleOperation) diff --git a/exchangelib/services/__init__.py b/exchangelib/services/__init__.py index 91ef4c21..77a22f21 100644 --- a/exchangelib/services/__init__.py +++ b/exchangelib/services/__init__.py @@ -113,5 +113,5 @@ "GetInboxRules", "CreateInboxRule", "SetInboxRule", - "DeleteInboxRule" + "DeleteInboxRule", ] diff --git a/exchangelib/services/inbox_rules.py b/exchangelib/services/inbox_rules.py index 4809d38d..3545d7c1 100644 --- a/exchangelib/services/inbox_rules.py +++ b/exchangelib/services/inbox_rules.py @@ -1,7 +1,6 @@ from typing import Any, Generator, Optional, Union from ..errors import ErrorInvalidOperation -from ..fields import CharField from ..properties import CreateRuleOperation, DeleteRuleOperation, InboxRules, Operations, Rule, SetRuleOperation from ..util import MNS, add_xml_child, create_element, get_xml_attr, set_xml_value from ..version import EXCHANGE_2010 @@ -18,9 +17,7 @@ class GetInboxRules(EWSAccountService): SERVICE_NAME = "GetInboxRules" supported_from = EXCHANGE_2010 element_container_name = InboxRules.response_tag() - ERRORS_TO_CATCH_IN_RESPONSE = EWSAccountService.ERRORS_TO_CATCH_IN_RESPONSE + ( - ErrorInvalidOperation, - ) + ERRORS_TO_CATCH_IN_RESPONSE = EWSAccountService.ERRORS_TO_CATCH_IN_RESPONSE + (ErrorInvalidOperation,) def call(self, mailbox: Optional[str] = None) -> Generator[Union[Rule, Exception, None], Any, None]: if not mailbox: @@ -34,7 +31,7 @@ def _elem_to_obj(self, elem): def get_payload(self, mailbox): payload = create_element(f"m:{self.SERVICE_NAME}") - add_xml_child(payload, 'm:MailboxSmtpAddress', mailbox) + add_xml_child(payload, "m:MailboxSmtpAddress", mailbox) return payload def _get_element_container(self, message, name=None): @@ -54,36 +51,32 @@ class UpdateInboxRules(EWSAccountService): UpdateInboxRules is used to create an Inbox rule, to set an Inbox rule, or to delete an Inbox rule. When you use the UpdateInboxRules operation, Exchange Web Services deletes client-side send rules. - Client-side send rules are stored on the client in the rule Folder Associated Information (FAI) Message and nowhere else. - EWS deletes this rule FAI message by default, based on the expectation that Outlook will recreate it. - However, Outlook can't recreate rules that don't also exist as an extended rule, and client-side send rules don't exist as extended rules. - As a result, these rules are lost. We suggest you consider this when designing your solution. + Client-side send rules are stored on the client in the rule Folder Associated Information (FAI) Message and nowhere + else. EWS deletes this rule FAI message by default, based on the expectation that Outlook will recreate it. + However, Outlook can't recreate rules that don't also exist as an extended rule, and client-side send rules don't + exist as extended rules. As a result, these rules are lost. We suggest you consider this when designing your + solution. """ SERVICE_NAME = "UpdateInboxRules" supported_from = EXCHANGE_2010 - ERRORS_TO_CATCH_IN_RESPONSE = EWSAccountService.ERRORS_TO_CATCH_IN_RESPONSE + ( - ErrorInvalidOperation, - ) + ERRORS_TO_CATCH_IN_RESPONSE = EWSAccountService.ERRORS_TO_CATCH_IN_RESPONSE + (ErrorInvalidOperation,) class CreateInboxRule(UpdateInboxRules): """ - MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateinboxrules-operation#updateinboxrules-create-rule-request-example + MSDN: + https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateinboxrules-operation#updateinboxrules-create-rule-request-example """ def call(self, rule: Rule, remove_outlook_rule_blob: bool = True): - payload = self.get_payload( - rule=rule, remove_outlook_rule_blob=remove_outlook_rule_blob) + payload = self.get_payload(rule=rule, remove_outlook_rule_blob=remove_outlook_rule_blob) return self._get_elements(payload=payload) - def get_payload(self, rule: Rule, - remove_outlook_rule_blob: bool = True): + def get_payload(self, rule: Rule, remove_outlook_rule_blob: bool = True): payload = create_element(f"m:{self.SERVICE_NAME}") - add_xml_child(payload, 'm:RemoveOutlookRuleBlob', - remove_outlook_rule_blob) - operations = Operations( - create_rule_operation=CreateRuleOperation(rule=rule)) + add_xml_child(payload, "m:RemoveOutlookRuleBlob", remove_outlook_rule_blob) + operations = Operations(create_rule_operation=CreateRuleOperation(rule=rule)) set_xml_value(payload, operations, version=self.account.version) return payload @@ -95,17 +88,14 @@ class SetInboxRule(UpdateInboxRules): """ def call(self, rule: Rule, remove_outlook_rule_blob: bool = True): - payload = self.get_payload( - rule=rule, remove_outlook_rule_blob=remove_outlook_rule_blob) + payload = self.get_payload(rule=rule, remove_outlook_rule_blob=remove_outlook_rule_blob) return self._get_elements(payload=payload) - def get_payload(self, rule: Rule, - remove_outlook_rule_blob: bool = True): - if not rule.rule_id: - raise ValueError("rule_id cannot be empty") + def get_payload(self, rule: Rule, remove_outlook_rule_blob: bool = True): + if not rule.id: + raise ValueError("Rule must have an ID") payload = create_element(f"m:{self.SERVICE_NAME}") - add_xml_child(payload, 'm:RemoveOutlookRuleBlob', - remove_outlook_rule_blob) + add_xml_child(payload, "m:RemoveOutlookRuleBlob", remove_outlook_rule_blob) operations = Operations(set_rule_operation=SetRuleOperation(rule=rule)) set_xml_value(payload, operations, version=self.account.version) return payload @@ -117,18 +107,15 @@ class DeleteInboxRule(UpdateInboxRules): https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/updateinboxrules-operation#updateinboxrules-delete-rule-request-example """ - def call(self, rule_id: Union[str, CharField], remove_outlook_rule_blob: bool = True): - payload = self.get_payload( - rule_id=rule_id, remove_outlook_rule_blob=remove_outlook_rule_blob) + def call(self, rule: Rule, remove_outlook_rule_blob: bool = True): + payload = self.get_payload(rule=rule, remove_outlook_rule_blob=remove_outlook_rule_blob) return self._get_elements(payload=payload) - def get_payload(self, rule_id: str, remove_outlook_rule_blob: bool = True): - if not rule_id: - raise ValueError("rule_id cannot be empty") + def get_payload(self, rule: Rule, remove_outlook_rule_blob: bool = True): + if not rule.id: + raise ValueError("Rule must have an ID") payload = create_element(f"m:{self.SERVICE_NAME}") - add_xml_child(payload, 'm:RemoveOutlookRuleBlob', - remove_outlook_rule_blob) - operations = Operations( - delete_rule_operation=DeleteRuleOperation(rule_id=rule_id)) + add_xml_child(payload, "m:RemoveOutlookRuleBlob", remove_outlook_rule_blob) + operations = Operations(delete_rule_operation=DeleteRuleOperation(id=rule.id)) set_xml_value(payload, operations, version=self.account.version) return payload diff --git a/tests/test_account.py b/tests/test_account.py index 8a3bc401..090c1fc9 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -19,11 +19,15 @@ from exchangelib.folders import Calendar from exchangelib.items import Message from exchangelib.properties import ( + Actions, + Conditions, DelegatePermissions, DelegateUser, + Exceptions, MailTips, OutOfOffice, RecipientAddress, + Rule, SendingAs, UserId, ) @@ -31,7 +35,7 @@ from exchangelib.services import GetDelegate, GetMailTips from exchangelib.version import EXCHANGE_2007_SP1, Version -from .common import EWSTest +from .common import EWSTest, get_random_string class AccountTest(EWSTest): @@ -337,3 +341,36 @@ def test_protocol_default_values(self): ) self.assertIsNotNone(a.protocol.auth_type) self.assertIsNotNone(a.protocol.retry_policy) + + def test_inbox_rules(self): + # Clean up first + for rule in self.account.rules: + self.account.delete_rule(rule) + + self.assertEqual(len(self.account.rules), 0) + + # Create rule + display_name = get_random_string(16) + rule = Rule( + display_name=display_name, + priority=1, + is_enabled=True, + conditions=Conditions(contains_sender_strings=[get_random_string(8)]), + exceptions=Exceptions(), + actions=Actions(delete=True), + ) + self.assertIsNone(rule.id) + self.account.create_rule(rule=rule) + self.assertIsNotNone(rule.id) + self.assertEqual(len(self.account.rules), 1) + self.assertEqual(self.account.rules[0].display_name, display_name) + + # Update rule + rule.display_name = get_random_string(16) + self.account.set_rule(rule=rule) + self.assertEqual(len(self.account.rules), 1) + self.assertNotEqual(self.account.rules[0].display_name, display_name) + + # Delete rule + self.account.delete_rule(rule=rule) + self.assertEqual(len(self.account.rules), 0)