鸿蒙网络编程系列54-仓颉版实现Smtp邮件发送客户端

发布于:2025-06-20 ⋅ 阅读:(18) ⋅ 点赞:(0)

1. SMTP邮件发送客户端

在本系列的第4篇文章《鸿蒙网络编程系列4-实现SMTP邮件发送客户端》中,基于ArkTS语言在API9环境下使用TCPSocket对象演示了SMTP客户端的实现,并且通过腾讯邮件服务器执行了实际的邮件发送。不过,在2024年末,腾讯发了一个通知,从2024年11月20日开始,停用以明文非加密方式登录的第三方邮件客户端,必需启用SSL/TLS加密方式。不过,除了腾讯邮件发送服务器,还有很多其他邮件服务器支持使用明文登录,其中比较知名的有搜狐邮箱,可以通过如下的方式启用:

保存的时候,搜狐邮箱会自动生成独立密码,将来可以使用这个密码执行登录。

本文将使用仓颉语言在API17环境下实现SMTP邮件发送客户端,具体的邮件发送将通过搜狐邮箱实现,关于SMTP协议的相关基础知识,可以参考本系列第4篇文章的第一部分,这里不再赘述。

2. 邮件发送客户端示例演示

本示例运行后的页面如图所示:

输入SMTP服务器地址和端口(这里输入的是搜狐邮箱发送服务器的地址),再输入邮箱用户名和登录密码,此时就可以单击“登录”按钮执行登录了,如图所示:

登录成功后,输入收件人、发件人邮箱地址以及邮件的标题和内容,再单击下面的“发送邮件”按钮,既可以执行邮件发送,过程如下所示:

发送成功后,登录收件人的邮箱,就可以查看发送的邮件了,邮件内容如下所示:

3. 邮件发送客户端示例编写

下面详细介绍创建该示例的步骤(确保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.130.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(FlexParams(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(FlexParams(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(FlexParams(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(FlexParams(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(FlexParams(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(FlexParams(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(
                    FlexParams(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


网站公告

今日签到

点亮在社区的每一天
去签到