9.案例

163邮箱邮件发送

链接:https://pan.wo.cn/s/1J1c4w90901 提取码:1Spc

在module.json5配置文件加上对权限的声明:

"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
// 导入所需的模块
import socket from '@ohos.net.socket'; // 网络套接字模块,用于TCP/TLS通信
import util from '@ohos.util'; // 工具模块,用于Base64编码/解码等

// 接口定义
interface LocalAddress {
address: string; // 本地绑定地址
family: number; // 地址族(IPv4=1, IPv6=2)
}

interface ServerAddress {
address: string; // 服务器地址
port: number; // 服务器端口
family: number; // 地址族
}

interface MessageValue {
message: ArrayBuffer; // 接收到的消息数据(二进制格式)
}

interface SecureOptions {
protocols: socket.Protocol[]; // 支持的TLS协议版本
cipherSuite: string; // 加密套件配置
}

interface ConnectOptions {
address: ServerAddress; // 服务器地址信息
secureOptions: SecureOptions; // TLS安全选项
}

// 全局TLSSocket实例
let tlsSocket: socket.TLSSocket | null = null;

@Entry
@Component
struct Index {
bindLocal: boolean = false // Socket是否已绑定本地地址
isServerResponse: boolean = false // 是否收到服务器响应
@State serverAddr: string = "smtp.163.com" // SMTP服务器地址
@State serverPort: number = 465 // SMTP TLS端口
@State userName: string = "staseaiot@163.com" // 邮箱用户名
@State passwd: string = "DAtTEFxcvXt76ByV" // 邮箱授权码(不是登录密码)
@State rcptList: string = "1970884166@qq.com" // 收件人列表(逗号分隔)
@State mailTitle: string = "意见反馈" // 邮件标题
@State mailFrom: string = "staseaiot@163.com" // 发件人邮箱
@State mailContent: string = "This is greeting from Harmony OS" // 邮件内容
@State canSend: boolean = false // 是否可以发送邮件(登录成功后设为true)

// UI构建函数
build() {
Row() {
Column() {
Text("邮件发送客户端(163邮箱 - 465 TLS)")
.fontSize(14)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.padding(10)

// 服务器地址输入
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("邮箱服务器地址:")
.width(120)
.fontSize(14)
.flexGrow(0)
TextInput({ text: this.serverAddr })
.onChange((value) => { this.serverAddr = value })
.width(110)
.fontSize(12)
.flexGrow(1)
}.width('100%').padding(5)

// 服务器端口输入
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("邮箱服务器端口:")
.width(120)
.fontSize(14)
.flexGrow(0)
TextInput({ text: this.serverPort.toString() })
.type(InputType.Number)
.onChange((value) => { this.serverPort = parseInt(value) })
.width(110)
.fontSize(12)
.flexGrow(1)
}.width('100%').padding(5)

// 用户名输入
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("邮箱用户名:")
.width(90)
.fontSize(14)
.flexGrow(0)
TextInput({ text: this.userName })
.onChange((value) => { this.userName = value })
.width(110)
.fontSize(12)
.flexGrow(1)
}.width('100%').padding(5)

// 密码输入和登录按钮
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("登录密码(授权码):")
.width(130)
.fontSize(14)
.flexGrow(0)
TextInput({ text: this.passwd })
.onChange((value) => { this.passwd = value })
.width(100)
.fontSize(12)
.flexGrow(1)

Button("登录")
.onClick(() => { this.login() })
.width(70)
.fontSize(14)
.flexGrow(0)
}.width('100%').padding(5)

// 收件人输入
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("收件人邮箱:")
.width(90)
.fontSize(14)
.flexGrow(0)
TextInput({ placeholder: "多个使用逗号分隔", text: this.rcptList })
.onChange((value) => { this.rcptList = value })
.width(110)
.fontSize(12)
.flexGrow(1)
}.width('100%').padding(5)

// 邮件标题输入
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("标题:")
.width(50)
.fontSize(14)
.flexGrow(0)
TextInput({ text: this.mailTitle })
.onChange((value) => { this.mailTitle = value })
.width(110)
.fontSize(12)
.flexGrow(1)
}.width('100%').padding(5)

