From 4abaaaaac820709cb65dd5d3439e927c70b7ca45 Mon Sep 17 00:00:00 2001 From: Claudius Ellsel Date: Fri, 25 Aug 2023 23:01:47 +0200 Subject: [PATCH 1/4] Update index.md (#1217) --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 1637149e..ba0f7b84 100644 --- a/docs/index.md +++ b/docs/index.md @@ -114,7 +114,7 @@ fails to install. ## Setup and connecting First, specify your credentials. Username is usually in `WINDOMAIN\username` format, -where `WINDOMAIN is` the name of the Windows Domain your username is connected +where `WINDOMAIN` is the name of the Windows Domain your username is connected to, but some servers also accept usernames in PrimarySMTPAddress ('myusername@example.com') format (Office365 requires it). UPN format is also supported, if your server expects that. From 22af5eec84f9654a25fcfe63c7dd3db622082483 Mon Sep 17 00:00:00 2001 From: ecederstrand Date: Sat, 26 Aug 2023 13:07:04 +0200 Subject: [PATCH 2/4] docs: Improve warning text to suggest how to fix the issue. While here, convert to warning so users can make it a hard error. Refs #541 --- exchangelib/fields.py | 22 ++++++++++++++-------- tests/test_field.py | 20 +++++++++++++++++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/exchangelib/fields.py b/exchangelib/fields.py index 07e9556e..08562e29 100644 --- a/exchangelib/fields.py +++ b/exchangelib/fields.py @@ -1,6 +1,7 @@ import abc import datetime import logging +import warnings from contextlib import suppress from decimal import Decimal, InvalidOperation from importlib import import_module @@ -739,16 +740,21 @@ def clean(self, value, version=None): def from_xml(self, elem, account): field_elem = elem.find(self.response_tag()) if field_elem is not None: - ms_id = field_elem.get("Id") - ms_name = field_elem.get("Name") + tz_id = field_elem.get("Id") or field_elem.get("Name") try: - return self.value_cls.from_ms_id(ms_id or ms_name) + return self.value_cls.from_ms_id(tz_id) except UnknownTimeZone: - log.warning( - "Cannot convert value '%s' on field '%s' to type %s (unknown timezone ID)", - (ms_id or ms_name), - self.name, - self.value_cls, + warnings.warn( + f"""\ +Cannot convert value {tz_id!r} on field {self.name!r} to type {self.value_cls.__name__!r} (unknown timezone ID). +You can fix this by adding a custom entry into the timezone translation map: + +from exchangelib.winzone import MS_TIMEZONE_TO_IANA_MAP, CLDR_TO_MS_TIMEZONE_MAP + +# Replace "Some_Region/Some_Location" with a reasonable value from CLDR_TO_MS_TIMEZONE_MAP.keys() +MS_TIMEZONE_TO_IANA_MAP[{tz_id!r}] = "Some_Region/Some_Location" + +# Your code here""" ) return None return self.default diff --git a/tests/test_field.py b/tests/test_field.py index 88e6d710..ad7128b1 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -1,4 +1,5 @@ import datetime +import warnings from collections import namedtuple from decimal import Decimal @@ -176,7 +177,24 @@ def test_garbage_input(self): """ elem = to_xml(payload).find(f"{{{TNS}}}Item") field = TimeZoneField("foo", field_uri="item:Foo", default="DUMMY") - self.assertEqual(field.from_xml(elem=elem, account=account), None) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + tz = field.from_xml(elem=elem, account=account) + self.assertEqual(tz, None) + self.assertEqual( + str(w[0].message), + """\ +Cannot convert value 'THIS_IS_GARBAGE' on field 'foo' to type 'EWSTimeZone' (unknown timezone ID). +You can fix this by adding a custom entry into the timezone translation map: + +from exchangelib.winzone import MS_TIMEZONE_TO_IANA_MAP, CLDR_TO_MS_TIMEZONE_MAP + +# Replace "Some_Region/Some_Location" with a reasonable value from CLDR_TO_MS_TIMEZONE_MAP.keys() +MS_TIMEZONE_TO_IANA_MAP['THIS_IS_GARBAGE'] = "Some_Region/Some_Location" + +# Your code here""", + ) def test_versioned_field(self): field = TextField("foo", field_uri="bar", supported_from=EXCHANGE_2010) From 0a0c0f4c29acb10badf529b6a3532243f90c342b Mon Sep 17 00:00:00 2001 From: ecederstrand Date: Mon, 28 Aug 2023 23:45:45 +0200 Subject: [PATCH 3/4] fix: Remove duplicate recipient in reply when author and recipient is the same person. Also remove recipient of riginal mail from recipients in reply. Fixes #1218 --- exchangelib/items/message.py | 18 ++++++++++++------ tests/test_items/test_messages.py | 9 +++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/exchangelib/items/message.py b/exchangelib/items/message.py index 8afaa810..0b9df830 100644 --- a/exchangelib/items/message.py +++ b/exchangelib/items/message.py @@ -121,7 +121,7 @@ def send_and_save( @require_id def create_reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recipients=None, author=None): - if to_recipients is None: + if not to_recipients: if not self.author: raise ValueError("'to_recipients' must be set when message has no 'author'") to_recipients = [self.author] @@ -141,17 +141,23 @@ def reply(self, subject, body, to_recipients=None, cc_recipients=None, bcc_recip @require_id def create_reply_all(self, subject, body, author=None): - to_recipients = list(self.to_recipients) if self.to_recipients else [] + me = MailboxField().clean(self.account.primary_smtp_address.lower()) + to_recipients = set(self.to_recipients or []) + to_recipients.discard(me) + cc_recipients = set(self.cc_recipients or []) + cc_recipients.discard(me) + bcc_recipients = set(self.bcc_recipients or []) + bcc_recipients.discard(me) if self.author: - to_recipients.append(self.author) + to_recipients.add(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, + to_recipients=list(to_recipients), + cc_recipients=list(cc_recipients), + bcc_recipients=list(bcc_recipients), author=author, ) diff --git a/tests/test_items/test_messages.py b/tests/test_items/test_messages.py index ae8688f8..07cb7575 100644 --- a/tests/test_items/test_messages.py +++ b/tests/test_items/test_messages.py @@ -133,6 +133,15 @@ def test_reply_all(self): with self.assertRaises(TypeError) as e: ReplyToItem(account="XXX") self.assertEqual(e.exception.args[0], "'account' 'XXX' must be of type ") + + # Test that to_recipients only has one entry even when we are both the sender and the receiver + item = self.get_test_item(folder=None) + item.id, item.changekey = 123, 456 + reply_item = item.create_reply_all(subject="", body="") + self.assertEqual(reply_item.to_recipients, item.to_recipients) + self.assertEqual(reply_item.to_recipients, item.to_recipients) + self.assertEqual(reply_item.to_recipients, item.to_recipients) + # Test that we can reply-all a Message item. EWS only allows items that have been sent to receive a reply item = self.get_test_item(folder=None) item.folder = None From b6d9e82f1698ebd2e0234ab72678ecd210876667 Mon Sep 17 00:00:00 2001 From: ecederstrand Date: Mon, 28 Aug 2023 23:46:20 +0200 Subject: [PATCH 4/4] ci: Make get_test_item(folder=None) work as intended --- tests/test_items/test_basics.py | 5 +++-- tests/test_items/test_messages.py | 15 ++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/test_items/test_basics.py b/tests/test_items/test_basics.py index 6964b7a6..7f49b51d 100644 --- a/tests/test_items/test_basics.py +++ b/tests/test_items/test_basics.py @@ -235,10 +235,11 @@ def get_random_update_kwargs(self, item, insert_kwargs): update_kwargs["end"] = (update_kwargs["end"] + datetime.timedelta(days=1)).date() return update_kwargs - def get_test_item(self, folder=None, categories=None): + def get_test_item(self, categories=None, **kwargs): + folder = kwargs.pop("folder", self.test_folder) item_kwargs = self.get_random_insert_kwargs() item_kwargs["categories"] = categories or self.categories - return self.ITEM_CLASS(folder=folder or self.test_folder, **item_kwargs) + return self.ITEM_CLASS(account=self.account, folder=folder, **item_kwargs) def get_test_folder(self, folder=None): return self.FOLDER_CLASS(parent=folder or self.test_folder, name=get_random_string(8)) diff --git a/tests/test_items/test_messages.py b/tests/test_items/test_messages.py index 07cb7575..060b0920 100644 --- a/tests/test_items/test_messages.py +++ b/tests/test_items/test_messages.py @@ -33,8 +33,7 @@ def get_incoming_message(self, subject): def test_send(self): # Test that we can send (only) Message items - item = self.get_test_item() - item.folder = None + item = self.get_test_item(folder=None) item.send() self.assertIsNone(item.id) self.assertIsNone(item.changekey) @@ -52,8 +51,7 @@ def test_send_pre_2013(self): self.assertIsNone(item.changekey) def test_send_no_copy(self): - item = self.get_test_item() - item.folder = None + item = self.get_test_item(folder=None) item.send(save_copy=False) self.assertIsNone(item.id) self.assertIsNone(item.changekey) @@ -98,8 +96,7 @@ def test_send_and_copy_to_folder(self): def test_reply(self): # Test that we can reply to a Message item. EWS only allows items that have been sent to receive a reply - item = self.get_test_item() - item.folder = None + item = self.get_test_item(folder=None) item.send() # get_test_item() sets the to_recipients to the test account sent_item = self.get_incoming_message(item.subject) new_subject = (f"Re: {sent_item.subject}")[:255] @@ -109,7 +106,6 @@ def test_reply(self): def test_create_reply(self): # Test that we can save a reply without sending it item = self.get_test_item(folder=None) - item.folder = None item.send() sent_item = self.get_incoming_message(item.subject) new_subject = (f"Re: {sent_item.subject}")[:255] @@ -144,17 +140,15 @@ def test_reply_all(self): # Test that we can reply-all a Message item. EWS only allows items that have been sent to receive a reply item = self.get_test_item(folder=None) - item.folder = None item.send() sent_item = self.get_incoming_message(item.subject) - new_subject = (f"Re: {sent_item.subject}")[:255] + new_subject = f"Re: {sent_item.subject}"[:255] sent_item.reply_all(subject=new_subject, body="Hello reply") self.assertEqual(self.account.sent.filter(subject=new_subject).count(), 1) def test_forward(self): # Test that we can forward a Message item. EWS only allows items that have been sent to receive a reply item = self.get_test_item(folder=None) - item.folder = None item.send() sent_item = self.get_incoming_message(item.subject) new_subject = (f"Re: {sent_item.subject}")[:255] @@ -164,7 +158,6 @@ def test_forward(self): def test_create_forward(self): # Test that we can forward a Message item. EWS only allows items that have been sent to receive a reply item = self.get_test_item(folder=None) - item.folder = None item.send() sent_item = self.get_incoming_message(item.subject) new_subject = (f"Re: {sent_item.subject}")[:255]