简介这份Android源码资源聚焦于「不依赖系统Mail客户端、直接向任意邮箱发送邮件」的实现方案面向具备一定Android开发基础、希望掌握SMTP邮件发送能力的开发者。资源通过对比调用系统Mail客户端与使用SMTP协议两种方式重点讲解后者如何借助JavaMail相关jar包完成邮件的中转与投递帮助读者理解SMTP作为TCP/IP协议簇成员在邮件传输中的作用并规避手机未安装邮件客户端时无法发信的痛点。压缩包共60个文件约3.81MB包含class、xml、png、jar、java、apk等多种类型涵盖源码、资源布局、依赖库与可运行安装包结构完整便于直接导入研究。目前已有2045人学习下载读者可从中获取完整的工程目录、SMTP发送邮件的核心实现代码以及依赖jar的集成方式适合作为Android邮件功能开发的参考范例。1. 支持任意邮箱发送邮件功能从被忽略的“最后一公里”说起做过邮件通知的人大概都有过这种经历本地用某公共邮件服务商的 SMTP 调试得好好的一上生产就翻车——要么被限流要么进了垃圾箱要么运维告诉你“公司出口 IP 不能随便连 25 端口”。更麻烦的是客户一句“我们用的是自己的企业邮箱”你就得把代码里写死的配置全部推倒重来。支持任意邮箱发送邮件功能说的就是把这最后一公里做成可插拔、可配置、可验证的能力而不是把某一家服务商的地址硬编码进业务逻辑。这件事的价值不在于“会发邮件”而在于把发信这件事从业务代码里彻底解耦换邮箱不改代码换服务商不改调用方出问题能定位到是认证、端口、加密方式还是对方拒收。适合谁适合正在做告警通知、报表推送、用户验证码、工单流转的开发者也适合需要对接多个客户邮箱环境的后端和运维。下面按“先立住模型、再动手跑通、最后讲坑”的顺序拆开讲。2. 把发信抽象成配置SMTP 参数模型与选型理由2.1 为什么是 SMTP 而不是各家 SDK支持任意邮箱第一件事是选一个足够通用的协议层。常见做法是走 SMTP因为几乎所有邮箱服务商都提供 SMTP 接入包括企业自建邮件系统。各家 SDK 虽然封装得更友好但每接一家就要引一个包、学一套鉴权方式最后业务代码里全是if 服务商A else 服务商B。SMTP 是那个“最小公约数”只要拿到主机、端口、账号、授权码或密码、加密方式这五个参数就能发信。这里要区分两个概念登录密码和授权码。很多公共邮箱服务默认关闭 SMTP需要单独开启并生成一个专用授权码它和网页登录密码不是一回事。企业邮箱则可能直接用账号密码也可能要求 OAuth。选型时优先确认对方文档里“SMTP 服务”这一节把参数抄全别靠猜。2.2 五个必填参数与两个可选参数把配置抽象成一张表后面所有代码都围绕它转参数含义典型取值是否必填hostSMTP 服务器地址smtp.example.com是port端口465 / 587 / 25是user登录账号完整邮箱地址是password密码或授权码服务商生成是secure是否用隐式 TLStrue / false是from发件人显示地址通常等于 user否timeout超时毫秒10000否端口和加密方式是强绑定的465 一般配securetrue隐式 TLS587 配securefalse再走 STARTTLS 升级25 端口多数环境已封禁且不加密生产别用。这个对应关系搞错最常见的报错就是连接超时或握手失败。2.3 用一份配置驱动所有邮箱我一般会把配置放在环境变量或配置中心而不是代码里。下面是一个最小可用的 Python 发信封装用标准库smtplib不引第三方依赖方便在任何环境复现import smtplib import ssl from email.mime.text import MIMEText from email.header import Header def send_mail(cfg, to_addrs, subject, body): # 构造纯文本邮件指定 utf-8 避免中文乱码 msg MIMEText(body, plain, utf-8) msg[Subject] Header(subject, utf-8) msg[From] cfg.get(from) or cfg[user] msg[To] , .join(to_addrs) # 465 用隐式 TLS587 用 STARTTLS这里按 secure 分流 if cfg[secure]: context ssl.create_default_context() server smtplib.SMTP_SSL(cfg[host], cfg[port], timeoutcfg.get(timeout, 10000), contextcontext) else: server smtplib.SMTP(cfg[host], cfg[port], timeoutcfg.get(timeout, 10000)) server.starttls(contextssl.create_default_context()) try: server.login(cfg[user], cfg[password]) server.sendmail(msg[From], to_addrs, msg.as_string()) finally: server.quit()逻辑说明先按secure决定用SMTP_SSL还是普通SMTP加starttls这一步是“任意邮箱”能跑通的关键login用账号和授权码sendmail的第二个参数是收件人列表第三个是完整报文。参数说明timeout一定要设否则网络不通时会挂很久from允许和user不同有些企业邮箱要求发件人必须是已验证别名。调用时把配置传进去即可换邮箱只改配置cfg { host: smtp.example.com, port: 465, user: noreplyexample.com, password: 授权码, secure: True, from: noreplyexample.com, } send_mail(cfg, [someoneexample.com], 测试主题, 这是一封测试邮件)2.4 先验证连通性再谈业务别急着把发信塞进业务逻辑。先写一个独立脚本只做“连上、登录、发一封给自己”。跑通了再谈模板、附件、批量。这一步能省掉后面大量“到底是网络问题还是代码问题”的扯皮。验证顺序是telnet host port看端口通不通再用上面的脚本看认证过不过最后看收件箱和垃圾箱。3. 从单发到可用模板、附件与批量发送的落地3.1 用 HTML 模板替换纯文本纯文本能跑通后下一步是 HTML 正文。把MIMEText(body, plain, utf-8)换成MIMEText(html, html, utf-8)即可。但要注意很多邮箱客户端对 HTML 有安全策略内联样式比外部 CSS 可靠图片尽量用 CID 内嵌而不是外链否则容易被判为营销邮件。html div stylefont-family:sans-serif h3您好{name}/h3 p您的验证码是 b{code}/b5 分钟内有效。/p /div msg MIMEText(html.format(nameA同学, code123456), html, utf-8)参数说明format里的字段来自业务数据务必做转义避免用户输入里带 HTML 标签造成注入。模板本身建议放独立文件用字符串替换而不是eval类操作。3.2 带附件的邮件怎么组装附件要用MIMEMultipart组合正文和附件分开挂from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication msg MIMEMultipart() msg[Subject] Header(月度报表, utf-8) msg[From] cfg[user] msg[To] someoneexample.com msg.attach(MIMEText(报表见附件, plain, utf-8)) with open(report.xlsx, rb) as f: part MIMEApplication(f.read()) part.add_header(Content-Disposition, attachment, filename(utf-8, , report.xlsx)) msg.attach(part)逻辑说明MIMEApplication适合二进制附件Content-Disposition的filename用三元组形式声明编码中文文件名才不会乱码。参数说明附件大小受服务商限制常见上限在 20MB 到 50MB 之间超了要么压缩要么改用下载链接。3.3 批量发送要控制节奏批量发送最容易踩的坑是“一次性循环几百封”结果被服务商判定为异常流量直接封号。正确做法是复用同一个连接、控制速率、记录失败重试import time def send_batch(cfg, tasks, interval0.5): # 复用连接避免每封都重新握手 server smtplib.SMTP_SSL(cfg[host], cfg[port], timeout10000) server.login(cfg[user], cfg[password]) failed [] try: for t in tasks: try: server.sendmail(cfg[user], [t[to]], t[raw]) except Exception as e: failed.append((t[to], str(e))) time.sleep(interval) # 控制发送间隔 finally: server.quit() return failed参数说明interval根据服务商限流策略调整公共邮箱通常每秒 1 到 5 封企业邮箱可放宽。失败列表要落库方便后续重试别只打日志。3.4 把配置和发送解耦成服务到这一步建议把发信封装成一个独立模块或小服务对外只暴露send(template, to, vars)。业务代码不碰 SMTP 参数配置从环境变量读取。这样换邮箱、加通道、做灰度都只动一处。常见做法是加一个“通道”概念每个通道一组 SMTP 配置按业务类型路由比如验证码走通道 A报表走通道 B。4. 避坑与排查那些让邮件发不出去的细节4.1 认证失败密码还是授权码现象SMTPAuthenticationError提示用户名或密码错误。原因多数公共邮箱要求用授权码而非登录密码且需先在网页端开启 SMTP 服务。解决登录邮箱设置页开启 SMTP生成授权码替换配置里的password。企业邮箱若开了二次验证也要单独生成应用密码。4.2 连接超时端口与出口限制现象连接阶段卡住最终TimeoutError。原因端口选错或服务器出口防火墙不允许访问该端口。解决先telnet host port确认连通性465 不通就试 587若都不通联系运维放行出口别在代码里反复重试。4.3 进了垃圾箱发件人与内容双重因素现象发送成功但收件人收不到或在垃圾箱。原因发件人地址与登录账号不一致、缺少 SPF/DKIM 记录、正文含大量链接或敏感词。解决from尽量等于user让域名管理员配置 SPF 和 DKIM正文控制链接数量验证码类邮件别加营销话术。4.4 中文乱码编码没声明全现象主题或正文显示乱码。原因Header或MIMEText没指定utf-8或附件文件名没做编码声明。解决主题用Header(subject, utf-8)正文MIMEText(body, plain, utf-8)附件文件名用三元组声明编码。4.5 批量被封速率与连接复用现象发了几十封后开始报错甚至账号被临时限制。原因短时间高频发送或每封都新建连接。解决复用连接加interval控制速率失败落库重试必要时分散到多个通道。5. 进阶技巧让“任意邮箱”真正可运维5.1 用配置校验挡住低级错误在加载配置时做一次校验把端口和secure的对应关系、必填项是否为空检查掉比运行到一半报错强得多def validate(cfg): required [host, port, user, password, secure] for k in required: if k not in cfg: raise ValueError(f缺少配置项: {k}) if cfg[port] 465 and not cfg[secure]: raise ValueError(465 端口应使用 secureTrue) if cfg[port] 587 and cfg[secure]: raise ValueError(587 端口应使用 secureFalse 配合 STARTTLS)这段校验能挡掉大部分“换了邮箱就连不上”的问题参数含义一目了然新人接手也不容易配错。5.2 发送结果的可观测性光有日志不够建议记录四个指标发送总数、成功数、失败数、平均耗时。失败按错误类型分类认证、超时、拒收这样出问题时一眼能看出是配置问题还是对方限制。我一般会在发送模块里埋点把每次发送的通道、耗时、结果写进监控而不是等用户来投诉。5.3 多通道与降级生产环境别只配一个邮箱。常见做法是主通道加备用通道主通道连续失败超过阈值就切备用同时告警。切换逻辑放在发送模块内部业务无感知。这样即使某家服务商临时故障通知也不会断。5.4 一个我踩过的坑早期我把from写成了和user不同的别名本地测试一切正常上线后部分收件人直接拒收。查了半天才发现是对方邮件网关校验发件人必须与认证账号一致。从那以后我养成了一个习惯任何新邮箱接入先发一封给自己再发一封给外部邮箱两个都通了才算接入完成。这个习惯帮我省掉了无数次“本地好好的线上不行”的返工。希望帮到你。本文还有配套的精品资源点击获取