歡迎關(guān)注我的專欄( つ??ω??)つ【人工智能通識】
【匯總】2019年4月專題
如何用Golang自動向用戶郵箱發(fā)送驗(yàn)證碼?

SMTP
Simple Mail Transfer Protocol,SMTP簡單郵件傳輸協(xié)議,它是在網(wǎng)絡(luò)傳輸電子郵件的常用標(biāo)準(zhǔn)。
我們的Golang可以通過SMTP方式調(diào)用右鍵服務(wù)商(比如Hotmail)的發(fā)郵件功能,替我們自動發(fā)郵件。
查看hotmail的SMTP設(shè)置
首先需要注冊一個hotmail郵箱。
注冊成功之后,右上角點(diǎn)齒輪彈出設(shè)置搜索,輸入pop,點(diǎn)擊進(jìn)入POP和IMAP設(shè)置。

然后開啟POP選項(xiàng),注意下面的SMTP設(shè)置。

Golang的net/smtp
Golang提供了一個基本的SMTP發(fā)郵件模塊,可以導(dǎo)入它import "net/smtp"。
它的基本流程是:
- 用
smtp.SendMail方法發(fā)送郵件,它需要幾個參數(shù)addr string, a Auth, from string, to []string, msg []byte,分別是addr發(fā)送服務(wù)器名稱(帶端口),Auth用戶授權(quán)(用戶名和密碼),發(fā)件人郵箱from,目標(biāo)收件人郵箱to,發(fā)送的內(nèi)容msg字符串字節(jié)形式。 -
smtp.PlainAuth可以生成這樣用戶授權(quán)(用戶名和密碼)。 - msg需要拼接,一般包含
From,To,Subject(標(biāo)題),Content-Type(文字類型),郵件文字。
一般情況直接使用Golang的這個設(shè)置就可以了,但是Hotmail目前已經(jīng)不再支持PlainAuth的認(rèn)證模式,所以就要做單獨(dú)的處理。
改名tool.go新增mail.go
在Golang里面,文件夾就是import的包名package,比如我們導(dǎo)入app/tool其實(shí)就是導(dǎo)入了tool文件夾下所有的.go文件,這些文件都必須以package tool開頭,每個文件里的首字母大寫的變量或者函數(shù)都會被放到tool.Xxx這樣的二級命令里,并且如果文件里面有init函數(shù)也會被自動運(yùn)行。
所以tool.go文件換成mongo.go也不會產(chǎn)生任何影響。那么我們就把它改名。
然后我們創(chuàng)建新的app/tool/mail.go文件,代碼如下:
package tool
import (
"net/smtp"
)
const umail string = "zhyuzhnd@hotmail.com"
const upw string = "zhyuzh3d"
const host string = "smtp.office365.com:587"
type loginAuth struct {
username, password string
}
func genLoginAuth(username, password string) smtp.Auth {
return &loginAuth{username, password}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", []byte(a.username), nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case "Username:":
return []byte(a.username), nil
case "Password:":
return []byte(a.password), nil
}
}
return nil, nil
}
//SendMail 發(fā)送郵件
func SendMail(target string, body string, subject string) error {
auth := genLoginAuth(umail, upw)
contentType := "Content-Type: text/plain" + "; charset=UTF-8"
msg := []byte("To: " + target +
"\r\nFrom: " + umail +
"\r\nSubject: " + subject +
"\r\n" + contentType + "\r\n\r\n" +
body)
err := smtp.SendMail(host, auth, umail, []string{target}, msg)
if err != nil {
return err
}
return nil
}
這個代碼看上去挺亂,但不必太理會它的意思,只要注意以下幾點(diǎn):
- 這個是針對借用hotmail郵箱來發(fā)送郵件的。簡單說就是我們的Golang呼叫hotmail郵箱,模擬用戶名和密碼登錄,模擬發(fā)郵件。
- 這個代碼并不完全適用于其他的郵箱(QQ郵箱或者Gmail或者網(wǎng)易郵箱等),可以參考官方這個說明文檔了解一下,其實(shí)可能只有Hotmail郵箱才這么麻煩。
- 頂部的幾個常量中的
umai,upw不要使用我的,你需要自己去注冊Hotmail郵箱用你自己的密碼,小心不要把你常用的密碼上傳到Github!一定不要!一定不要!一定不要!。 - 注意這個發(fā)送郵件函數(shù)
SendMail(target string, body string, subject string) error包含了三個參數(shù),target發(fā)送目標(biāo)郵箱,body是發(fā)送的文字內(nèi)容,subject是郵件的標(biāo)題。
重新組織所有api
同樣,我們也把api/login/login.go和api/register/register.go移動到api文件夾下面,把空的register文件夾和login文件夾刪除。
這樣我們可以在app.go中只導(dǎo)入一個src/api就可以了,并修改下面的服務(wù)接口設(shè)置:
//API-注冊登錄相關(guān)
http.HandleFunc("/api/register", api.Register)
http.HandleFunc("/api/login", api.Login)
然后我們還要修改login.go中:
- 第一行改為
package api - 修改
struct ReqDS結(jié)構(gòu)改名為struct loginReqDS,并對應(yīng)修改下面的ds := loginReqDS{}。 - 將
HandleFunc改名為Login與app.go中設(shè)置的一致。
同樣修改register.go:
- 第一行改為
package api - 修改
struct ReqDS結(jié)構(gòu)改名為struct registerReqDS,并對應(yīng)修改下面的ds := registerReqDS{}。 - 將
HandleFunc改名為Register與app.go中設(shè)置的一致。
然后測試運(yùn)行,檢查已有的舊功能是否正常。
修改注冊頁面register.html
我們在頁面上增加驗(yàn)證碼界面元素:
<div class="form-group">
<label for="exampleInputPassword1">郵箱驗(yàn)證碼:</label>
<div class="row">
<div class="col-7">
<input id='verify' onkeyup="checkVerify()" class="form-control"
placeholder="請輸入6位驗(yàn)證碼">
</div>
<div class="col-5">
<button id='sendVerify' onClick="sendVerify()"
class="btn btn-success btn-block">發(fā)送驗(yàn)證碼</button>
</div>
</div>
<small id='verifyTip' style="display:none">請輸入6位數(shù)字驗(yàn)證碼</small>
</div>
利用Go Live查看效果大致是:

