feat(libappbase+appbase): 添加SMTP邮件发送工具类及配置界面

libappbase:
- 新增SMTPUtils工具类,原生Socket实现QQ SMTP SSL邮件发送(零外部依赖)
- 支持sendTextMail/sendHtmlMail,Java7兼容

appbase:
- APPExcetionMailSettingsActivity添加5项SMTP参数配置UI
  (服务器、端口、发件人邮箱、授权码、收件人邮箱)
- SharedPreferences持久化存储配置
- 子线程测试发送 + Toast结果反馈

备注:SMTPUtils测试邮件发送功能未调试,待实际环境验证
This commit is contained in:
BigPickle
2026-07-21 13:42:27 +08:00
parent 033b9aec67
commit 301227c330
5 changed files with 568 additions and 20 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Jul 21 13:23:28 HKT 2026
#Tue Jul 21 13:37:58 HKT 2026
stageCount=2
libraryProject=libappbase
baseVersion=15.21
publishVersion=15.21.1
buildCount=6
buildCount=9
baseBetaVersion=15.21.2
@@ -0,0 +1,274 @@
package cc.winboll.studio.libappbase.utils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import javax.net.ssl.SSLSocketFactory;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author 豆包&BigPickle&MiMo&ZhanGSKen<zhangsken@qq.com>
* @CreateDate 2026/07/21
* @LastEditDate 2026/07/21
* @Describe SMTP邮件发送工具类(原生Socket实现,零外部依赖)
* 基于QQ SMTP服务(smtp.qq.com:465 SSL),支持纯文本和HTML邮件
* Java7兼容标准,无高版本语法特性
*/
public final class SMTPUtils {
private static final String TAG = "SMTPUtils";
// QQ SMTP 服务器地址
private static final String DEFAULT_SMTP_HOST = "smtp.qq.com";
// QQ SMTP SSL 端口
private static final int DEFAULT_SMTP_PORT = 465;
// 私有构造,禁止实例化
private SMTPUtils() {
final String errMsg = "Cannot instantiate static class SMTPUtils";
LogUtils.e(TAG, "constructor invoke failed, msg = " + errMsg);
throw new AssertionError(errMsg);
}
/**
* 发送纯文本邮件(QQ SMTP SSL
* @param to收件人邮箱地址
* @param subject 邮件主题
* @param body 邮件正文(纯文本)
* @param from 发件人QQ邮箱地址
* @param authCode QQ邮箱授权码(非QQ密码)
* @return true=发送成功,false=发送失败
*/
public static boolean sendTextMail(
final String to,
final String subject,
final String body,
final String from,
final String authCode) {
return sendMail(to, subject, body, from, authCode, false);
}
/**
* 发送HTML邮件(QQ SMTP SSL
* @param to 收件人邮箱地址
* @param subject 邮件主题
* @param body 邮件正文(HTML格式)
* @param from 发件人QQ邮箱地址
* @param authCode QQ邮箱授权码(非QQ密码)
* @return true=发送成功,false=发送失败
*/
public static boolean sendHtmlMail(
final String to,
final String subject,
final String body,
final String from,
final String authCode) {
return sendMail(to, subject, body, from, authCode, true);
}
/**
* 发送邮件核心方法(QQ SMTP SSL)
* @param to 收件人邮箱地址
* @param subject 邮件主题
* @param body 邮件正文
* @param from 发件人QQ邮箱地址
* @param authCode QQ邮箱授权码
* @param isHtml 是否HTML格式
* @return true=发送成功,false=发送失败
*/
private static boolean sendMail(
final String to,
final String subject,
final String body,
final String from,
final String authCode,
final boolean isHtml) {
LogUtils.d(TAG, "sendMail invoke, to=" + to + ", subject=" + subject + ", isHtml=" + isHtml);
// 参数校验
if (to == null || to.isEmpty()) {
LogUtils.e(TAG, "sendMail failed, reason: recipient address is empty");
return false;
}
if (from == null || from.isEmpty()) {
LogUtils.e(TAG, "sendMail failed, reason: sender address is empty");
return false;
}
if (authCode == null || authCode.isEmpty()) {
LogUtils.e(TAG, "sendMail failed, reason: auth code is empty");
return false;
}
Socket socket = null;
BufferedReader reader = null;
OutputStream output = null;
try {
// 1. 创建SSL连接
LogUtils.d(TAG, " connecting to " + DEFAULT_SMTP_HOST + ":" + DEFAULT_SMTP_PORT);
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
socket = factory.createSocket(DEFAULT_SMTP_HOST, DEFAULT_SMTP_PORT);
socket.setSoTimeout(10000);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
output = socket.getOutputStream();
// 2. 读取服务器欢迎信息
String response = readResponse(reader);
LogUtils.d(TAG, "S: " + response);
if (!response.startsWith("220")) {
LogUtils.e(TAG, "sendMail failed, SMTP server greeting error: " + response);
return false;
}
// 3. EHLO 握手
sendCommand(output, "EHLO localhost", reader);
// 4. AUTH LOGIN
sendCommand(output, "AUTH LOGIN", reader);
// 5. 发送Base64编码的用户名(发件人邮箱)
sendCommand(output, base64Encode(from), reader);
// 6. 发送Base64编码的授权码
sendCommand(output, base64Encode(authCode), reader);
// 7. 设置发件人
sendCommand(output, "MAIL FROM:<" + from + ">", reader);
// 8. 设置收件人
sendCommand(output, "RCPT TO:<" + to + ">", reader);
// 9. 开始数据传输
sendCommand(output, "DATA", reader);
// 10. 构建邮件内容
String contentType = isHtml ? "text/html; charset=UTF-8" : "text/plain; charset=UTF-8";
StringBuilder mailContent = new StringBuilder();
mailContent.append("From: ").append(from).append("\r\n");
mailContent.append("To: ").append(to).append("\r\n");
mailContent.append("Subject: ").append(subject).append("\r\n");
mailContent.append("Content-Type: ").append(contentType).append("\r\n");
mailContent.append("\r\n");
mailContent.append(body).append("\r\n");
mailContent.append(".\r\n");
// 11. 发送邮件内容
output.write(mailContent.toString().getBytes("UTF-8"));
output.flush();
response = readResponse(reader);
LogUtils.d(TAG, "S: " + response);
// 12. QUIT 退出
sendCommand(output, "QUIT", reader);
LogUtils.d(TAG, "sendMail success, to=" + to + ", subject=" + subject);
return true;
} catch (Exception e) {
LogUtils.e(TAG, "sendMail exception: " + e.getMessage(), e);
return false;
} finally {
// 关闭资源
try {
if (output != null) output.close();
} catch (Exception e) {
LogUtils.e(TAG, "close output exception: " + e.getMessage());
}
try {
if (reader != null) reader.close();
} catch (Exception e) {
LogUtils.e(TAG, "close reader exception: " + e.getMessage());
}
try {
if (socket != null) socket.close();
} catch (Exception e) {
LogUtils.e(TAG, "close socket exception: " + e.getMessage());
}
}
}
/**
* 发送SMTP命令并读取多行响应
* @param output 输出流
* @param command SMTP命令
* @param reader 输入流
* @return 服务器响应字符串
*/
private static String sendCommand(
final OutputStream output,
final String command,
final BufferedReader reader) throws Exception {
LogUtils.d(TAG, "C: " + command);
output.write((command + "\r\n").getBytes("UTF-8"));
output.flush();
return readResponse(reader);
}
/**
* 读取SMTP服务器多行响应
* SMTP协议:以"数字 "开头表示最后一行,以"数字-"开头表示后续还有行
* @param reader 输入流
* @return 完整的服务器响应字符串
*/
private static String readResponse(final BufferedReader reader) throws Exception {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line).append("\n");
// 最后一行:第4位是空格(如 "250 OK"
if (line.length() >= 4 && line.charAt(3) == ' ') {
break;
}
}
return response.toString().trim();
}
/**
* 标准Base64编码(Java7兼容,无第三方依赖)
* @param input 待编码字符串
* @return Base64编码后的字符串
*/
static String base64Encode(final String input) {
if (input == null || input.isEmpty()) {
return "";
}
try {
byte[] data = input.getBytes("UTF-8");
final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
StringBuilder result = new StringBuilder();
int i;
// 每3字节一组处理
for (i = 0; i + 2 < data.length; i += 3) {
int triple = ((data[i] & 0xFF) << 16)
| ((data[i + 1] & 0xFF) << 8)
| (data[i + 2] & 0xFF);
result.append(alphabet.charAt((triple >> 18) & 0x3F));
result.append(alphabet.charAt((triple >> 12) & 0x3F));
result.append(alphabet.charAt((triple >> 6) & 0x3F));
result.append(alphabet.charAt(triple & 0x3F));
}
// 处理剩余字节
if (i < data.length) {
int remaining = data[i] & 0xFF;
result.append(alphabet.charAt((remaining >> 2) & 0x3F));
if (i + 1 < data.length) {
remaining = ((remaining << 8) | (data[i + 1] & 0xFF));
result.append(alphabet.charAt((remaining >> 4) & 0x3F));
result.append(alphabet.charAt((remaining << 2) & 0x3F));
} else {
result.append(alphabet.charAt((remaining << 4) & 0x3F));
result.append('=');
}
}
return result.toString();
} catch (Exception e) {
LogUtils.e(TAG, "base64Encode exception: " + e.getMessage(), e);
return "";
}
}
}