使用exchangelib下载Outlook邮件时触发WinError 10054连接中断问题
处理Outlook Exchange连接被强制关闭(WinError 10054)问题
使用Python的exchangelib从Outlook收件箱下载邮件时,每次处理到第100封邮件就会触发WinError 10054错误,连接被远程主机强制关闭,错误日志如下:
Account ------------@------.com: Exception in _get_elements: Traceback (most recent call last): File "C:\Program Files\Python38\lib\site-packages\urllib3\connectionpool.py", line 665, in urlopen httplib_response = self._make_request( File "C:\Program Files\Python38\lib\site-packages\urllib3\connectionpool.py", line 421, in _make_request six.raise_from(e, None) File "<string>", line 3, in raise_from File "C:\Program Files\Python38\lib\site-packages\urllib3\connectionpool.py", line 416, in _make_request httplib_response = conn.getresponse() File "C:\Program Files\Python38\lib\http\client.py", line 1322, in getresponse response.begin() File "C:\Program Files\Python38\lib\http\client.py", line 303, in begin version, status, reason = self._read_status() File "C:\Program Files\Python38\lib\http\client.py", line 264, in _read_status line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") File "C:\Program Files\Python38\lib\socket.py", line 669, in readinto return self._sock.recv_into(b) File "C:\Program Files\Python38\lib\ssl.py", line 1241, in recv_into return self.read(nbytes, buffer) File "C:\Program Files\Python38\lib\ssl.py", line 1099, in read return self._sslobj.read(len, buffer) ConnectionResetError: [WinError 10054] An existing connection was forcibly closed by the remote host During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Program Files\Python38\lib\site-packages\requests\adapters.py", line 439, in send resp = conn.urlopen( File "C:\Program Files\Python38\lib\site-packages\urllib3\connectionpool.py", line 719, in urlopen retries = retries.increment( File "C:\Program Files\Python38\lib\site-packages\urllib3\util\retry.py", line 400, in increment raise six.reraise(type(error), error, _stacktrace) File "C:\Program Files\Python38\lib\site-packages\urllib3\packages\six.py", line 734, in reraise raise value.with_traceback(tb) File "C:\Program Files\Python38\lib\site-packages\urllib3\connectionpool.py", line 665, in urlopen httplib_response = self._make_request( File "C:\Program Files\Python38\lib\site-packages\urllib3\connectionpool.py", line 421, in _make_request six.raise_from(e, None) File "<string>", line 3, in raise_from File "C:\Program Files\Python38\lib\site-packages\urllib3\connectionpool.py", line 416, in _make_request httplib_response = conn.getresponse() File "C:\Program Files\Python38\lib\http\client.py", line 1322, in getresponse response.begin() File "C:\Program Files\Python38\lib\http\client.py", line 303, in begin version, status, reason = self._read_status() File "C:\Program Files\Python38\lib\http\client.py", line 264, in _read_status line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") File "C:\Program Files\Python38\lib\socket.py", line 669, in readinto return self._sock.recv_into(b) File "C:\Program Files\Python38\lib\ssl.py", line 1241, in recv_into return self.read(nbytes, buffer) File "C:\Program Files\Python38\lib\ssl.py", line 1099, in read return self._sslobj.read(len, buffer) urllib3.exceptions.ProtocolError: ('Connection aborted.', ConnectionResetError(10054, 'An existing connection was forcibly closed by the remote host', None, 10054, None)) During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Program Files\Python38\lib\site-packages\exchangelib\services\common.py", line 330, in _get_elements yield from self._response_generator(payload=payload) File "C:\Program Files\Python38\lib\site-packages\exchangelib\services\common.py", line 292, in _response_generator response = self._get_response_xml(payload=payload) File "C:\Program Files\Python38\lib\site-packages\exchangelib\services\common.py", line 416, in _get_response_xml r = self._get_response(payload=payload, api_version=api_version) File "C:\Program Files\Python38\lib\site-packages\exchangelib\services\common.py", line 370, in _get_response r, session = post_ratelimited( File "C:\Program Files\Python38\lib\site-packages\exchangelib\util.py", line 917, in post_ratelimited protocol.retry_policy.raise_response_errors(r) # Always raises an exception File "C:\Program Files\Python38\lib\site-packages\exchangelib\protocol.py", line 700, in raise_response_errors raise response.headers["TimeoutException"] File "C:\Program Files\Python38\lib\site-packages\exchangelib\util.py", line 843, in post_ratelimited r = session.post( File "C:\Program Files\Python38\lib\site-packages\requests\sessions.py", line 581, in post return self.request('POST', url, data=data, json=json, **kwargs) File "C:\Program Files\Python38\lib\site-packages\requests_oauthlib\oauth2_session.py", line 515, in request return super(OAuth2Session, self).request( File "C:\Program Files\Python38\lib\site-packages\requests\sessions.py", line 533, in request resp = self.send(prep, **send_kwargs) File "C:\Program Files\Python38\lib\site-packages\requests\sessions.py", line 646, in send r = adapter.send(request, **kwargs) File "C:\Program Files\Python38\lib\site-packages\requests\adapters.py", line 498, in send raise ConnectionError(err, request=request) requests.exceptions.ConnectionError: ('Connection aborted.', ConnectionResetError(10054, 'An existing connection was forcibly closed by the remote host', None, 10054, None))
引发问题的代码块:
for aNum, item in enumerate(unprocessed_emails.filter(subject__contains='Notification').order_by('-datetime_received')): emailBody = item.body emailBody = emailBody.split('has invited you to view:<br><a href="') emailSubject = item.subject link = ((emailBody[1]).split('" originalsrc="'))[0] #Opens link which auto-downloads file #webbrowser.open_new(link) r = requests.get(link, allow_redirects=True) try: sleep(2) eMessage = f"{aNum} of {emailCount}: Processing email with subject: {emailSubject}" myLogger.info(msg=eMessage) filename = getFilename_fromCd(r.headers.get('content-disposition')).replace(":","_") except Exception as e: exceptionContent = getFilename_fromCd(r.headers.get('content-disposition')) eMessage = str('Connection to Office365 Exchange API Failed\n' + 'Error Message: ' + str(e) + '\nContent that caused exception:\n{exceptionContent}\nContinuing...') myLogger.critical(msg=eMessage) supportMessage = f"" try: sendEmailToSupport(supportMessage) except Exception as e2: eMessage2 = str('Attempt to send email to support failed. Error Message: ' + str(e)) myLogger.critical(msg=eMessage2) continue continue print(filename) if "event" in emailSubject.lower(): with open(os.path.join(SourcePath, filename), 'wb') as aFile: aFile.write(r.content) elif "alarm" in emailSubject.lower(): with open(os.path.join(ASourcePath, filename), 'wb') as aFile: aFile.write(r.content) print(reportCounter) reportCounter += 1
问题根源
这是Exchange服务器的单连接请求阈值限制导致的:Exchange默认对单个TCP连接的请求次数设置了上限(通常为100次),当超过这个次数后,服务器会强制断开连接以防止滥用。代码在遍历邮件时,每次访问item.body、item.subject等属性都会触发一次单独的API请求,累计到100次时就会触发断开。
解决方案
1. 批量预加载邮件属性
在查询时显式指定需要加载的属性,避免每次访问字段都触发新请求:
# 预加载所需属性,减少API请求次数 target_emails = unprocessed_emails.filter( subject__contains='Notification' ).order_by('-datetime_received').only('body', 'subject', 'datetime_received') for aNum, item in enumerate(target_emails): # 后续处理逻辑不变
2. 配置exchangelib重试策略
让exchangelib在连接断开时自动重试并重建连接:
from exchangelib import Account, Protocol, RetryPolicy # 自定义重试策略,包含连接错误重试 retry_policy = RetryPolicy( backoff_factor=1, status_codes=[500, 502, 503, 504, 429], connection_errors=True # 允许重试连接断开类错误 ) # 初始化带重试策略的协议 protocol = Protocol( server='outlook.office365.com', credentials=your_credentials, retry_policy=retry_policy ) account = Account( primary_smtp_address='your_email@domain.com', protocol=protocol, autodiscover=False )
3. 拆分批量处理
每处理一批邮件(数量小于100)后,手动刷新会话重建连接:
batch_size = 90 # 小于服务器阈值 total_emails = target_emails.count() for batch_start in range(0, total_emails, batch_size): batch = target_emails[batch_start:batch_start+batch_size] for aNum, item in enumerate(batch, start=batch_start): # 原处理逻辑... # 处理完一批后刷新会话 account.protocol.refresh_session()
4. 复用requests连接
使用requests.Session()复用HTTP连接,减少额外的连接开销:
import requests # 创建会话复用连接 requests_session = requests.Session() for aNum, item in enumerate(target_emails): # ... r = requests_session.get(link, allow_redirects=True) # ...
内容的提问来源于stack exchange,提问作者Pramit Bhatia
相关产品推荐
相关产品推荐

