軟件技術(shù)-零基礎(chǔ)-Golang用Hotmail發(fā)送驗(yàn)證郵件

歡迎關(guān)注我的專欄( つ??ω??)つ【人工智能通識】
【匯總】2019年4月專題


如何用Golang自動向用戶郵箱發(fā)送驗(yàn)證碼?

人工智能通識-2019年3月專題匯總

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)這里注冊Hotmail/Outlook郵箱

注冊成功之后,右上角點(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"。

點(diǎn)這里看smtp的詳細(xì)說明

它的基本流程是:

  • 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.goapi/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改名為Loginapp.go中設(shè)置的一致。

同樣修改register.go:

  • 第一行改為package api
  • 修改struct ReqDS結(jié)構(gòu)改名為struct registerReqDS,并對應(yīng)修改下面的ds := registerReqDS{}
  • HandleFunc改名為Registerapp.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 umailcosnt 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容