简介:这份Android源码资源聚焦于不依赖系统Mail客户端的邮件发送方案,面向需要集成邮件功能的Android开发者,尤其适合希望摆脱第三方邮箱App限制、直接通过SMTP协议向任意邮箱投递邮件的进阶学习者。资源包共60个文件,涵盖class、xml、png、jar、java等类型,其中jar包提供JavaMail与Android支持库,xml与java文件构成界面布局与核心逻辑,png为图标资源,压缩包整体约3.81MB,结构完整可直接导入Eclipse工程。源码对比了调用系统Mail客户端与使用SMTP两种实现路径,重点演示后者如何借助mail.jar、activation.jar等依赖完成邮件中转,并附带可运行的APK与帮助文档,便于读者对照调试、理解SMTP在TCP/IP协议簇中的定位与配置要点。目前已有2045人学习下载,适合作为Android网络编程与邮件功能开发的实战参考。
1. 任意邮箱发信:为什么你的代码总在“最后一公里”翻车
做过邮件通知的人多半有过这种经历:本地用某公共邮箱的 SMTP 跑得好好的,一上生产、一换企业邮箱、一换云厂商的邮件推送服务,就开始报535 Authentication failed、554 Message rejected、Connection timed out。标题里说的“支持任意邮箱发送邮件功能”,本质不是让你去写一个 SMTP 协议栈,而是让同一套业务代码,能在 QQ 邮箱、163、企业自建邮局、云厂商邮件推送之间平滑切换,发件人换一个配置就能跑,而不是换一次邮箱改一遍代码。
这件事的价值在于解耦:业务层只关心“给谁发、发什么”,传输层只关心“用哪个通道、怎么认证”。适合谁?做后台告警、用户注册验证码、订单通知、报表定时推送的开发者,尤其是那种一开始图省事把 SMTP 地址硬编码在业务逻辑里,后来被运维追着改配置的人。下面按“先立协议模型,再落地实现,最后讲坑”的顺序拆开讲,代码以 Python 为主,思路换任何语言都成立。
2. 把“任意邮箱”抽象成配置:SMTP 模型与选型理由
2.1 为什么不是每个邮箱写一套 if-else
很多人第一版代码长这样:如果是 QQ 邮箱就走smtp.qq.com:465,如果是 163 就走smtp.163.com:25,如果是企业邮局再写一个分支。这种写法在第三个邮箱接入时就会失控,因为真正变化的只有五个参数:服务器地址、端口、加密方式、账号、授权码。把这五个抽成配置,业务代码就只剩一个send()调用。
SMTP 本身是标准协议,差异不在协议,而在各家服务商的“附加规则”:有的强制要求发件人地址与登录账号一致,有的对From显示名有编码要求,有的对单日发信量有硬限制。所以抽象的目标不是抹平协议,而是把“协议参数”和“服务商策略”分开管理。
2.2 三个必须搞清的协议参数
| 参数 | 常见取值 | 说明 |
|---|---|---|
| 端口 + 加密 | 465 / SSL,587 / STARTTLS,25 / 明文 | 465 是隐式 SSL,587 是先明文再升级,25 基本被云厂商封禁 |
| 认证方式 | LOGIN / PLAIN | 多数邮箱用授权码而非登录密码,认证失败先查这里 |
| 发件人一致性 | From 必须等于登录账号 | 企业邮局常要求别名与账号绑定,否则 554 |
选型上,我一般优先 465 + SSL,因为它连接即加密,不用处理 STARTTLS 升级失败的分支;只有在服务商明确只开 587 时才用 STARTTLS。25 端口在云主机上大概率被出口策略拦掉,别在这上面浪费时间。
2.3 用一份配置驱动所有邮箱
# mail_config.py # 每个邮箱一条配置,业务代码只认这个结构 MAIL_PROFILES = { "default": { "host": "smtp.example-mail.com", # SMTP 服务器地址 "port": 465, # 465 走 SSL "use_ssl": True, # 隐式 SSL "user": "noreply@example-mail.com", # 登录账号 "password": "AUTHORIZATION_CODE", # 授权码,不是登录密码 "sender": "noreply@example-mail.com", # 发件人,通常与 user 一致 "sender_name": "系统通知", # 显示名,中文需编码 }, "backup": { "host": "smtp.backup-mail.com", "port": 587, "use_ssl": False, # 587 走 STARTTLS "user": "alert@backup-mail.com", "password": "ANOTHER_CODE", "sender": "alert@backup-mail.com", "sender_name": "Backup Alert", }, }这段配置的关键点是:use_ssl决定用SMTP_SSL还是SMTP+starttls(),sender单独列出是因为有些企业邮局允许登录账号和发件别名不同。密码字段存授权码,很多新手在这里填登录密码,结果一直 535,这是最高频的翻车点。
3. 从配置到发信:一个可复用的发送器怎么写
3.1 最小可用发送函数
# mailer.py import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.header import Header from email.utils import formataddr from mail_config import MAIL_PROFILES def send_mail(profile_name, to_addrs, subject, html_body, text_body=None): cfg = MAIL_PROFILES[profile_name] # 用 alternative 同时挂纯文本和 HTML,客户端自己选 msg = MIMEMultipart("alternative") msg["Subject"] = Header(subject, "utf-8") # formataddr 处理中文显示名,避免乱码 msg["From"] = formataddr((str(Header(cfg["sender_name"], "utf-8")), cfg["sender"])) msg["To"] = ", ".join(to_addrs) if text_body: msg.attach(MIMEText(text_body, "plain", "utf-8")) msg.attach(MIMEText(html_body, "html", "utf-8")) if cfg["use_ssl"]: server = smtplib.SMTP_SSL(cfg["host"], cfg["port"], timeout=10) else: server = smtplib.SMTP(cfg["host"], cfg["port"], timeout=10) server.starttls() # 587 必须先升级再登录 try: server.login(cfg["user"], cfg["password"]) server.sendmail(cfg["sender"], to_addrs, msg.as_string()) finally: server.quit()逻辑说明:MIMEMultipart("alternative")让邮件同时带纯文本和 HTML 两个版本,收件端按能力渲染,这是避免被判垃圾邮件的细节之一。formataddr配合Header解决中文发件人乱码。timeout=10是必须的,否则网络不通时线程会挂很久。finally里quit()保证连接释放,不然高频发送会耗尽连接数。
参数说明:profile_name对应配置里的键,切换邮箱只改这个字符串;to_addrs是列表,支持群发但注意服务商单封收件人上限;html_body和text_body建议都传,只传 HTML 会提高进垃圾箱的概率。
3.2 把发送放进重试与降级链路
单次发送失败很常见,网络抖动、服务商限流都会触发。我一般包一层重试,并在主通道连续失败后切到备用配置。
import time from smtplib import SMTPException def send_with_fallback(to_addrs, subject, html_body, text_body=None): # 主通道重试 2 次,仍失败则切备用通道 for profile in ("default", "default", "backup"): try: send_mail(profile, to_addrs, subject, html_body, text_body) return True except (SMTPException, OSError) as e: print(f"[{profile}] send failed: {e}") time.sleep(2) # 退避,避免触发限流 return False这里的重试次数和退避时间要按服务商限流策略调。多数公共邮箱对同一账号有每分钟发信上限,退避 2 秒只是起步,批量场景建议做成队列加令牌桶,而不是在循环里硬发。
3.3 验证配置是否真的通
上线前用一个独立脚本探活,别等业务触发才发现配置错。
# 探活:只登录不发信,快速验证账号密码和网络 def check_profile(profile_name): cfg = MAIL_PROFILES[profile_name] try: if cfg["use_ssl"]: s = smtplib.SMTP_SSL(cfg["host"], cfg["port"], timeout=8) else: s = smtplib.SMTP(cfg["host"], cfg["port"], timeout=8) s.starttls() s.login(cfg["user"], cfg["password"]) s.quit() return "OK" except Exception as e: return f"FAIL: {e}"探活只做登录,不消耗发信额度,适合放进部署后的健康检查。返回FAIL: 535基本是授权码问题,FAIL: timed out是网络或端口被封,FAIL: 554多半是发件人与账号不一致。
4. 避坑与排查:五条血泪经验
4.1 535 认证失败:九成是授权码不是密码
现象:本地能连上服务器,login()直接抛535 Authentication failed。原因:很多邮箱开启 SMTP 服务后要求用独立授权码,登录密码在 SMTP 场景被拒绝。解决:到邮箱设置里开启 SMTP 并生成授权码,配置里password填授权码。注意授权码只显示一次,丢了重新生成。
4.2 554 被拒:发件人和登录账号不一致
现象:认证通过,sendmail时报554 Message rejected。原因:服务商校验From头必须等于登录账号,或别名未绑定。解决:把配置里sender改成与user完全一致;企业邮局要用别名,先在管理后台把别名绑定到账号。
4.3 连接超时:25 端口在云主机上基本不可用
现象:Connection timed out,换 465 就好了。原因:云厂商默认封禁 25 端口出站,防止垃圾邮件。解决:统一用 465 或 587,别在 25 上排查。如果 465 也超时,检查安全组出站规则和 DNS 解析。
4.4 中文乱码:Subject 和显示名都要编码
现象:收件箱里主题是乱码,或发件人显示成一串问号。原因:Subject和From显示名没做 RFC 2047 编码。解决:Subject用Header(subject, "utf-8"),From用formataddr包Header。正文用MIMEText(..., "utf-8")指定字符集。
4.5 进垃圾箱:只发 HTML 且没有退订头
现象:发送成功但收件人收不到,或在垃圾箱。原因:纯 HTML 邮件、缺少Message-ID、发信频率突增都会触发风控。解决:同时附纯文本版本,控制发送频率,批量场景接入服务商的邮件推送通道而不是硬发 SMTP。这块没有银弹,只能靠逐步提量和监控退信率。
5. 进阶:把发信做成可观测、可切换的通道
走到这一步,功能已经能跑,但生产环境还需要两样东西:可观测和可切换。可观测是指每次发送都记录通道、耗时、结果,出问题能定位是哪个邮箱、哪类错误;可切换是指不改代码就能换通道,通常靠配置中心或环境变量注入。
我一般会在发送器外面包一层日志和指标:
import logging, time logger = logging.getLogger("mailer") def send_observed(profile_name, to_addrs, subject, html_body, text_body=None): start = time.time() try: send_mail(profile_name, to_addrs, subject, html_body, text_body) logger.info("mail ok profile=%s to=%s cost=%.2fs", profile_name, to_addrs, time.time() - start) return True except Exception as e: logger.error("mail fail profile=%s to=%s err=%s cost=%.2fs", profile_name, to_addrs, e, time.time() - start) return False日志里带上profile和cost,排查时一眼能看出是某个通道整体慢,还是个别收件人地址有问题。指标上关注三个:发送成功率、平均耗时、退信率。成功率骤降通常是授权码过期或服务商限流;耗时上升多半是网络或服务商侧排队;退信率上升要检查收件人列表质量和发信频率。
配置切换建议用环境变量覆盖默认值,比如MAIL_PROFILE=backup让进程启动时选通道,配合配置中心能做到不重启切换。验证方法很简单:写一个定时任务,每天用探活脚本检查所有 profile,结果推送到内部告警,这样授权码过期能在业务受影响前发现。
最后说个我自己的习惯:任何新接入的邮箱,先手动发一封到自己的多个邮箱(不同服务商),确认不进垃圾箱、中文不乱码、附件正常,再写进配置。这一步花十分钟,能省掉后面半夜被叫起来查“为什么验证码收不到”的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取