如何用Python的socket库发送邮件?多媒体附件编码问题求助
问题与解决方案
问题背景
我正在开发一个纯socket实现的Python邮件客户端,不依赖任何第三方邮件库。目前能正常发送.txt和.docx格式附件,但发送图片(.png/.jpg)、.pdf、.zip时,服务器无法识别文件编码信息,代码运行无报错。
现有代码
import base64 import os import time def get_attachment_content(file_path): with open(file_path, "rb") as file: file_data = file.read() base64_encoded = base64.b64encode(file_data) base64_encoded_str = base64_encoded.decode("utf-8") return base64_encoded_str def guess_mime_type(file_path): # 部分常见文件格式的默认MIME类型 mime_types = { '.txt': 'text/plain', '.pdf': 'application/pdf', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.zip': 'application/zip', } _, file_extension = os.path.splitext(file_path) return mime_types.get(file_extension.lower(), 'application/octet-stream') def generate_email_content(to, from_, subject, message, attachment_file_list, cc_recipients): boundary = ' ------ ' + str(time.time()) + ' ------ ' email_content = '' email_content += f'Content-Type: multipart/mixed; boundary="{boundary}"\r\n' email_content += f"Message-ID: {generate_message_id()}\r\n" email_content += f'Date: {time.strftime("%a, %d %b %Y %H:%M:%S +0700", time.localtime())}\r\n' email_content += f'User-Agent: VMS\r\n' email_content += f'To: {to}\r\n' if len(cc_recipients) > 1: email_content += f'CC: {", ".join(cc_recipients)}\r\n' email_content += f'From: {from_}\r\n' email_content += f'Subject: {subject}\r\n' email_content += f'\r\n{boundary}\r\nContent-Type: text/plain\r\n\r\n{message}\r\n' if attachment_file_list: for attachment_path in attachment_file_list: attachment_content = get_attachment_content(attachment_path) mime_type = guess_mime_type(attachment_path) email_content += f'\r\n{boundary}\r\nContent-Type: {mime_type}\r\n' email_content += f'Content-Transfer-Encoding: base64\r\n' email_content += f'Content-Disposition: attachment; filename= "{os.path.basename(attachment_path)}\"\r\n\r\n{attachment_content}\r\n' email_content += f'{boundary}\r\n' return email_content # 假设generate_message_id是已实现的函数 def generate_message_id(): return f"<{time.time()}@example.com>"
问题原因与修复方案
1. Base64编码未按规范换行
MIME协议要求Base64编码内容每行不超过76个字符,否则邮件服务器无法正确解析。原代码直接返回完整的Base64字符串,会导致长内容超出限制。
修改get_attachment_content函数:
def get_attachment_content(file_path): with open(file_path, "rb") as file: file_data = file.read() base64_encoded = base64.b64encode(file_data) base64_encoded_str = base64_encoded.decode("utf-8") # 按每行76字符分割 return '\r\n'.join([base64_encoded_str[i:i+76] for i in range(0, len(base64_encoded_str), 76)])
2. MIME边界格式错误
原边界包含多余空格,会导致邮件客户端/服务器无法正确识别分隔符。边界应使用无空格的格式,且结束边界需要添加--后缀。
修改generate_email_content中的边界定义:
# 生成标准MIME边界,无多余空格 boundary = f'----Boundary_{int(time.time()*1000)}' # ... # 结束边界需要添加-- email_content += f'\r\n--{boundary}--\r\n'
3. Content-Disposition参数格式错误
filename参数前后的多余空格会导致文件名解析异常,应修正为标准格式:
email_content += f'Content-Disposition: attachment; filename="{os.path.basename(attachment_path)}"\r\n'
4. CC字段判断逻辑错误
原代码判断len(cc_recipients) > 1,会导致只有多个收件人时才添加CC字段,即使有1个CC收件人也会被忽略,修正为:
if cc_recipients: email_content += f'CC: {", ".join(cc_recipients)}\r\n'
5. MIME部分边界前缀缺失
所有MIME部分的边界前必须添加--,原代码遗漏了该前缀,导致服务器无法识别多部分内容的分隔。
修正后的完整代码
import base64 import os import time def get_attachment_content(file_path): with open(file_path, "rb") as file: file_data = file.read() base64_encoded = base64.b64encode(file_data) base64_encoded_str = base64_encoded.decode("utf-8") # 按MIME规范分割Base64内容为每行76字符 return '\r\n'.join([base64_encoded_str[i:i+76] for i in range(0, len(base64_encoded_str), 76)]) def guess_mime_type(file_path): mime_types = { '.txt': 'text/plain', '.pdf': 'application/pdf', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.zip': 'application/zip', } _, file_extension = os.path.splitext(file_path) return mime_types.get(file_extension.lower(), 'application/octet-stream') def generate_message_id(): return f"<{int(time.time()*1000)}@your-domain.com>" def generate_email_content(to, from_, subject, message, attachment_file_list, cc_recipients): # 生成符合规范的MIME边界 boundary = f'----Boundary_{int(time.time()*1000)}' email_content = '' email_content += f'Content-Type: multipart/mixed; boundary="{boundary}"\r\n' email_content += f"Message-ID: {generate_message_id()}\r\n" email_content += f'Date: {time.strftime("%a, %d %b %Y %H:%M:%S +0700", time.localtime())}\r\n' email_content += f'User-Agent: VMS\r\n' email_content += f'To: {to}\r\n' # 修正CC字段判断逻辑 if cc_recipients: email_content += f'CC: {", ".join(cc_recipients)}\r\n' email_content += f'From: {from_}\r\n' email_content += f'Subject: {subject}\r\n' # 头部与正文的分隔空行,然后添加第一个MIME部分 email_content += f'\r\n--{boundary}\r\nContent-Type: text/plain\r\n\r\n{message}\r\n' if attachment_file_list: for attachment_path in attachment_file_list: attachment_content = get_attachment_content(attachment_path) mime_type = guess_mime_type(attachment_path) # 附件部分的边界前缀要加-- email_content += f'\r\n--{boundary}\r\nContent-Type: {mime_type}\r\n' email_content += f'Content-Transfer-Encoding: base64\r\n' # 修正filename参数格式 email_content += f'Content-Disposition: attachment; filename="{os.path.basename(attachment_path)}"\r\n\r\n{attachment_content}\r\n' # 结束边界,必须添加--后缀 email_content += f'\r\n--{boundary}--\r\n' return email_content
内容的提问来源于stack exchange,提问作者Quốc Huy
相关产品推荐
相关产品推荐

