You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 19:19:56