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_entryimport 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 {@Statevar title: String = 'SMTP郵件發送客戶端示例';//連接、通訊歷史記錄@Statevar msgHistory: String = ''//服務器是否響應(發送數據到客戶端)var isServerResponse: Bool = false//服務端地址,smtp.sohu.com的ip地址為116.130.217.16@Statevar serverAddr: String = "116.130.217.16"//服務端端口,smtp.sohu.com的端口為25,不同的smtp服務器端口可能不一樣@Statevar serverPort: UInt16 = 25//用戶名@Statevar userName: String = "youmail@sohu.com"//密碼,對于搜狐郵箱,這里是獨立密碼@Statevar passwd: String = "youpassword"//收件人郵箱列表(如果多個使用逗號分隔)@Statevar rcptList: String = "*****@sohu.com,****@qq.com"//發件人郵箱@Statevar mailFrom: String = "youmail@sohu.com"//郵件標題@Statevar mailTitle: String = "測試郵件標題"//郵件內容@Statevar mailContent: String = "這是來自鴻蒙的問候!"//是否正在登錄@Statevar isLogin: Bool = false//是否可以發送郵件@Statevar canSend: Bool = false//TCP客戶端var tcpClient: ?TcpSocket = Nonelet 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 = falsereturn}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