// 发件人邮箱输入
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("发件人邮箱:")
.width(90)
.fontSize(14)
.flexGrow(0)
TextInput({ text: this.mailFrom })
.onChange((value) => { this.mailFrom = value })
.width(110)
.fontSize(12)
.flexGrow(1)
}.width('100%').padding(5)

// 邮件内容输入和发送按钮
Flex({ justifyContent: FlexAlign.Start, direction: FlexDirection.Column, alignItems: ItemAlign.Center }) {
Text("邮件内容:")
.width('100%')
.fontSize(14)

TextArea({ text: this.mailContent })
.onChange((value) => { this.mailContent = value })
.width('100%')
.height(120)
.fontSize(12)

Button("发送")
.enabled(this.canSend) // 仅在登录成功后可用
.onClick(() => { this.sendMail() })
.width(80)
.fontSize(14)
}
.flexGrow(1)
.width('100%')
.padding(5)
.height('100%')
}
.width('100%')
.justifyContent(FlexAlign.Start)
.height('100%')
.padding(10)
}
.height('100%')
}

// 创建新的TLSSocket实例
createNewSocket() {
if (tlsSocket) {
try { tlsSocket.close(); } catch (e) {}
}
tlsSocket = socket.constructTLSSocketInstance();
this.bindLocal = false;
}

// 绑定Socket到本地地址并设置监听器
async bindSocket() {
this.createNewSocket();

try {
let localAddress: LocalAddress = { address: "0.0.0.0", family: 1 }; // 绑定到所有IPv4地址
await tlsSocket!.bind(localAddress);
console.log("C: bind success");
this.bindLocal = true;
} catch (e) {
console.error(`C: bind fail - ${e.message || e}`);
}

// 监听服务器消息
tlsSocket!.on("message", (value: MessageValue) => {
this.isServerResponse = true;
let msg = buf2String(value.message);
console.log(`S: ${msg}`);
});

// 监听错误事件
tlsSocket!.on("error", (err) => {
console.error(`E: Socket 错误 - ${err.message || err.code || '未知 TLS 错误'}`);
console.error("详细错误对象:", err);
this.cleanupSocket();
});

// 监听连接关闭事件
tlsSocket!.on("close", () => {
console.log("C: 连接已被服务器断开(可能是空闲超时)");
console.log("C: 发送按钮已禁用,请重新点击'登录'或'发送'来重连");
this.canSend = false;
this.cleanupSocket();
});
}

// 清理Socket资源
cleanupSocket() {
if (tlsSocket) {
try { tlsSocket.close(); } catch (e) {}
tlsSocket = null;
}
this.bindLocal = false;
this.isServerResponse = false;
}

// SMTP登录流程
async login() {
try {
this.cleanupSocket();
await this.bindSocket();

let serverAddress: ServerAddress = { address: this.serverAddr, port: this.serverPort, family: 1 };

// 配置TLS选项
const secureOptions: SecureOptions = {
protocols: [socket.Protocol.TLSv12, socket.Protocol.TLSv13], // 支持TLS 1.2和1.3
cipherSuite: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256' // 加密套件
};

const connectOptions: ConnectOptions = {
address: serverAddress,
secureOptions: secureOptions
};

// 连接SMTP服务器
console.log(`C: 正在连接 ${this.serverAddr}:${this.serverPort} (TLS)...`);
await tlsSocket!.connect(connectOptions);
console.log("C: TLS 连接成功!");

// 等待服务器欢迎消息(220)
await this.wait4ServerResponse(10000);
if (!this.isServerResponse) {
throw new Error("未收到 220 欢迎消息");
}

// SMTP握手流程
await this.exeCmdAndWait4Response("EHLO harmonyos.next"); // 发送EHLO命令
await this.exeCmdAndWait4Response("AUTH LOGIN"); // 开始认证

// Base64编码的用户名和密码
let loginName = string2Base64(this.userName);
await this.exeCmdAndWait4Response(loginName);

let passWd = string2Base64(this.passwd);
await this.exeCmdAndWait4Response(passWd);

if (this.isServerResponse) { // 简化判断(实际应检查235响应码)
this.canSend = true;
console.log("C: 登录成功!(235 Authentication successful)");
console.log("C: 可以开始发送邮件");
} else {
throw new Error("登录失败,请检查邮箱/授权码");
}

} catch (e) {
console.error(`C: 登录失败 - ${e.message || e}`);
this.cleanupSocket();
}
}

