写点什么

鸿蒙网络编程系列 4- 实现 Smtp 邮件发送客户端

作者:长弓三石
  • 2024-10-22
    广东
  • 本文字数:6053 字

    阅读完需:约 20 分钟

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@Componentstruct 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()函数,执行这个转换。


(本文作者原创,除非明确授权禁止转载)


本文源码地址:


https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tcp/SmtpClient


本系列源码地址:


https://gitee.com/zl3624/harmonyos_network_samples


发布于: 刚刚阅读数: 6
用户头像

长弓三石

关注

还未添加个人签名 2024-10-16 加入

二十多年软件开发经验的软件架构师,华为HDE、华为云HCDE、仓颉语言CLD、CCS,著有《仓颉语言网络编程》、《仓颉语言元编程》、《仓颉语言实战》、《鲲鹏架构入门与实战》等书籍,清华大学出版社出版。

评论

发布
暂无评论
鸿蒙网络编程系列4-实现Smtp邮件发送客户端_DevEco Studio_长弓三石_InfoQ写作社区