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

发布于 1周前 作者 sinazl 来自 鸿蒙OS

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

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信箱,也成功了:

Gmail邮箱发送成功截图

这样,我们就创建了鸿蒙版本的邮件发送客户端。

3. 注意事项

在SMTP协议里,对于用户名和密码需要转换为base64格式,所以代码里包括了一个string2Base64()函数,执行这个转换。

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

本文源码地址:

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

本系列源码地址:

https://gitee.com/zl3624/harmonyos_network_samples

其他鸿蒙网络编程文章:

鸿蒙网络编程系列1-UDP通讯示例

鸿蒙网络编程系列2-UDP回声服务器的实现

鸿蒙网络编程系列3-TCP客户端通讯示例

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

鸿蒙网络编程系列5-TCP连接超时分析

鸿蒙网络编程系列6-TCP数据粘包表现及原因分析

鸿蒙网络编程系列7-TLS安全数据传输单向认证示例

鸿蒙网络编程系列8-TLS安全数据传输双向认证示例

鸿蒙网络编程系列9-使用HttpRequest模拟登录示例

鸿蒙网络编程系列10-使用HttpRequest下载文件到本地示例

鸿蒙网络编程系列11-使用HttpRequest上传文件到服务端示例

鸿蒙网络编程系列12-使用Request部件下载文件到本地示例

鸿蒙网络编程系列13-使用Request部件上传文件到服务端示例

鸿蒙网络编程系列14-WebSocket客户端通讯示例

鸿蒙网络编程系列15-域名解析示例

鸿蒙网络编程系列16-获取Wifi信息示例

鸿蒙网络编程系列17-网络状态监测示例

鸿蒙网络编程系列18-Web组件加载网页的四种方式示例

鸿蒙网络编程系列19-获取网络连接信息并选择一种绑定到应用示例

鸿蒙网络编程系列20-解决web组件加载网页白屏示例

鸿蒙网络编程系列21-使用HttpRequest上传任意文件到服务端示例


更多关于HarmonyOS 鸿蒙Next网络编程系列4-实现Smtp邮件发送客户端的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

在HarmonyOS(鸿蒙Next)中实现SMTP邮件发送客户端,可以使用ohos.net.smtp包中的相关类。以下是一个简单的实现步骤:

  1. 导入必要的包

    import smtp from '[@ohos](/user/ohos).net.smtp';
  2. 配置SMTP服务器信息

    const smtpConfig = {
      host: 'smtp.example.com', // SMTP服务器地址
      port: 465, // SMTP服务器端口
      secure: true, // 是否使用SSL/TLS
      auth: {
        user: 'your-email@example.com', // 发件人邮箱
        pass: 'your-email-password' // 发件人邮箱密码
      }
    };
  3. 创建SMTP客户端

    const client = smtp.createClient(smtpConfig);
  4. 发送邮件

    const mailOptions = {
      from: 'your-email@example.com', // 发件人邮箱
      to: 'recipient@example.com', // 收件人邮箱
      subject: 'Test Email', // 邮件主题
      text: 'This is a test email from HarmonyOS.' // 邮件正文
    };
    
    client.sendMail(mailOptions, (error, info) => {
      if (error) {
        console.error('Error sending email:', error);
      } else {
        console.log('Email sent:', info.response);
      }
    });
  5. 关闭SMTP客户端

    client.close();

以上代码展示了如何在HarmonyOS中使用SMTP协议发送邮件。通过配置SMTP服务器信息、创建SMTP客户端、发送邮件并关闭客户端,可以实现基本的邮件发送功能。

更多关于HarmonyOS 鸿蒙Next网络编程系列4-实现Smtp邮件发送客户端的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS鸿蒙Next中实现SMTP邮件发送客户端,首先需导入javax.mail库以支持邮件协议。创建Session对象,配置SMTP服务器地址、端口及认证信息。使用MimeMessage类构建邮件内容,设置发件人、收件人、主题和正文。通过Transport类连接SMTP服务器并发送邮件。注意处理异常,确保网络连接稳定。此实现适用于鸿蒙系统的轻量级应用,支持高效、安全的邮件发送功能。

回到顶部
AI 助手
你好,我是IT营的 AI 助手
您可以尝试点击下方的快捷入口开启体验!