def test_redirect_url_is_valid(self, m): # This method is private but hard to get to otherwise a = Autodiscovery("*****@*****.**") # Already visited a._urls_visited.append("https://example.com") self.assertFalse(a._redirect_url_is_valid("https://example.com")) a._urls_visited.clear() # Max redirects exceeded a._redirect_count = 10 self.assertFalse(a._redirect_url_is_valid("https://example.com")) a._redirect_count = 0 # Must be secure self.assertFalse(a._redirect_url_is_valid("http://example.com")) # Does not resolve with DNS url = f"https://{get_random_hostname()}" m.head(url, status_code=200) self.assertFalse(a._redirect_url_is_valid(url)) # Bad response from URL on valid hostname m.head(self.account.protocol.config.service_endpoint, status_code=501) self.assertTrue(a._redirect_url_is_valid(self.account.protocol.config.service_endpoint)) # OK response from URL on valid hostname m.head(self.account.protocol.config.service_endpoint, status_code=200) self.assertTrue(a._redirect_url_is_valid(self.account.protocol.config.service_endpoint))
def test_autodiscover_cache(self, m): # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post(self.dummy_ews_endpoint, status_code=200) discovery = Autodiscovery( email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ) # Not cached self.assertNotIn(discovery._cache_key, autodiscover_cache) discovery.discover() # Now it's cached self.assertIn(discovery._cache_key, autodiscover_cache) # Make sure the cache can be looked by value, not by id(). This is important for multi-threading/processing self.assertIn( (self.account.primary_smtp_address.split('@')[1], Credentials(self.account.protocol.credentials.username, self.account.protocol.credentials.password), True), autodiscover_cache) # Poison the cache with a failing autodiscover endpoint. discover() must handle this and rebuild the cache autodiscover_cache[discovery._cache_key] = AutodiscoverProtocol( config=Configuration( service_endpoint= 'https://example.com/Autodiscover/Autodiscover.xml', credentials=Credentials(get_random_string(8), get_random_string(8)), auth_type=NTLM, retry_policy=FailFast(), )) m.post('https://example.com/Autodiscover/Autodiscover.xml', status_code=404) discovery.discover() self.assertIn(discovery._cache_key, autodiscover_cache) # Make sure that the cache is actually used on the second call to discover() _orig = discovery._step_1 def _mock(slf, *args, **kwargs): raise NotImplementedError() discovery._step_1 = MethodType(_mock, discovery) discovery.discover() # Fake that another thread added the cache entry into the persistent storage but we don't have it in our # in-memory cache. The cache should work anyway. autodiscover_cache._protocols.clear() discovery.discover() discovery._step_1 = _orig # Make sure we can delete cache entries even though we don't have it in our in-memory cache autodiscover_cache._protocols.clear() del autodiscover_cache[discovery._cache_key] # This should also work if the cache does not contain the entry anymore del autodiscover_cache[discovery._cache_key]
def test_autodiscover_path_1_2_3_no301_4(self, m): # Test steps 1 -> 2 -> 3 -> no 301 response -> 4 d = Autodiscovery(email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials) m.post(self.dummy_ad_endpoint, status_code=501) m.post(f"https://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=501) m.get(f"http://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=200) with self.assertRaises(AutoDiscoverFailed): # Fails in step 4 with invalid SRV entry ad_response, _ = d.discover()
def test_autodiscover_path_1_5_valid_redirect_url_invalid_response(self, m): # Test steps 1 -> -> 5 -> Invalid response from redirect URL clear_cache() d = Autodiscovery(email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials) redirect_url = "https://httpbin.org/Autodiscover/Autodiscover.xml" m.post(self.dummy_ad_endpoint, status_code=200, content=self.redirect_url_xml(redirect_url)) m.head(redirect_url, status_code=501) m.post(redirect_url, status_code=501) with self.assertRaises(AutoDiscoverFailed): # Fails in step 5 with invalid response ad_response, _ = d.discover()
def test_autodiscover_path_1_5_invalid_redirect_url(self, m): # Test steps 1 -> -> 5 -> Invalid redirect URL clear_cache() d = Autodiscovery(email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials) m.post( self.dummy_ad_endpoint, status_code=200, content=self.redirect_url_xml(f"https://{get_random_hostname()}/EWS/Exchange.asmx"), ) with self.assertRaises(AutoDiscoverFailed): # Fails in step 5 with invalid redirect URL ad_response, _ = d.discover()
def test_autodiscover_path_1_5_valid_redirect_url_valid_response(self, m): # Test steps 1 -> -> 5 -> Valid response from redirect URL -> 5 clear_cache() d = Autodiscovery(email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials) redirect_hostname = "httpbin.org" redirect_url = f"https://{redirect_hostname}/Autodiscover/Autodiscover.xml" ews_url = f"https://{redirect_hostname}/EWS/Exchange.asmx" email = f"john@redirected.{redirect_hostname}" m.post(self.dummy_ad_endpoint, status_code=200, content=self.redirect_url_xml(redirect_url)) m.head(redirect_url, status_code=200) m.post(redirect_url, status_code=200, content=self.settings_xml(email, ews_url)) ad_response, _ = d.discover() self.assertEqual(ad_response.autodiscover_smtp_address, email) self.assertEqual(ad_response.protocol.ews_url, ews_url)
def test_autodiscover_path_1_2_3_4_invalid_srv(self, m): # Test steps 1 -> 2 -> 3 -> 4 -> invalid SRV URL d = Autodiscovery(email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials) m.post(self.dummy_ad_endpoint, status_code=501) m.post(f"https://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=501) m.get(f"http://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=200) tmp = d._get_srv_records d._get_srv_records = Mock(return_value=[SrvRecord(1, 1, 443, get_random_hostname())]) try: with self.assertRaises(AutoDiscoverFailed): # Fails in step 4 with invalid response ad_response, _ = d.discover() finally: d._get_srv_records = tmp
def test_autodiscover_path_1_2_3_invalid301_4(self, m): # Test steps 1 -> 2 -> 3 -> invalid 301 URL -> 4 clear_cache() d = Autodiscovery(email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials) m.post(self.dummy_ad_endpoint, status_code=501) m.post(f"https://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=501) m.get( f"http://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=301, headers=dict(location="XXX"), ) with self.assertRaises(AutoDiscoverFailed): # Fails in step 4 with invalid SRV entry ad_response, _ = d.discover()
def test_autodiscover_path_1_2_5(self, m): # Test steps 1 -> 2 -> 5 clear_cache() d = Autodiscovery(email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials) ews_url = f"https://xxx.{self.domain}/EWS/Exchange.asmx" email = f"xxxd@{self.domain}" m.post(self.dummy_ad_endpoint, status_code=501) m.post( f"https://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=200, content=self.settings_xml(email, ews_url), ) ad_response, _ = d.discover() self.assertEqual(ad_response.autodiscover_smtp_address, email) self.assertEqual(ad_response.protocol.ews_url, ews_url)
def test_autodiscover_path_1_2_3_4_valid_srv_valid_response(self, m): # Test steps 1 -> 2 -> 3 -> 4 -> 5 d = Autodiscovery(email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials) redirect_srv = "httpbin.org" ews_url = f"https://{redirect_srv}/EWS/Exchange.asmx" redirect_email = f"john@redirected.{redirect_srv}" m.post(self.dummy_ad_endpoint, status_code=501) m.post(f"https://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=501) m.get(f"http://autodiscover.{self.domain}/Autodiscover/Autodiscover.xml", status_code=200) m.head(f"https://{redirect_srv}/Autodiscover/Autodiscover.xml", status_code=200) m.post( f"https://{redirect_srv}/Autodiscover/Autodiscover.xml", status_code=200, content=self.settings_xml(redirect_email, ews_url), ) tmp = d._get_srv_records d._get_srv_records = Mock(return_value=[SrvRecord(1, 1, 443, redirect_srv)]) try: ad_response, _ = d.discover() self.assertEqual(ad_response.autodiscover_smtp_address, redirect_email) self.assertEqual(ad_response.protocol.ews_url, ews_url) finally: d._get_srv_records = tmp
def test_get_srv_records(self): from exchangelib.autodiscover.discovery import SrvRecord ad = Autodiscovery('*****@*****.**') # Unknown domain self.assertEqual(ad._get_srv_records('example.XXXXX'), []) # No SRV record self.assertEqual(ad._get_srv_records('example.com'), []) # Finding a real server that has a correct SRV record is not easy. Mock it _orig = dns.resolver.Resolver class _Mock1: @staticmethod def resolve(hostname, cat): class A: @staticmethod def to_text(): # Return a valid record return '1 2 3 example.com.' return [A()] dns.resolver.Resolver = _Mock1 del ad.resolver # Test a valid record self.assertEqual( ad._get_srv_records('example.com.'), [SrvRecord(priority=1, weight=2, port=3, srv='example.com')]) class _Mock2: @staticmethod def resolve(hostname, cat): class A: @staticmethod def to_text(): # Return malformed data return 'XXXXXXX' return [A()] dns.resolver.Resolver = _Mock2 del ad.resolver # Test an invalid record self.assertEqual(ad._get_srv_records('example.com'), []) dns.resolver.Resolver = _orig del ad.resolver
def test_autodiscover_redirect(self, m): # Test various aspects of autodiscover redirection. Mock all HTTP responses because we can't force a live server # to send us into the correct code paths. # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post(self.dummy_ews_endpoint, status_code=200) discovery = Autodiscovery( email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials, retry_policy=self.retry_policy, ) discovery.discover() # Make sure we discover a different return address m.post(self.dummy_ad_endpoint, status_code=200, content=b'''\ <?xml version="1.0" encoding="utf-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <User> <AutoDiscoverSMTPAddress>[email protected]</AutoDiscoverSMTPAddress> </User> <Account> <AccountType>email</AccountType> <Action>settings</Action> <Protocol> <Type>EXPR</Type> <EwsUrl>https://expr.example.com/EWS/Exchange.asmx</EwsUrl> </Protocol> </Account> </Response> </Autodiscover>''') # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post('https://expr.example.com/EWS/Exchange.asmx', status_code=200) ad_response, p = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, '*****@*****.**') # Make sure we discover an address redirect to the same domain. We have to mock the same URL with two different # responses. We do that with a response list. redirect_addr_content = b'''\ <?xml version="1.0" encoding="utf-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <Account> <Action>redirectAddr</Action> <RedirectAddr>redirect_me@%s</RedirectAddr> </Account> </Response> </Autodiscover>''' % self.domain.encode() settings_content = b'''\ <?xml version="1.0" encoding="utf-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <User> <AutoDiscoverSMTPAddress>redirected@%s</AutoDiscoverSMTPAddress> </User> <Account> <AccountType>email</AccountType> <Action>settings</Action> <Protocol> <Type>EXPR</Type> <EwsUrl>https://redirected.%s/EWS/Exchange.asmx</EwsUrl> </Protocol> </Account> </Response> </Autodiscover>''' % (self.domain.encode(), self.domain.encode()) # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post('https://redirected.%s/EWS/Exchange.asmx' % self.domain, status_code=200) m.post(self.dummy_ad_endpoint, [ dict(status_code=200, content=redirect_addr_content), dict(status_code=200, content=settings_content), ]) ad_response, p = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, 'redirected@%s' % self.domain) self.assertEqual( ad_response.ews_url, 'https://redirected.%s/EWS/Exchange.asmx' % self.domain) # Test that we catch circular redirects on the same domain with a primed cache. Just mock the endpoint to # return the same redirect response on every request. self.assertEqual(len(autodiscover_cache), 1) m.post(self.dummy_ad_endpoint, status_code=200, content=b'''\ <?xml version="1.0" encoding="utf-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <Account> <Action>redirectAddr</Action> <RedirectAddr>foo@%s</RedirectAddr> </Account> </Response> </Autodiscover>''' % self.domain.encode()) self.assertEqual(len(autodiscover_cache), 1) with self.assertRaises(AutoDiscoverCircularRedirect): discovery.discover() # Test that we also catch circular redirects when cache is empty clear_cache() self.assertEqual(len(autodiscover_cache), 0) with self.assertRaises(AutoDiscoverCircularRedirect): discovery.discover() # Test that we can handle being asked to redirect to an address on a different domain m.post(self.dummy_ad_endpoint, status_code=200, content=b'''\ <?xml version="1.0" encoding="utf-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <Account> <Action>redirectAddr</Action> <RedirectAddr>[email protected]</RedirectAddr> </Account> </Response> </Autodiscover>''') m.post('https://example.com/Autodiscover/Autodiscover.xml', status_code=200, content=b'''\ <?xml version="1.0" encoding="utf-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <User> <AutoDiscoverSMTPAddress>[email protected]</AutoDiscoverSMTPAddress> </User> <Account> <AccountType>email</AccountType> <Action>settings</Action> <Protocol> <Type>EXPR</Type> <EwsUrl>https://redirected.example.com/EWS/Exchange.asmx</EwsUrl> </Protocol> </Account> </Response> </Autodiscover>''') # Also mock the EWS URL. We try to guess its auth method as part of autodiscovery m.post('https://redirected.example.com/EWS/Exchange.asmx', status_code=200) ad_response, p = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, '*****@*****.**') self.assertEqual(ad_response.ews_url, 'https://redirected.example.com/EWS/Exchange.asmx')
def test_autodiscover_redirect(self, m): # Test various aspects of autodiscover redirection. Mock all HTTP responses because we can't force a live server # to send us into the correct code paths. # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) discovery = Autodiscovery( email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials, ) discovery.discover() # Make sure we discover a different return address m.post( self.dummy_ad_endpoint, status_code=200, content=self.settings_xml("*****@*****.**", "https://expr.example.com/EWS/Exchange.asmx"), ) ad_response, _ = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, "*****@*****.**") # Make sure we discover an address redirect to the same domain. We have to mock the same URL with two different # responses. We do that with a response list. m.post( self.dummy_ad_endpoint, [ dict(status_code=200, content=self.redirect_address_xml(f"redirect_me@{self.domain}")), dict( status_code=200, content=self.settings_xml( f"redirected@{self.domain}", f"https://redirected.{self.domain}/EWS/Exchange.asmx" ), ), ], ) ad_response, _ = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, f"redirected@{self.domain}") self.assertEqual(ad_response.protocol.ews_url, f"https://redirected.{self.domain}/EWS/Exchange.asmx") # Test that we catch circular redirects on the same domain with a primed cache. Just mock the endpoint to # return the same redirect response on every request. self.assertEqual(len(autodiscover_cache), 1) m.post(self.dummy_ad_endpoint, status_code=200, content=self.redirect_address_xml(f"foo@{self.domain}")) self.assertEqual(len(autodiscover_cache), 1) with self.assertRaises(AutoDiscoverCircularRedirect): discovery.discover() # Test that we also catch circular redirects when cache is empty clear_cache() self.assertEqual(len(autodiscover_cache), 0) with self.assertRaises(AutoDiscoverCircularRedirect): discovery.discover() # Test that we can handle being asked to redirect to an address on a different domain # Don't use example.com to redirect - it does not resolve or answer on all ISPs ews_hostname = "httpbin.org" redirect_email = f"john@redirected.{ews_hostname}" ews_url = f"https://{ews_hostname}/EWS/Exchange.asmx" m.post(self.dummy_ad_endpoint, status_code=200, content=self.redirect_address_xml(f"john@{ews_hostname}")) m.post( f"https://{ews_hostname}/Autodiscover/Autodiscover.xml", status_code=200, content=self.settings_xml(redirect_email, ews_url), ) ad_response, _ = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, redirect_email) self.assertEqual(ad_response.protocol.ews_url, ews_url) # Test redirect via HTTP 301 clear_cache() redirect_url = f"https://{ews_hostname}/OtherPath/Autodiscover.xml" redirect_email = f"john@otherpath.{ews_hostname}" ews_url = f"https://xxx.{ews_hostname}/EWS/Exchange.asmx" discovery.email = self.account.primary_smtp_address m.post(self.dummy_ad_endpoint, status_code=301, headers=dict(location=redirect_url)) m.post(redirect_url, status_code=200, content=self.settings_xml(redirect_email, ews_url)) m.head(redirect_url, status_code=200) ad_response, _ = discovery.discover() self.assertEqual(ad_response.autodiscover_smtp_address, redirect_email) self.assertEqual(ad_response.protocol.ews_url, ews_url)
def test_autodiscover_cache(self, m): # Mock the default endpoint that we test in step 1 of autodiscovery m.post(self.dummy_ad_endpoint, status_code=200, content=self.dummy_ad_response) discovery = Autodiscovery( email=self.account.primary_smtp_address, credentials=self.account.protocol.credentials, ) # Not cached self.assertNotIn(discovery._cache_key, autodiscover_cache) discovery.discover() # Now it's cached self.assertIn(discovery._cache_key, autodiscover_cache) # Make sure the cache can be looked by value, not by id(). This is important for multi-threading/processing self.assertIn( ( self.account.primary_smtp_address.split("@")[1], Credentials(self.account.protocol.credentials.username, self.account.protocol.credentials.password), True, ), autodiscover_cache, ) # Poison the cache with a failing autodiscover endpoint. discover() must handle this and rebuild the cache p = self.get_test_protocol() autodiscover_cache[discovery._cache_key] = p m.post("https://example.com/Autodiscover/Autodiscover.xml", status_code=404) discovery.discover() self.assertIn(discovery._cache_key, autodiscover_cache) # Make sure that the cache is actually used on the second call to discover() _orig = discovery._step_1 def _mock(slf, *args, **kwargs): raise NotImplementedError() discovery._step_1 = MethodType(_mock, discovery) discovery.discover() # Fake that another thread added the cache entry into the persistent storage but we don't have it in our # in-memory cache. The cache should work anyway. autodiscover_cache._protocols.clear() discovery.discover() discovery._step_1 = _orig # Make sure we can delete cache entries even though we don't have it in our in-memory cache autodiscover_cache._protocols.clear() del autodiscover_cache[discovery._cache_key] # This should also work if the cache does not contain the entry anymore del autodiscover_cache[discovery._cache_key]