然后我們再修改下面的script部分,增加檢查驗(yàn)證碼格式的checkVerify和發(fā)送驗(yàn)證碼郵件的sendVerify方法。
checkVerify部分:
//檢查驗(yàn)證碼格式是否正確
var verifyRe = /^[0-9]{6}$/
function checkVerify() {
var ver = $('#verify').val();
if (verifyRe.test(ver) == false) {
$('#verifyTip').css('display', 'block')
$('#verify').removeClass('is-valid')
$('#verify').addClass('is-invalid')
} else {
$('#verifyTip').css('display', 'none')
$('#verify').removeClass('is-invalid')
$('#verify').addClass('is-valid')
}
checkBtn()
}
sendVerify部分:
//發(fā)送驗(yàn)證郵件
function sendVerify() {
var data = {
Email: $('#email').val(),
}
$.post('/api/sendRegVerifyMail', JSON.stringify(data), function (res) {
alert(res.Msg);
}, 'json')
}
此外,我們還要修改checkBtn檢測提交按鈕的函數(shù)中應(yīng)該加入驗(yàn)證碼格式是否正確的檢查:
//檢查按鈕是否可以被開啟
function checkBtn() {
var agree = $('#agree').is(':checked');
var mail = $('#email').val();
var pw = $('#pw').val();
var ver = $('#verify').val();
if (pwRe.test(pw) && mailRe.test(mail) && verifyRe.test(ver) && agree) {
$('#regBtn').removeAttr('disabled')
} else {
$('#regBtn').attr('disabled', 'true')
}
}
最后我們還要限制發(fā)送驗(yàn)證按鈕只能在郵箱格式正確的時候才被啟用,所以先在界面代碼加入disabled禁止...class="btn btn-success btn-block" disabled="true">發(fā)送驗(yàn)證碼</button>,然后在checkMail中對它進(jìn)行開啟或關(guān)閉:
//檢查郵箱的輸入格式
function checkMail() {
var mail = $('#email').val();
if (mailRe.test(mail) == false) {
$('#mailTip').css('display', 'block')
$('#email').removeClass('is-valid')
$('#email').addClass('is-invalid')
$('#sendVerify').attr('disabled', 'true')
} else {
$('#mailTip').css('display', 'none')
$('#email').removeClass('is-invalid')
$('#email').addClass('is-valid')
$('#sendVerify').removeAttr('disabled')
}
checkBtn()
}
增加sendRegVerifyMail.go
對應(yīng)頁面上的sendVerify方法的$.post('/api/sendRegVerifyMail',...,我們在Golang服務(wù)器上也應(yīng)該增加對應(yīng)的服務(wù)接口。
創(chuàng)建api/sendRegVerifyMail.go,其內(nèi)容如下(我們已經(jīng)擁有一個能發(fā)送郵件的tool.SendMail方法):
package api
import (
"app/tool"
"app/util"
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"regexp"
"strconv"
"time"
"go.mongodb.org/mongo-driver/bson"
)
type sendRegVerifyMailReqDS struct {
Email string
}
//SendRegVerifyMail 注冊接口處理函數(shù)
func SendRegVerifyMail(w http.ResponseWriter, r *http.Request) {
ds := sendRegVerifyMailReqDS{}
json.NewDecoder(r.Body).Decode(&ds)
mailRe, _ := regexp.Compile(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`)
if !mailRe.MatchString(ds.Email) {
util.WWrite(w, 1, "郵箱格式錯誤。", nil)
return
}
//檢查是否存在,如果已經(jīng)存在且時間小于1分鐘就就不再發(fā)送
dbc := tool.MongoDBCLient.Database("myweb").Collection("regVerify")
var u bson.M
dbc.FindOne(context.TODO(), bson.M{"Email": ds.Email}).Decode(&u)
now := time.Now().Unix()
if u["Ts"] != nil && now-u["Ts"].(int64) < 60 {
util.WWrite(w, 1, "請不要重復(fù)發(fā)送郵件。", nil)
return
}
//生成隨機(jī)6位數(shù),并發(fā)送
code := rand.Intn(899999) + 100000
s := strconv.Itoa(code)
err := tool.SendMail(ds.Email, "您在www.myweb.com的注冊碼是:"+s, "來自Myweb的注冊驗(yàn)證碼")
if err != nil {
util.WWrite(w, 1, "發(fā)送郵件失敗。", nil)
fmt.Println(err)
return
}
//刪除原有數(shù)據(jù),創(chuàng)建新數(shù)據(jù)
dbc.DeleteOne(context.TODO(), bson.M{"Email": ds.Email})
dt := bson.M{"Code": s, "Email": ds.Email, "Ts": now}
_, err = dbc.InsertOne(context.TODO(), dt)
if err != nil {
util.WWrite(w, 1, "寫入數(shù)據(jù)庫出錯。", nil)
fmt.Println(err)
} else {
util.WWrite(w, 0, "發(fā)送成功,請檢查郵箱。", nil)
}
return
}
注意其中的幾點(diǎn):
- 首先我們檢查了郵箱格式,格式不對直接返回。
- 然后我們堅(jiān)持?jǐn)?shù)據(jù)集
regVerify中是否已經(jīng)存在這個郵箱的數(shù)據(jù),而且還檢測這個數(shù)據(jù)的Ts,它是timestamp時間戳,它記錄了上一次發(fā)送郵件的時間,如果這個時間到現(xiàn)在now不到60秒,那么就提示不要重復(fù)發(fā)郵件。 - 生成100000~999999的六位數(shù),并利
tool.SendMail用發(fā)送郵件,三個參數(shù),發(fā)給誰,內(nèi)容是什么,標(biāo)題是什么。 - 嘗試刪除原有數(shù)據(jù)
DeleteOne然后插入新的數(shù)據(jù)InsertOne,注意這里的Ts時間戳數(shù)字被存儲了。
寫好這個文件,別忘了在app.go中添加服務(wù)路徑:
http.HandleFunc("/api/sendRegVerifyMail", api.SendRegVerifyMail)
關(guān)于Hotmail郵箱
你必須要注冊一個Hotmail郵箱才能借用它來發(fā)送郵件。

注冊之后修改你mail.go里面的const umail和cosnt upw。
啟動app.go之后嘗試在注冊頁面上輸入郵箱,然后點(diǎn)擊發(fā)送驗(yàn)證碼按鈕,很可能會沒有反應(yīng),Golang的輸出上會出錯,這是正常的,請?jiān)诰W(wǎng)頁里檢查你新注冊的Hotmail郵箱,會收到一封提示郵件,這是要求你綁定手機(jī)號,綁定之后才可以正常發(fā)郵件。
在Hotmail網(wǎng)站綁定好手機(jī)之后,重新啟動app.go,注冊頁中輸入郵箱點(diǎn)擊發(fā)送按鈕,稍后能彈出發(fā)送成功,請檢查郵箱。提示,如果連續(xù)點(diǎn)擊則會提示請不要重復(fù)發(fā)送郵件。然后檢查郵件,在垃圾郵件里面可以找到自己發(fā)送的內(nèi)容:

注意Hotmail對這種用Golang的
smtp.SendMail發(fā)送郵件的方式有限制,官方說法是每天不超過100封,但實(shí)際上可能十幾封之后就被禁止發(fā)送了,需要你重新進(jìn)入郵箱激活才能用,也可能今天就用不了了...遇到這個情況沒有什么好辦法,只能再去注冊一個新郵箱測試了。
在register.html中限制發(fā)送按鈕
我們也可以在網(wǎng)頁代碼中限定用戶每次點(diǎn)擊發(fā)送按鈕之后都凍結(jié)3秒,避免用戶不小心連續(xù)點(diǎn)兩下。
//發(fā)送驗(yàn)證郵件
function sendVerify() {
var data = {
Email: $('#email').val(),
}
$.post('/api/sendRegVerifyMail', JSON.stringify(data), function (res) {
alert(res.Msg);
}, 'json')
$('#sendVerify').attr('disabled', 'true')
setTimeout(() => {
$('#sendVerify').removeAttr('disabled')
}, 3000);
}
好了,差不多的時候可以切換到左側(cè)的版本管理面板,輸入框?qū)扅c(diǎn)什么,然后Ctril+回車提交代碼到Git,然后再從小菜單推送到Github。

如果彈出username和mail的提示,請切換到終端輸入下面的命令(用戶名和郵箱任意):
git config --global user.name "zhyuzh"
git config --global user.email "zhyuzh3d@hotmail.com"
幾點(diǎn)補(bǔ)充
- 正常開發(fā)中都會使用專業(yè)的郵件推送服務(wù),比如可以購買使用阿里云的郵件推送服務(wù),但你首先需要有個已經(jīng)備案的域名,購買域名只要幾十塊第一年,但備案就需要再花至少幾十塊買個ECS云服務(wù)器,而且備案一般都要一兩周才能完成...當(dāng)然最后郵件推送服務(wù)也是要花錢的。所以,如果你計(jì)劃成為一名正式開發(fā)者,這些都是不可少的。
- 另外一個快一些的辦法是改用手機(jī)號碼注冊+短信驗(yàn)證,阿里云提供了很劃算的短信套餐,你以后可以考慮學(xué)習(xí)使用這個。
雖然我們基本上完成了注冊功能,但還沒有提供修改密碼相關(guān)的功能,登錄功能也不完善,稍后的文章我們會繼續(xù)這些內(nèi)容。
歡迎關(guān)注我的專欄( つ??ω??)つ【人工智能通識】
每個人的智能新時代
如果您發(fā)現(xiàn)文章錯誤,請不吝留言指正;
如果您覺得有用,請點(diǎn)喜歡;
如果您覺得很有用,歡迎轉(zhuǎn)載~
END