Python 用 IMAP 读取邮箱验证码:和租用邮箱 API 怎么选
用 Python 读取邮件验证码,如果读的是你自己的邮箱,标准库 imaplib 最直接:用授权码或应用专用密码登录 IMAP,按发件人和时间找到验证邮件,解析正文,再用正则取出验证码。如果每次都需要一个新的真实地址,又不想维护邮箱和授权码,就改用租用邮箱的 API。
下面的代码只用于你自己有权访问的邮箱,例如团队的测试邮箱。先讲登录凭据,再给 IMAP 示例和 Java 写法,最后用一张表对比两种做法。
准备:IMAP 不能再用登录密码
很多旧教程直接用邮箱密码登录 IMAP,现在大多行不通了:
- Gmail:谷歌说明,个人账号的 IMAP 一直处于开启状态,但 Gmail 已不再支持只靠用户名和密码登录的第三方应用。可选的是 OAuth 2.0(IMAP 的 XOAUTH2 方式),或者在开启两步验证后创建“应用专用密码”。应用专用密码是 16 位,修改谷歌账号密码后会被撤销;只用安全密钥做两步验证、开启了高级保护,或者是工作、学校账号时,可能找不到这个选项。谷歌自己也说,多数情况下不推荐用应用专用密码。服务器是
imap.gmail.com,端口 993,必须用 SSL。 - QQ 邮箱、163 邮箱等国内邮箱:通常要先在网页版的设置里开启 IMAP 服务,拿到一个“授权码”,在程序里用它代替登录密码。开启入口和服务器地址以各邮箱的帮助页为准。
无论哪种,凭据都放在环境变量或密钥管理里,不要写进代码。
imaplib 示例:Python 读取邮箱邮件并找出验证码
import email, imaplib, os, re, time
from email.header import decode_header, make_header
from email.utils import parsedate_to_datetime
HOST = os.environ.get("IMAP_HOST", "imap.gmail.com")
USER = os.environ["IMAP_USER"]
SECRET = os.environ["IMAP_SECRET"] # 授权码或应用专用密码,不是登录密码
MONTHS = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()
NEAR_WORD = re.compile(r"(?:code|验证码|verification)\D{0,40}?(\d{4,8})(?!\d)", re.I)
SIX_DIGITS = re.compile(r"(?<!\d)\d{6}(?!\d)")
def body_text(msg):
"""优先取 text/plain;只有 HTML 时去掉标签。"""
html = ""
for part in msg.walk():
if part.get_content_maintype() == "multipart" or part.get_filename():
continue
raw = part.get_payload(decode=True) or b""
text = raw.decode(part.get_content_charset() or "utf-8", errors="replace")
if part.get_content_type() == "text/plain":
return text
if part.get_content_type() == "text/html":
html = text
return re.sub(r"<[^>]+>", " ", html)
def find_code(sender, since_ts, timeout=180, interval=5):
t = time.gmtime(since_ts)
since = f"{t.tm_mday:02d}-{MONTHS[t.tm_mon - 1]}-{t.tm_year}" # IMAP 日期格式
end = time.time() + timeout
while time.time() < end:
with imaplib.IMAP4_SSL(HOST, 993) as box:
box.login(USER, SECRET)
box.select("INBOX", readonly=True) # 只读,不改变已读状态
_, data = box.search(None, "FROM", sender, "SINCE", since)
for num in reversed(data[0].split()): # 从最新的开始
_, parts = box.fetch(num, "(RFC822)")
msg = email.message_from_bytes(parts[0][1])
if msg["Date"] and parsedate_to_datetime(msg["Date"]).timestamp() < since_ts - 60:
continue # 触发发送之前的旧邮件
subject = str(make_header(decode_header(msg["Subject"] or "")))
text = subject + "\n" + body_text(msg)
m = NEAR_WORD.search(text) or SIX_DIGITS.search(text)
if m:
return m.group(m.lastindex or 0)
time.sleep(interval)
return None
# 用法:先记下时间,再触发网站发送验证码
started = time.time()
# ……在这里提交注册表单……
print(find_code("[email protected]", started))
几个细节,老教程里常常没有:
- IMAP 的
SINCE只认日期,不认时间,而且月份必须是英文缩写。strftime("%b")会跟随程序的区域设置(locale),一旦切换成中文就变成“10月”,搜索随之失败,所以上面手动拼了月份。 - 只按日期筛会拿到当天的旧邮件,所以再用邮件头的
Date和你触发发送的时间比一次,留 60 秒余量。 - 只找“任意 4–8 位数字”很容易误判:邮件里的年份、订单号、邮编都会被抓到。示例先找“code / 验证码”附近的数字,找不到再退回到 6 位数字。
- 中文标题是编码过的,形如
=?utf-8?b?...?=,要用decode_header解码后再匹配。 - 只搜 INBOX 会漏掉进了垃圾箱的邮件。测试邮箱最好加一条规则,把目标网站的发件人放进收件箱。
Gmail 还支持在 IMAP 里直接使用网页版的搜索语法(X-GM-RAW),条件写起来更灵活。例如把上面的搜索换成:
_, data = box.search(None, "X-GM-RAW", '"from:[email protected] newer_than:1d"')
newer_than 只接受天、月、年(d、m、y),所以仍然要靠 Date 再筛一次。
Java:javax.mail 读取邮件
Java 的思路完全一样,用 javax.mail(新版本叫 Jakarta Mail,包名改成了 jakarta.mail):
Session session = Session.getInstance(new Properties());
Store store = session.getStore("imaps");
store.connect("imap.gmail.com", user, appPassword); // 授权码或应用专用密码
Folder inbox = store.getFolder("INBOX");
inbox.open(Folder.READ_ONLY);
Message[] found = inbox.search(new FromStringTerm("[email protected]"));
// 从 found 的最后一封往前看:比较 getSentDate(),取正文,再用正则找验证码
IMAP 和租用邮箱 API:怎么选
| 自己的邮箱 + IMAP | 租用邮箱 API | |
|---|---|---|
| 要准备什么 | 开通 IMAP,管理授权码、应用专用密码或 OAuth | 一个 API 密钥 |
| 地址 | 一个或少数几个固定地址 | 每个订单一个新的真实 @gmail.com 或 @icloud.com 地址 |
| 并行测试 | 多个用例共用一个收件箱,要靠发件人和时间区分 | 每个用例各自一个订单,互不干扰 |
| 解析邮件 | 自己处理 MIME、编码和正则 | 系统已把验证码或链接提取到 otp[].code;提取不出时给出邮件原文 |
| 收不到时 | 只能自己排查 | 到 codeDeadlineAt 没有验证码,订单自动取消并全额退款 |
| 成本 | 不花钱,花维护时间 | 按次付费 |
| 适合 | 读你自己的邮件,少量、串行的测试 | 每次都要新地址、并行测试、注册流程的冒烟测试 |
如果你只是想读自己邮箱里的某封邮件,IMAP 就够了,不需要任何第三方服务。如果瓶颈在“地址”上,比如每次注册都提示邮箱已被使用,或者多个用例的邮件混在一起,再考虑租用邮箱。
同一个任务,换成租用邮箱 API
下面是和上面 IMAP 示例做同一件事的写法:租一个地址,把它填进注册表单,然后轮询验证码。测试你自己的网站时,服务代码用 ot,也就是任意服务;列表里有的网站用它自己的代码。
import time, uuid, requests
HOST = "otpgmail.net"
H = {"Authorization": "Bearer og_YOUR_API_KEY"}
order = requests.post(f"https://{HOST}/v1/orders", json={"service": "ot"},
headers={**H, "Idempotency-Key": str(uuid.uuid4())}, timeout=15).json()["data"][0]
# 把 order["email"] 填进注册表单,然后每 4 秒查一次
while True:
o = requests.get(f"https://{HOST}/v1/orders/{order['orderId']}", headers=H, timeout=15).json()["data"]
if o["otp"] or o["status"] == "cancelled":
break
time.sleep(4)
print(o["otp"][-1]["code"] if o["otp"] else "没有收到,订单已自动取消并退款")
这段为了简短省略了错误处理;错误码怎么处理、怎么提前取消,见用 Python 接收邮箱验证码。最长 25 分钟没有验证码,订单自动取消,费用全额退回余额,规则见退款与质保政策。放进 Playwright、Selenium 或 Cypress 的写法,见自动化测试怎么处理邮箱验证码。
请只读取你有权访问的邮箱,只在你自己的项目或目标平台条款允许的范围内使用。使用前请确认符合该平台的服务条款和你所在地的法律法规;OTPGmail 只提供接收验证邮件的邮箱,不提供绕过平台或地区限制的方法。
常见问题
Python 读取 QQ 邮箱要注意什么?
先在 QQ 邮箱网页版的设置里开启 IMAP 服务,拿到授权码,程序里用授权码登录;中文标题要先用 decode_header 解码再匹配。服务器地址和端口以 QQ 邮箱帮助页为准。
Gmail 用账号密码登录 IMAP 报错怎么办?
谷歌已经不支持只用用户名和密码登录的第三方应用。改用 OAuth 2.0,或者开启两步验证后创建应用专用密码,把它当作密码使用。
为什么脚本搜不到刚收到的验证邮件?
常见原因有三个:邮件进了垃圾箱,而脚本只搜 INBOX;SINCE 的日期按服务器时区计算,跨零点时差了一天;邮件还没到,轮询的总时长太短。
租用邮箱的 API 能读我自己的邮箱吗?
不能。租用邮箱的 API 只返回租到的那个地址收到的验证码;读你自己的邮箱,请用 IMAP。
