一、前言
一周前,我发了一篇《Python应用开发学习:做一个邮件发送工具,实现带附件及延时发送》的日志,记录了我借助DeepSeek做了一个发送电子邮件的小工具,方便我在公司内部网络进行ip限制的情况下,能够通过QQ邮箱的stmp服务发送邮件。但还没解决在公司内网下载QQ、163、新浪等邮箱里的邮件的问题。这次我又把问题抛给DeepSeek,让它帮我写代码,效率高了很多。虽然在测试时出现了一些问题,最终目标只实现了一部分,但总比啥都不能做要强。
二、功能描述
我最初的设想是在之前开发的收件小工具上进行扩展,希望能下载QQ邮箱、163邮箱、新浪邮箱的邮件,并将邮件内容及其附件保存到本地。在开发过程中,通过DeepSeek还添加了收件选项,能够选择(设置)开始时间,下载邮件数量,只下载未读邮件,将已下载的邮件标记为已读。并可以指定邮件保存路径。
三、借助DeepSeek开发软件
我主要借助DeepSeek编写逻辑部分的代码,之前已经通过它完成了发送邮件的逻辑代码了。这次继续提问要求它写个下载邮件及其附件的代码。
DeepSeek给出了一段200多行的代码,用到了imaplib库和email库。运行代码,根据提示依次输入邮箱地址、授权码、下载目录、最大下载数、开始日期,之后执行下载程序,由程序自动从邮箱下载邮件,并将邮件保存到指定目录下。在目录下每个邮件又建有一个邮件目录,邮件目录下有邮件信息文件、邮件正文文件、附件。
DeepSeek不仅给出了代码,还给出了一些说明,看到这些说明对代码会更容易理解。另外,要使用这段代码,需要获取到QQ邮箱、163邮箱、新浪邮箱的imap服务的授权码。
之后我又提出了希望添加只下载未读邮件的功能,DeepSeek给下载邮件的函数添加了参数,并修改了搜索条件。
这段代码,我用QQ邮箱测试后,发现下载了未读邮件后不能将其标注为已读(如下图)。
而DeepSeek给出的注意事项里告诉了我“ 如果需要将邮件标记为已读,需要额外添加 mail.store(email_id, '+FLAGS', '\\Seen')
命令 ”。我直接问它怎么添加。
再次对新生成的代码进行测试,能够将已下载的邮件标记为已读了。
至此,下载邮件的逻辑代码部分基本完成了。
四、功能实现
我将新得到的代码与之前得到的发送邮件的代码进行整合,方在一个email_module.py文件里。再通过wxFormBuilder对之前制作的GUI进行修改,已适应当前的软件需求。最后通过pycharm完成代码的整合,得到如下的小工具。
在测试过程中,出现了几个问题。
1、QQ邮箱不能选择起始日期,否则,无法下载到任何的邮件。
(虽然搜到了82封邮件,但下载数为0)
2、163邮箱无法正常下载到邮件。
返回的错误信息是:[b'SELECT Unsafe Login. Please contact kefu@188.com for help']
我将问题反馈给DeepSeek,它的回复大致意思是:
这通常是由于 163 邮箱的安全机制导致的,特别是当使用 IMAP 登录时。虽然给出了一些建议,但似乎,它也无法解决这个问题。
最后对新浪邮箱的测试比较正常,顺利下载到了邮件。
(在指定的下载路径下会看到多个子文件夹,每个文件夹就是一封邮件。)
(每个子文件夹下有三个文件分别是信息、正文、附件)
这个软件存在的问题似乎不是DeepSeek给出的代码的问题,而是QQ邮箱或163邮箱的设置问题。当前,DeepSeek也没有给出有效的方案。只能算是部分成功了,先这样吧。
五、代码展示
最后放上DeepSeek生成的代码供参考,此段代码运行后执行main函数,提示用户输入邮箱地址、授权码、下载路径、最大下载数量、开始日期等关键参数,程序会调用download_emails函数自动下载邮件,并保存到指定的路径下。
# -*- coding: UTF-8 -*-
import imaplib
import email
import os
import re
from email.header import decode_header
from datetime import datetime
import time
def get_email_service_config(email_address):
"""根据邮箱地址返回对应的IMAP服务器配置"""
if '@qq.com' in email_address:
return {
'imap_server': 'imap.qq.com',
'port': 993,
'use_ssl': True
}
elif '@163.com' in email_address:
return {
'imap_server': 'imap.163.com',
'port': 993,
'use_ssl': True
}
elif '@sina.com' in email_address:
return {
'imap_server': 'imap.sina.com',
'port': 993,
'use_ssl': True
}
else:
raise ValueError(f"不支持的邮箱类型: {email_address}")
def clean_filename(filename):
"""清理文件名,移除非法字符"""
if not filename:
return "unknown"
# 尝试解码编码过的文件名
if filename.startswith('=?') and filename.endswith('?='):
decoded = decode_header(filename)
if decoded and decoded[0][0]:
filename = decoded[0][0]
if isinstance(filename, bytes):
filename = filename.decode(decoded[0][1] or 'utf-8', errors='ignore')
# 移除特殊字符
cleaned = re.sub(r'[\\/*?:"<>|]', '', filename)
# 截断过长的文件名
return cleaned[:100] if cleaned else "attachment"
def download_emails(email_address, password, output_dir, max_emails=10, since_date=None, only_unread=False, mark_as_read=False):
"""
从邮箱下载邮件及其附件
参数:
email_address: 邮箱地址
password: 邮箱授权码
output_dir: 下载文件保存目录
max_emails: 最多下载的邮件数量
since_date: 只下载此日期之后的邮件 (格式: "YYYY-MM-DD")
only_unread: 是否只下载未读邮件
mark_as_read: 是否将下载的邮件标记为已读
"""
# 验证参数
if not email_address or not password:
print("错误: 邮箱地址和授权码不能为空")
return
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 获取邮箱配置
try:
config = get_email_service_config(email_address)
except ValueError as e:
print(e)
return
# 连接邮箱服务器
try:
if config['use_ssl']:
mail = imaplib.IMAP4_SSL(config['imap_server'], config['port'])
else:
mail = imaplib.IMAP4(config['imap_server'], config['port'])
mail.login(email_address, password)
print(f"成功登录到 {email_address}")
except Exception as e:
print(f"登录失败: {e}")
return
# 选择收件箱 - 修复选择状态问题
try:
status, data = mail.select('inbox')
if status != 'OK':
print(f"选择收件箱失败: {data}")
mail.logout()
return
print(f"收件箱选择成功,共有 {data[0].decode()} 封邮件")
except Exception as e:
print(f"选择收件箱失败: {e}")
mail.logout()
return
# 构建搜索条件
search_criteria = 'ALL'
criteria_parts = []
# 添加日期筛选
if since_date:
try:
# 转换日期格式为 IMAP 格式 (dd-MMM-yyyy)
dt = datetime.strptime(since_date, "%Y-%m-%d")
imap_date = dt.strftime("%d-%b-%Y")
criteria_parts.append(f'SINCE "{imap_date}"')
except ValueError:
print(f"警告: 无效的日期格式 {since_date}, 忽略日期筛选")
# 添加未读邮件筛选
if only_unread:
criteria_parts.append('UNSEEN')
# 组合搜索条件
if criteria_parts:
search_criteria = f'({" ".join(criteria_parts)})'
print(f"搜索条件: {search_criteria}")
# 搜索邮件 - 添加状态检查
try:
status, messages = mail.search(None, search_criteria)
if status != 'OK':
print(f"搜索邮件失败: {messages}")
mail.logout()
return
# 获取邮件ID列表
email_ids = messages[0].split()
total_emails = len(email_ids)
print(f"找到 {total_emails} 封符合条件的邮件")
# 限制下载数量
if max_emails > 0 and total_emails > max_emails:
email_ids = email_ids[-max_emails:] # 下载最新的N封邮件
print(f"将下载最新的 {max_emails} 封邮件")
except Exception as e:
print(f"邮件搜索出错: {e}")
mail.logout()
return
# 下载邮件
downloaded_count = 0
for i, email_id in enumerate(reversed(email_ids), 1): # 从最新邮件开始
try:
# 获取邮件内容 - 添加状态检查
status, msg_data = mail.fetch(email_id, '(RFC822)')
if status != 'OK':
print(f"获取邮件 {email_id} 失败: {msg_data}")
continue
# 解析邮件
msg = email.message_from_bytes(msg_data[0][1])
# 创建邮件保存目录
email_dir = os.path.join(output_dir, f"email_{email_id.decode()}")
if not os.path.exists(email_dir):
os.makedirs(email_dir)
# 保存邮件基本信息
subject, encoding = decode_header(msg["Subject"])[0] if msg["Subject"] else ("无主题", None)
if isinstance(subject, bytes):
subject = subject.decode(encoding or 'utf-8', errors='ignore')
from_email = msg.get("From", "未知发件人")
date = msg.get("Date", "未知日期")
info_file = os.path.join(email_dir, "email_info.txt")
with open(info_file, 'w', encoding='utf-8') as f:
f.write(f"主题: {subject}\n")
f.write(f"发件人: {from_email}\n")
f.write(f"日期: {date}\n")
f.write(f"收件人: {msg.get('To', '')}\n")
f.write(f"邮件ID: {email_id.decode()}\n")
f.write(f"是否未读: {'是' if only_unread else '未知'}\n")
f.write(f"是否标记为已读: {'是' if mark_as_read else '否'}\n\n")
# 保存邮件正文
body_file = os.path.join(email_dir, "email_body.txt")
body_content = ""
# 解析邮件内容
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))
# 保存附件
if "attachment" in content_disposition:
filename = part.get_filename()
if filename:
filename = clean_filename(filename)
attachment_path = os.path.join(email_dir, filename)
with open(attachment_path, "wb") as f:
f.write(part.get_payload(decode=True))
print(f" - 附件已保存: {filename}")
# 提取文本内容
elif content_type in ["text/plain", "text/html"]:
try:
payload = part.get_payload(decode=True)
charset = part.get_content_charset() or 'utf-8'
text = payload.decode(charset, errors='replace')
if content_type == "text/plain":
body_content += text + "\n"
elif content_type == "text/html":
# 保存HTML内容到单独文件
html_path = os.path.join(email_dir, "email_body.html")
with open(html_path, "w", encoding='utf-8') as f:
f.write(text)
except Exception as e:
print(f"解析邮件内容出错: {e}")
# 保存纯文本正文
if body_content:
with open(body_file, "w", encoding='utf-8') as f:
f.write(body_content)
# 标记邮件为已读
if mark_as_read:
try:
result = mail.store(email_id, '+FLAGS', '\\Seen')
if result[0] == 'OK':
print(f" - 邮件已标记为已读")
else:
print(f" - 标记为已读失败: {result[1]}")
except Exception as e:
print(f" - 标记为已读失败: {e}")
downloaded_count += 1
print(f"({i}/{len(email_ids)}) 邮件已保存: {subject}")
except Exception as e:
print(f"处理邮件 {email_id} 时出错: {e}")
mail.logout()
print(f"\n下载完成! 共保存了 {downloaded_count} 封邮件到目录: {output_dir}")
def main():
"""主函数,提供用户交互界面"""
print("\n===== 邮箱邮件下载工具 =====")
# 用户输入
email_address = input("请输入邮箱地址: ").strip()
password = input("请输入邮箱授权码: ").strip()
# 设置输出目录
default_dir = f"emails_{int(time.time())}"
output_dir = input(f"请输入保存目录 (默认为 {default_dir}): ").strip()
if not output_dir:
output_dir = default_dir
# 设置下载选项
max_emails = 10
try:
max_input = input("请输入最多下载邮件数 (默认为10): ").strip()
if max_input:
max_emails = max(1, min(100, int(max_input)))
except:
print("输入无效,使用默认值10")
since_date = None
date_input = input("请输入起始日期 (YYYY-MM-DD, 默认为所有邮件): ").strip()
if date_input:
try:
datetime.strptime(date_input, "%Y-%m-%d")
since_date = date_input
except ValueError:
print("日期格式无效,将下载所有邮件")
# 添加只下载未读邮件选项
only_unread = False
unread_input = input("是否只下载未读邮件? (y/n, 默认为n): ").strip().lower()
if unread_input == 'y' or unread_input == 'yes':
only_unread = True
print("将只下载未读邮件")
# 添加标记为已读选项
mark_as_read = False
read_input = input("是否将下载的邮件标记为已读? (y/n, 默认为n): ").strip().lower()
if read_input == 'y' or read_input == 'yes':
mark_as_read = True
print("下载的邮件将被标记为已读")
# 开始下载
print("\n开始下载邮件...")
download_emails(
email_address=email_address,
password=password,
output_dir=output_dir,
max_emails=max_emails,
since_date=since_date,
only_unread=only_unread,
mark_as_read=mark_as_read
)
if __name__ == "__main__":
main()