HarmonyOS 鸿蒙Next网络编程系列54-仓颉版实现Smtp邮件发送客户端
HarmonyOS 鸿蒙Next网络编程系列54-仓颉版实现Smtp邮件发送客户端
1. SMTP邮件发送客户端
在本系列的第4篇文章《鸿蒙网络编程系列4-实现SMTP邮件发送客户端》中,基于ArkTS语言在API9环境下使用TCPSocket对象演示了SMTP客户端的实现,并且通过腾讯邮件服务器执行了实际的邮件发送。不过,在2024年末,腾讯发了一个通知,从2024年11月20日开始,停用以明文非加密方式登录的第三方邮件客户端,必需启用SSL/TLS加密方式。不过,除了腾讯邮件发送服务器,还有很多其他邮件服务器支持使用明文登录,其中比较知名的有搜狐邮箱,可以通过如下的方式启用:
保存的时候,搜狐邮箱会自动生成独立密码,将来可以使用这个密码执行登录。
本文将使用仓颉语言在API17环境下实现SMTP邮件发送客户端,具体的邮件发送将通过搜狐邮箱实现,关于SMTP协议的相关基础知识,可以参考本系列第4篇文章的第一部分,这里不再赘述。
2. 邮件发送客户端示例编写
下面详细介绍创建该示例的步骤(确保DevEco Studio已安装仓颉插件)。
步骤1:创建[Cangjie]Empty Ability项目。
步骤2:在module.json5配置文件加上对权限的声明:
{
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
这里添加了访问互联网的权限。
步骤3:在build-profile.json5配置文件加上仓颉编译架构:
{
"cangjieOptions": {
"path": "./src/main/cangjie/cjpm.toml",
"abiFilters": ["arm64-v8a", "x86_64"]
}
}
步骤4:在index.cj文件里添加如下的代码:
package ohos_app_cangjie_entry
import ohos.base.*
import ohos.component.*
import ohos.state_manage.*
import ohos.state_macro_manage.*
import std.collection.HashMap
import std.convert.*
import std.net.*
import std.socket.*
import encoding.base64.toBase64String
@Entry
@Component
class EntryView {
@State
var title: String = 'SMTP邮件发送客户端示例';
//连接、通讯历史记录
@State
var msgHistory: String = ''
//服务器是否响应(发送数据到客户端)
var isServerResponse: Bool = false
//服务端地址,smtp.sohu.com的ip地址为116.30.217.16
@State
var serverAddr: String = "116.130.217.16"
//服务端端口,smtp.sohu.com的端口为25,不同的smtp服务器端口可能不一样
@State
var serverPort: UInt16 = 25
//用户名
@State
var userName: String = "youmail@sohu.com"
//密码,对于搜狐邮箱,这里是独立密码
@State
var passwd: String = "youpassword"
//收件人邮箱列表(如果多个使用逗号分隔)
@State
var rcptList: String = "*****@sohu.com,****@qq.com"
//发件人邮箱
@State
var mailFrom: String = "youmail@sohu.com"
//邮件标题
@State
var mailTitle: String = "测试邮件标题"
//邮件内容
@State
var mailContent: String = "这是来自鸿蒙的问候!"
//是否正在登录
@State
var isLogin: Bool = false
//是否可以发送邮件
@State
var canSend: Bool = false
//TCP客户端
var tcpClient: ?TcpSocket = None
let scroller: Scroller = Scroller()
func build() {
Row {
Column {
Text(title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.width(100.percent)
.textAlign(TextAlign.Center)
.padding(10)
Flex(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center) {
Text("SMTP服务器地址:").fontSize(14)
TextInput(text: serverAddr)
.onChange({
value => serverAddr = value
})
.width(100)
.fontSize(11)
.flexGrow(1)
Text(":").fontSize(14)
TextInput(text: serverPort.toString())
.onChange({
value => serverPort = UInt16.parse(value)
})
.setType(InputType.Number)
.width(80)
.fontSize(11)
}.width(100.percent).padding(5)
Flex(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center) {
Text("邮箱用户名:").fontSize(14).width(100).flexGrow(0)
TextInput(text: userName).onChange({
value => userName = value
}).width(110).fontSize(12).flexGrow(1)
}.width(100.percent).padding(5)
Flex(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center) {
Text("登录密码:").fontSize(14).width(100).flexGrow(0)
TextInput(text: passwd)
.onChange({
value => passwd = value
})
.setType(InputType.Password)
.width(110)
.fontSize(12)
.flexGrow(1)
Button("登录")
.onClick {
evt => login()
}
.enabled(!isLogin && userName != "" && passwd != "")
.width(70)
.fontSize(14)
}.width(100.percent).padding(5)
Flex(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center) {
Text("收件人邮箱:").fontSize(14).width(100).flexGrow(0)
TextArea(placeholder: "多个收件人使用逗号分隔", text: rcptList)
.onChange({
value => rcptList = value
})
.width(110)
.fontSize(12)
.flexGrow(1)
}.width(100.percent).padding(5)
Flex(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center) {
Text("发件人邮箱:").fontSize(14).width(100).flexGrow(0)
TextInput(text: mailFrom).onChange({
value => mailFrom = value
}).width(110).fontSize(12).flexGrow(1)
}.width(100.percent).padding(5)
Flex(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center) {
Text("邮件标题:").fontSize(14).width(100).flexGrow(0)
TextInput(text: mailTitle)
.onChange({
value => mailTitle = value
})
.width(110)
.fontSize(12)
.flexGrow(1)
}.width(100.percent).padding(5)
Flex(
direction: FlexDirection.Column,
justifyContent: FlexAlign.Start,
alignItems: ItemAlign.Center) {
Text("邮件内容:").fontSize(14).width(100.percent)
TextArea(placeholder: "请输入要发送的邮件内容", text: mailContent)
.onChange({
value => mailContent = value
})
.width(100.percent)
.height(80)
.fontSize(12)
Row() {
Button("发送邮件").onClick {
evt => sendMail()
}.enabled(canSend).width(100).fontSize(14)
}.width(100.percent).justifyContent(FlexAlign.End)
Scroll(scroller) {
Text(msgHistory)
.textAlign(TextAlign.Start)
.padding(10)
.width(100.percent)
.backgroundColor(0xeeeeee)
}
.align(Alignment.Top)
.backgroundColor(0xeeeeee)
.height(200)
.flexGrow(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.On)
.scrollBarWidth(20)
}.width(100.percent).padding(5).flexGrow(1).height(300)
}.width(100.percent).height(100.percent)
}.height(100.percent)
}
//发送命令到服务器
func sendCmd2ServerWithCRLF(cmd: String) {
let fullCmd: String = cmd + "\r\n"
tcpClient?.write(fullCmd.toArray())
msgHistory += "C:${cmd}\r\n"
}
//从服务器读取消息
func readMsgFromServer() {
let buffer = Array<UInt8>(1024, item: 0)
//从socket读取数据
var readCount = tcpClient?.read(buffer)
//把接收到的数据转换为字符串
let content = String.fromUtf8(buffer[0..readCount.getOrThrow()])
msgHistory += "S:${content}"
return content
}
//登录
func login() {
tcpClient = TcpSocket(serverAddr, serverPort)
isLogin = true
//启动一个线程执行登录
spawn {
try {
tcpClient?.connect()
msgHistory += "C:连接成功!\r\n"
} catch (err: Exception) {
msgHistory += "C:连接失败${err.message}!\r\n"
isLogin = false
return
}
try {
sendCmd2ServerWithCRLF("ehlo anyname")
var content = readMsgFromServer()
sendCmd2ServerWithCRLF("auth login")
content = readMsgFromServer()
sendCmd2ServerWithCRLF(toBase64String(userName.toArray()))
content = readMsgFromServer()
sendCmd2ServerWithCRLF(toBase64String(passwd.toArray()))
content = readMsgFromServer()
canSend = true
} catch (exp: Exception) {
msgHistory += "从Socket读取数据错误:${exp}\r\n"
}
isLogin = false
}
}
func sendMail() {
//启动一个线程执行发送
spawn {
try {
sendCmd2ServerWithCRLF("mail from:<${mailFrom}>")
var content = readMsgFromServer()
for (rcpt in rcptList.split(",")) {
sendCmd2ServerWithCRLF("rcpt to:<${rcpt}>")
content = readMsgFromServer()
}
//准备发送邮件内容
sendCmd2ServerWithCRLF("data")
content = readMsgFromServer()
let mailBody = "Subject: ${mailTitle} \r\nFrom: ${mailFrom}\r\n\r\n${mailContent}\r\n."
sendCmd2ServerWithCRLF(mailBody)
content = readMsgFromServer()
sendCmd2ServerWithCRLF("quit")
content = readMsgFromServer()
} catch (exp: Exception) {
msgHistory += "从套接字读取数据错误:${exp}\r\n"
}
}
}
}
步骤5:编译运行,可以使用模拟器或者真机。
步骤6:按照本文第2部分“邮件发送客户端示例演示”操作即可。
4. 代码分析
本文的核心代码主要是两个函数,第一个是发送命令到服务器的函数sendCmd2ServerWithCRLF,该函数在发送命令给服务器时,会在命令后面添加回车换行符号,然后调用tcpClient的write函数执行实际的发送。第二个是从服务器读取消息的函数readMsgFromServer,该函数会从套接字读取数据并写入到缓冲区buffer中,然后把数据转换为字符串。
需要特别注意的是,为了简化开发,第二个函数假设可以一次性读取服务器的完整回复,并且服务器的回复不超过1024字节,这个假设一般是成立的,不过,在一些特殊情况下,比如网络不太好,或者网络数据“粘包”,可能会出现接收问题。这时候,可以通过更复杂的代码来解决,这里就不展开了,可以参考本系列相关的“TCP粘包”文章。
(本文作者原创,除非明确授权禁止转载)
本文源码地址: https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tcp/SmtpClient4Cj
本系列源码地址: https://gitee.com/zl3624/harmonyos_network_samples
更多关于HarmonyOS 鸿蒙Next网络编程系列54-仓颉版实现Smtp邮件发送客户端的实战教程也可以访问 https://www.itying.com/category-93-b0.html
在鸿蒙Next仓颉版中实现SMTP邮件发送需使用@ohos.net.socket模块。关键步骤:
- 创建TCPSocket连接SMTP服务器(默认端口25或465/587加密端口);
- 发送HELO/EHLO、AUTH LOGIN等命令完成握手认证;
- 使用MAIL FROM/RCPT TO/DATA命令构造邮件;
- 需Base64编码用户名密码。
仓颉特有语法需注意异步回调处理,如socket.on(‘message’)接收服务器响应。SSL加密需调用TLS相关API。邮件内容需符合RFC 822格式规范,结尾需单独一行包含"."。
更多关于HarmonyOS 鸿蒙Next网络编程系列54-仓颉版实现Smtp邮件发送客户端的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
这是一个很好的使用仓颉语言实现SMTP邮件客户端的示例。代码结构清晰,完整实现了SMTP协议的基本流程:
- 代码使用了TcpSocket进行网络通信,实现了SMTP协议的几个关键步骤:
- EHLO握手
- AUTH LOGIN认证(使用Base64编码)
- MAIL FROM/RCPT TO指定发件人和收件人
- DATA传输邮件内容
- 界面设计合理,包含了必要的输入控件:
- 服务器地址和端口
- 用户名密码
- 收件人列表
- 邮件标题和内容
- 通信日志显示区域
- 代码中需要注意的几个技术点:
- 使用spawn创建新线程处理网络操作,避免阻塞UI
- 每条SMTP命令后需要添加CRLF(\r\n)
- 认证信息需要进行Base64编码
- 邮件正文以单独一行的"."结束
- 安全性考虑:
- 密码输入框使用Password类型
- 搜狐邮箱需要独立密码
这个示例很好地展示了如何在HarmonyOS Next上使用仓颉语言进行网络编程,可以作为学习SMTP协议和仓颉网络编程的参考。