@@ -1065,6 +1065,159 @@ def uid_side_effect(cmd, uid_list, fetch_type):
10651065 # Should still get results via fallback
10661066 assert len (emails ) == 2
10671067
1068+ @pytest .mark .asyncio
1069+ async def test_get_emails_stream_sort_empty_response (self , email_client ):
1070+ """Test handling of empty SORT response (covers 451-453)."""
1071+ mock_imap = AsyncMock ()
1072+ mock_imap ._client_task = asyncio .Future ()
1073+ mock_imap ._client_task .set_result (None )
1074+ mock_imap .wait_hello_from_server = AsyncMock ()
1075+ mock_imap .login = AsyncMock ()
1076+ mock_imap .select = AsyncMock ()
1077+ mock_imap .logout = AsyncMock ()
1078+
1079+ mock_protocol = MagicMock ()
1080+ mock_protocol .capabilities = {"SORT" , "IMAP4rev1" }
1081+ mock_imap .protocol = mock_protocol
1082+
1083+ # SORT returns empty/None response
1084+ mock_imap .uid = AsyncMock (return_value = (None , [None ]))
1085+
1086+ with patch .object (email_client , "imap_class" , return_value = mock_imap ):
1087+ emails = []
1088+ async for email_data in email_client .get_emails_metadata_stream (page = 1 , page_size = 10 ):
1089+ emails .append (email_data )
1090+
1091+ assert len (emails ) == 0
1092+
1093+ @pytest .mark .asyncio
1094+ async def test_get_emails_stream_sort_empty_page (self , email_client ):
1095+ """Test handling when pagination results in empty page with SORT (covers 463)."""
1096+ mock_imap = AsyncMock ()
1097+ mock_imap ._client_task = asyncio .Future ()
1098+ mock_imap ._client_task .set_result (None )
1099+ mock_imap .wait_hello_from_server = AsyncMock ()
1100+ mock_imap .login = AsyncMock ()
1101+ mock_imap .select = AsyncMock ()
1102+ mock_imap .logout = AsyncMock ()
1103+
1104+ mock_protocol = MagicMock ()
1105+ mock_protocol .capabilities = {"SORT" , "IMAP4rev1" }
1106+ mock_imap .protocol = mock_protocol
1107+
1108+ # Only 2 emails, but request page 10
1109+ sort_response = [b"1 2" ]
1110+ mock_imap .uid = AsyncMock (return_value = (None , sort_response ))
1111+
1112+ with patch .object (email_client , "imap_class" , return_value = mock_imap ):
1113+ emails = []
1114+ async for email_data in email_client .get_emails_metadata_stream (page = 10 , page_size = 10 ):
1115+ emails .append (email_data )
1116+
1117+ assert len (emails ) == 0
1118+
1119+ @pytest .mark .asyncio
1120+ async def test_get_emails_stream_empty_email_ids_after_split (self , email_client ):
1121+ """Test handling when email_ids is empty after split (covers 494-495)."""
1122+ mock_imap = AsyncMock ()
1123+ mock_imap ._client_task = asyncio .Future ()
1124+ mock_imap ._client_task .set_result (None )
1125+ mock_imap .wait_hello_from_server = AsyncMock ()
1126+ mock_imap .login = AsyncMock ()
1127+ mock_imap .select = AsyncMock ()
1128+ # Return non-empty response that splits to empty list
1129+ mock_imap .uid_search = AsyncMock (return_value = (None , [b" " ]))
1130+ mock_imap .logout = AsyncMock ()
1131+
1132+ mock_protocol = MagicMock ()
1133+ mock_protocol .capabilities = set () # No SORT
1134+ mock_imap .protocol = mock_protocol
1135+
1136+ with patch .object (email_client , "imap_class" , return_value = mock_imap ):
1137+ emails = []
1138+ async for email_data in email_client .get_emails_metadata_stream (page = 1 , page_size = 10 ):
1139+ emails .append (email_data )
1140+
1141+ assert len (emails ) == 0
1142+
1143+ @pytest .mark .asyncio
1144+ async def test_get_emails_stream_fallback_empty_page (self , email_client ):
1145+ """Test handling when pagination results in empty page in fallback path (covers 518)."""
1146+ mock_imap = AsyncMock ()
1147+ mock_imap ._client_task = asyncio .Future ()
1148+ mock_imap ._client_task .set_result (None )
1149+ mock_imap .wait_hello_from_server = AsyncMock ()
1150+ mock_imap .login = AsyncMock ()
1151+ mock_imap .select = AsyncMock ()
1152+ mock_imap .uid_search = AsyncMock (return_value = (None , [b"1 2" ]))
1153+ mock_imap .logout = AsyncMock ()
1154+
1155+ mock_protocol = MagicMock ()
1156+ mock_protocol .capabilities = set () # No SORT
1157+ mock_imap .protocol = mock_protocol
1158+
1159+ date_response = [
1160+ b"1 FETCH (UID 1 BODY[HEADER.FIELDS (DATE)] {30}" ,
1161+ bytearray (b"Date: Mon, 1 Jan 2024 00:00:00 +0000\r \n " ),
1162+ b"2 FETCH (UID 2 BODY[HEADER.FIELDS (DATE)] {30}" ,
1163+ bytearray (b"Date: Tue, 2 Jan 2024 00:00:00 +0000\r \n " ),
1164+ ]
1165+
1166+ mock_imap .uid = AsyncMock (return_value = (None , date_response ))
1167+
1168+ with patch .object (email_client , "imap_class" , return_value = mock_imap ):
1169+ emails = []
1170+ # Request page 10 when only 2 emails exist
1171+ async for email_data in email_client .get_emails_metadata_stream (page = 10 , page_size = 10 ):
1172+ emails .append (email_data )
1173+
1174+ assert len (emails ) == 0
1175+
1176+ @pytest .mark .asyncio
1177+ async def test_get_emails_stream_logout_error (self , email_client ):
1178+ """Test handling of logout error (covers 534-535)."""
1179+ mock_imap = AsyncMock ()
1180+ mock_imap ._client_task = asyncio .Future ()
1181+ mock_imap ._client_task .set_result (None )
1182+ mock_imap .wait_hello_from_server = AsyncMock ()
1183+ mock_imap .login = AsyncMock ()
1184+ mock_imap .select = AsyncMock ()
1185+ mock_imap .uid_search = AsyncMock (return_value = (None , [b"1" ]))
1186+ # Logout raises an error
1187+ mock_imap .logout = AsyncMock (side_effect = Exception ("Connection lost" ))
1188+
1189+ mock_protocol = MagicMock ()
1190+ mock_protocol .capabilities = set ()
1191+ mock_imap .protocol = mock_protocol
1192+
1193+ date_response = [
1194+ b"1 FETCH (UID 1 BODY[HEADER.FIELDS (DATE)] {30}" ,
1195+ bytearray (b"Date: Mon, 1 Jan 2024 00:00:00 +0000\r \n " ),
1196+ ]
1197+ header_response = [
1198+ b"1 FETCH (UID 1 BODY[HEADER] {100}" ,
1199+ bytearray (
1200+ b"From: sender@example.com\r \n To: recipient@example.com\r \n Subject: Test\r \n Date: Mon, 1 Jan 2024 00:00:00 +0000\r \n \r \n "
1201+ ),
1202+ ]
1203+
1204+ def uid_side_effect (cmd , uid_list , fetch_type ):
1205+ if "HEADER.FIELDS" in fetch_type :
1206+ return (None , date_response )
1207+ else :
1208+ return (None , header_response )
1209+
1210+ mock_imap .uid = AsyncMock (side_effect = uid_side_effect )
1211+
1212+ with patch .object (email_client , "imap_class" , return_value = mock_imap ):
1213+ emails = []
1214+ # Should complete successfully despite logout error
1215+ async for email_data in email_client .get_emails_metadata_stream (page = 1 , page_size = 10 ):
1216+ emails .append (email_data )
1217+
1218+ assert len (emails ) == 1
1219+ assert emails [0 ]["email_id" ] == "1"
1220+
10681221
10691222class TestDeleteEmails :
10701223 """Tests for delete_emails functionality."""
0 commit comments