1. SMTP简介
SMTP协议是邮件发送的应用层协议,被称为简单邮件传输协议,采用请求应答方式进行通讯,SMTP命令和响应是基于ASCII文本的,并且以回车换行符(CR和LF)结束,SMTP服务端的底层一般使用TCP协议,在掌握了TCP通讯的方法后,就可以自己实现一个简单的邮件发送客户端,关于TCP通讯的常用方法可以参见我上一篇博文鸿蒙网络编程系列3-TCP客户端通讯示例
SMTP的标准命令有14个,后来通过扩展命令又增加了一些,针对本文的示例,需要了解ehlo(问候信息)、auth login(登录)、mail from(发件人)、rcpt to(收件人)、data(邮件内容)、quit(登出)等命令,关于这些命令的详细解释可以查找相关文档,这些命令比较简单,不深入研究的话,光看看本文的具体示例也大概能明白用法。
2.邮件发送客户端示例
本示例演示登录腾讯邮箱SMTP服务器并发送邮件的过程,不同的邮件服务器对密码的定义可能不一样,在腾讯的邮件服务器里,密码是指授权码,可以登录官方网站了解生成方式。本示例成功登录并发送邮件后的截图如下所示:
对于应用界面,上部是邮箱服务器地址和用户名密码,腾讯邮箱服务器地址为smtp.qq.com,对应的ip地址为157.148.54.34;中部是要发送的邮件信息,包括收件人邮箱、标题、内容等部分;最下面是发送日志,把客户端和服务端交互的过程记录下来,方便调试分析。
下面详细介绍创建该应用的步骤。
步骤1:创建Empty Ability项目。
步骤2:在module.json5配置文件加上对权限的声明:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
这里添加了访问互联网的权限。
步骤3:在Index.ets文件里添加如下的代码:
import socket from '@ohos.net.socket';
import util from '@ohos.util';
//执行TCP通讯的对象
let tcpSocket = socket.constructTCPSocketInstance();
@Entry
@Component
struct Index {
//连接、通讯历史记录
@State msgHistory: string = ''
//套接字是否已绑定本地地址
bindLocal: boolean = false
//服务器是否响应(发送数据到客户端)
isServerResponse: boolean = false
//服务端地址,smtp.qq.com的ip地址为157.148.54.34
@State serverAddr: string = "157.148.54.34"
//服务端端口,smtp.qq.com的端口为587
@State serverPort: number = 587
//用户名
@State userName: string = "用户名,一般是你的邮箱地址"
//密码,对于腾讯邮箱,这里是授权码
@State passwd: string = "you auth code or password"
//收件人邮箱列表(如果多个使用逗号分隔)
@State rcptList: string = "8888@qq.com,9999@gmail.com"
//邮件标题
@State mailTitle: string = "测试邮件标题"
//发件人邮箱
@State mailFrom: string = "你的邮箱"
//邮件内容
@State mailContent: string = "This is greeting from Harmony OS"
//是否可以登录
@State canLogin: boolean = false
//是否可以发送邮件
@State canSend: boolean = false
scroller: Scroller = new Scroller()
build() {
Row() {
Column() {
Text("邮件发送客户端")
.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.toString() })
.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.toString() })
.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.toString() })
.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(80)
.fontSize(12)
Button("发送")
.enabled(this.canSend)
.onClick(() => {
this.sendMail()
})
.width(70)
.fontSize(14)
Scroll(this.scroller) {
Text(this.msgHistory)
.textAlign(TextAlign.Start)
.padding(10)
.width('100%')
.backgroundColor(0xeeeeee)
.fontSize(10)
}
.align(Alignment.Top)
.backgroundColor(0xeeeeee)
.height(200)
.flexGrow(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.On)
.scrollBarWidth(20)
}
.flexGrow(1)
.width('100%')
.padding(5)
.height(300)
}
.width('100%')
.justifyContent(FlexAlign.Start)
.height('100%')
.padding(10)
}
.height('100%')
}
//发送邮件
async sendMail() {
//发送发件人信箱
await this.exeCmdAndWait4Response(`mail from:<${this.mailFrom}>`)
let rcptMails = this.rcptList.split(',')
for(let i=0;i<rcptMails.length;i++){
//发送收件人信箱
let rcpt = rcptMails[i]
await this.exeCmdAndWait4Response(`rcpt to:<${rcpt}>`)
}
//准备发送邮件内容
await this.exeCmdAndWait4Response("data")
let mailBody =`Subject: ${this.mailTitle} \r\nFrom: ${this.mailFrom}\r\n\r\n${this.mailContent}\r\n.`
//发送邮件内容
await this.exeCmdAndWait4Response(mailBody)
//登出
await this.exeCmdAndWait4Response("quit")
}
async bindSocket() {
//本地地址
let localAddress = { address: "0.0.0.0", family: 1 }
await tcpSocket.bind(localAddress)
.then(() => {
this.msgHistory += 'C:bind success' + "\r\n";
})
.catch((e) => {
this.msgHistory += 'C:bind fail ' + e.message + "\r\n";
})
//收到消息时的处理
tcpSocket.on("message", async (value) => {
this.isServerResponse = true
let msg = buf2String(value.message)
this.msgHistory += "S:" + msg + "\r\n"
this.scroller.scrollEdge(Edge.Bottom)
})
this.bindLocal = true
}
//登录服务器
async login() {
//首先判断套接字是否绑定到本地地址,如果没有绑定就绑定一下
if (!this.bindLocal) {
this.bindSocket()
}
//服务器地址
let serverAddress = { address: this.serverAddr, port: this.serverPort, family: 1 }
//连接smtp服务器
await tcpSocket.connect({ address: serverAddress })
.then(() => {
this.msgHistory += 'C:connect success ' + "\r\n";
})
.catch((e) => {
this.msgHistory += 'C:connect fail ' + e.message + "\r\n";
return
})
//等待服务器响应
await this.wait4ServerResponse()
//服务端发送ehlo,anyname为发送服务器名称,可以随便写,但不能没有
await this.exeCmdAndWait4Response("ehlo anyname")
//告诉服务器,我要登录了
await this.exeCmdAndWait4Response("auth login")
//发送用户名,用户名需要base64编码
let loginName = string2Base64(this.userName)
await this.exeCmdAndWait4Response(loginName)
//发送密码,密码需要base64编码,对于腾讯邮箱,这里是授权码,具体的可以参考腾讯邮箱文档
//你说为什么要编码?自欺欺人罢了,起不到加密作用,还给新手带来一堆bug
//正常情况下,登录后服务器就认可你了,下面就可以正式发送邮件了
let passWd = string2Base64(this.passwd)
await this.exeCmdAndWait4Response(passWd)
//设置发送按钮可用,当然,这里还要一堆逻辑需要判断,比如判断服务端的返回信息是否表明登录成功了
//毕竟本例只是演示smtp的使用,实际中可以添加上这些逻辑
this.canSend = true
}
//给服务器发送命令并等待响应
async exeCmdAndWait4Response(cmd:string){
this.isServerResponse = false
let result = await this.sendCmd2ServerWithCRLF(cmd)
if (result != true) {
return
}
//等待服务器响应
await this.wait4ServerResponse()
}
//等待服务器响应
async wait4ServerResponse() {
while (!this.isServerResponse) {
await sleep(100)
}
}
//发送命令到服务端,自动在命令后加上回车换行
async sendCmd2ServerWithCRLF(cmd: string): Promise<boolean> {
cmd = cmd + "\r\n"
let result = false
await tcpSocket.send({ data: cmd })
.then(() => {
this.msgHistory += "C:" + cmd;
result = true
})
.catch((e) => {
this.msgHistory += 'C:send fail ' + e.message + "\r\n";
result = false
})
return result
}
}
//对字符串base64编码
function string2Base64(src: string) {
let textEncoder = new util.TextEncoder();
let encodeValue = textEncoder.encodeInto(src)
let tool = new util.Base64Helper()
return tool.encodeToStringSync(encodeValue)
}
//ArrayBuffer转utf8字符串
function buf2String(buf: ArrayBuffer) {
let msgArray = new Uint8Array(buf);
let textDecoder = util.TextDecoder.create("utf-8");
return textDecoder.decodeWithStream(msgArray)
}
//休眠指定的毫秒数
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
步骤4:编译运行,可以使用模拟器或者真机。
步骤5:配置服务器信息、填写用户名密码以及要发送的邮件信息,然后单击“登录”按钮进行登录,成功后单击“发送”按钮发送邮件。
步骤6:查看收件人信箱看看是否发送成功,首先是第一个腾讯邮箱,发送成功了:
然后看看第二个,第二个是谷歌的gmail信箱,也成功了:
这样,我们就创建了鸿蒙版本的邮件发送客户端。
3.注意事项
在SMTP协议里,对于用户名和密码需要转换为base64格式,所以代码里包括了一个string2Base64()函数,执行这个转换。
最后
有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。
点击领取→【纯血版鸿蒙全套最新学习资料】(安全链接,放心点击)希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!
这份鸿蒙(HarmonyOS NEXT)资料包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、(南向驱动、嵌入式等)鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。
鸿蒙(HarmonyOS NEXT)最新学习路线
有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。
获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料
HarmonyOS Next 最新全套视频教程
《鸿蒙 (OpenHarmony)开发基础到实战手册》
OpenHarmony北向、南向开发环境搭建
《鸿蒙开发基础》
- ArkTS语言
- 安装DevEco Studio
- 运用你的第一个ArkTS应用
- ArkUI声明式UI开发
- .……
《鸿蒙开发进阶》
- Stage模型入门
- 网络管理
- 数据管理
- 电话服务
- 分布式应用开发
- 通知与窗口管理
- 多媒体技术
- 安全技能
- 任务管理
- WebGL
- 国际化开发
- 应用测试
- DFX面向未来设计
- 鸿蒙系统移植和裁剪定制
- ……
《鸿蒙进阶实战》
- ArkTS实践
- UIAbility应用
- 网络案例
- ……
大厂面试必问面试题
鸿蒙南向开发技术
鸿蒙APP开发必备
鸿蒙生态应用开发白皮书V2.0PDF
总结
总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,他们才能在这个变革的时代中立于不败之地。