// 发送邮件
async sendMail() {
if (!this.canSend || !tlsSocket) {
console.log("C: 连接不可用,正在自动尝试重连...");
await this.login();
if (!this.canSend) return;
}

try {
console.log("C: 开始发送邮件...");

// SMTP发送流程
await this.exeCmdAndWait4Response(`MAIL FROM:<${this.mailFrom}>`); // 发件人

// 收件人列表(支持多个)
let rcptMails = this.rcptList.split(',');
for (let rcpt of rcptMails) {
rcpt = rcpt.trim();
if (rcpt) await this.exeCmdAndWait4Response(`RCPT TO:<${rcpt}>`);
}

await this.exeCmdAndWait4Response("DATA"); // 准备发送数据

// 构建邮件头
let mailBody = `From: ${this.mailFrom}\r\n`;
mailBody += `To: ${this.rcptList}\r\n`;
mailBody += `Subject: =?utf-8?B?${string2Base64(this.mailTitle)}?=\r\n`; // Base64编码标题
mailBody += `Date: ${new Date().toUTCString()}\r\n`;
mailBody += `Content-Type: text/plain; charset="utf-8"\r\n`;
mailBody += `Content-Transfer-Encoding: base64\r\n\r\n`;
mailBody += `${string2Base64(this.mailContent)}\r\n`; // Base64编码内容
mailBody += ".\r\n"; // SMTP数据结束标记

// 分块发送邮件内容(避免大包)
const chunkSize = 2048;
for (let i = 0; i < mailBody.length; i += chunkSize) {
const chunk = mailBody.substring(i, i + chunkSize);
await tlsSocket!.send(chunk);
console.log(`C: [chunk sent, length=${chunk.length}]`);
}

// 等待服务器确认
await this.wait4ServerResponse(15000);
if (!this.isServerResponse) {
throw new Error("服务器未确认邮件接收");
}

console.log("C: 邮件发送成功!连接保持打开,可继续发送下一封");

} catch (e) {
console.error(`C: 发送失败 - ${e.message || e}`);
this.cleanupSocket();
}
}

// 执行SMTP命令并等待响应
async exeCmdAndWait4Response(cmd: string) {
if (!tlsSocket) {
throw new Error("Socket 未初始化");
}

this.isServerResponse = false;
let fullCmd = cmd + "\r\n"; // SMTP命令以CRLF结束
try {
await tlsSocket.send(fullCmd);
console.log(`C: ${cmd}`);
} catch (sendErr) {
console.error(`C: 发送命令失败 - ${sendErr.message || sendErr}`);
throw new Error(sendErr.message || sendErr);
}

await this.wait4ServerResponse(10000);
}

// 等待服务器响应(轮询方式)
async wait4ServerResponse(timeoutMs: number = 10000) {
const start = Date.now();
while (!this.isServerResponse) {
if (Date.now() - start > timeoutMs) {
throw new Error(`等待响应超时 (${timeoutMs}ms)`);
}
await sleep(100); // 每100毫秒检查一次
}
}
}

// Base64 编码函数
function string2Base64(src: string): string {
try {
let textEncoder = new util.TextEncoder();
let encodeValue = textEncoder.encodeInto(src); // 字符串转Uint8Array
let tool = new util.Base64Helper();
return tool.encodeToStringSync(encodeValue); // Base64编码
} catch (e) {
console.error("Base64 失败:", e);
return "";
}
}

// ArrayBuffer 转字符串函数
function buf2String(buf: ArrayBuffer): string {
try {
let msgArray = new Uint8Array(buf);
let textDecoder = util.TextDecoder.create("utf-8");
return textDecoder.decodeWithStream(msgArray); // UTF-8解码
} catch (e) {
console.error("解码失败:", e);
return "[解码失败]";
}
}

// 异步休眠函数
function sleep(time: number) {
return new Promise<void>((resolve) => setTimeout(resolve, time));
}