From e0f8d15ec71fa6363685ad8c113cfb2db5cb3cb3 Mon Sep 17 00:00:00 2001 From: yehongyang Date: Thu, 14 Oct 2021 15:22:18 +0800 Subject: [PATCH] =?UTF-8?q?=E9=82=AE=E4=BB=B6=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E6=8F=90=E4=BA=A4=201=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dao/base/EscalateModelHead.dao.go | 19 + .../implments/EscalateModelHead.dao.impl.go | 55 +++ main.go | 2 - models/base/EscalateMsg.model.go | 35 +- .../EscalateModelHead.service.impl.go | 150 ++++++- .../implments/EscalateMsg.service.impl.go | 9 + utils/email.go | 193 ++++++++ utils/mysmtp/auth.go | 110 +++++ utils/mysmtp/mysmtp.go | 47 ++ utils/mysmtp/smtp.go | 423 ++++++++++++++++++ utils/reflect.go | 34 ++ 11 files changed, 1057 insertions(+), 20 deletions(-) create mode 100644 utils/email.go create mode 100644 utils/mysmtp/auth.go create mode 100644 utils/mysmtp/mysmtp.go create mode 100644 utils/mysmtp/smtp.go create mode 100644 utils/reflect.go diff --git a/dao/base/EscalateModelHead.dao.go b/dao/base/EscalateModelHead.dao.go index 867dfde..4a84e71 100644 --- a/dao/base/EscalateModelHead.dao.go +++ b/dao/base/EscalateModelHead.dao.go @@ -75,6 +75,25 @@ type EscalateModelHeadDAO interface { * ******************************************************************************/ SelectOne(string) (*model.EscalateModelHead, error) + /****************************************************************************** + * + * @Function Name : SelectAll + *----------------------------------------------------------------------------- + * + * @Description : 查找所有EscalateModelHead + * + * @Function Parameters : 主键 + * + * @Return Value : 查找到的EscalateModelHead + * + * @Return Value : 执行时发生的错误 + * + * @Author : 代码生成器创建 + * + * @Date : 2021-10-13 10:39:41 + * + ******************************************************************************/ + SelectAll() (map[string]model.EscalateModelHead, error) /****************************************************************************** * * @Function Name : UpdateOne diff --git a/dao/base/implments/EscalateModelHead.dao.impl.go b/dao/base/implments/EscalateModelHead.dao.impl.go index c007f12..1770875 100644 --- a/dao/base/implments/EscalateModelHead.dao.impl.go +++ b/dao/base/implments/EscalateModelHead.dao.impl.go @@ -98,6 +98,61 @@ func (impl *EscalateModelHeadDAOImplement) SelectOne(escalateModelId string) (*m return &data, nil } +/****************************************************************************** + * + * @Reference leit.com/LAPP_CHEERSSON_BACKEND/dao/base/EscalateModelHeadDAO.SelectAll + * + ******************************************************************************/ +func (impl *EscalateModelHeadDAOImplement) SelectAll() (map[string]model.EscalateModelHead, error) { + parameters := []interface{}{impl.plantNr} + where := fmt.Sprintf("%s = ?", meta.EscalateModelHead_PlantNr.ColumnName) + session := impl.session.Table(impl.meta.TableName) + session = session.Where(where, parameters...) + session = session.OrderBy(meta.EscalateModelHead_PlantNr.ColumnName) + + var data []model.EscalateModelHead + err := session.Find(&data) + if err != nil { + return nil, grmi.NewDataBaseError(err) + } + newData := make(map[string]model.EscalateModelHead) + var oneKey []string + for _, _v := range data { + oneKey = append(oneKey, _v.EscalateModelId) + } + //查询EscalateModelLevellst + var leveList []model.EscalateModelLevellst + leveListMap := make(map[string][]model.EscalateModelLevellst) + err = impl.session.Table(meta.EscalateModelLevellst.TableName).In("EscalateModelId", oneKey).Find(&leveList) + if err != nil { + return nil, grmi.NewDataBaseError(err) + } + //查询EscalateModelLevelDetail + var detailList []model.EscalateModelLevelDetail + dateilListMap := make(map[string][]model.EscalateModelLevelDetail) + err = impl.session.Table(meta.EscalateModelLevelDetail.TableName).OrderBy(meta.EscalateModelLevellst_EscalateLevel.Name+" asc").In("EscalateModelId", oneKey).Find(&detailList) + if err != nil { + return nil, grmi.NewDataBaseError(err) + } + //将详情放入map数组 + for _, _v := range detailList { + dateilListMap[string(_v.EscalateLevel)+","+_v.EscalateModelId] = append(dateilListMap[string(_v.EscalateLevel)+","+_v.EscalateModelId], _v) + } + //将详情map 放入listmap + for _, _v := range leveList { + _v.EscalateModelLevelDetail = dateilListMap[string(_v.EscalateLevel)+","+_v.EscalateModelId] + leveListMap[_v.EscalateModelId] = append(leveListMap[_v.EscalateModelId], _v) + } + //将详情map放入head + + //将详情放入级别map + for _, _v := range data { + _v.EscalateModelLevellst = leveListMap[_v.EscalateModelId] + newData[_v.EscalateModelId] = _v + } + return newData, nil +} + /****************************************************************************** * * @Reference leit.com/LAPP_CHEERSSON_BACKEND/dao/base/EscalateModelHeadDAO.UpdateOne diff --git a/main.go b/main.go index c850a70..db99bee 100644 --- a/main.go +++ b/main.go @@ -120,8 +120,6 @@ func imain() { if err != nil { log.Fatal("init logger config failed, error:", err.Error()) } - - //启动监听端口 app.Run(iris.Addr(":9003"), iris.WithConfiguration(conf.C)) diff --git a/models/base/EscalateMsg.model.go b/models/base/EscalateMsg.model.go index 24c2182..8fb545f 100644 --- a/models/base/EscalateMsg.model.go +++ b/models/base/EscalateMsg.model.go @@ -20,23 +20,24 @@ import ( * ******************************************************************************/ type EscalateMsg struct { - PlantNr int `xorm:"pk int 'PlantNr'" json:"EscalateMsg-PlantNr"` - MsgId string `xorm:"pk nvarchar(40) 'MsgId'" json:"EscalateMsg-MsgId"` - EscalateModelId string `xorm:"nvarchar(40) 'EscalateModelId' not null" json:"EscalateMsg-EscalateModelId"` - EscalateLevel int `xorm:"int 'EscalateLevel' not null" json:"EscalateMsg-EscalateLevel"` - Descr string `xorm:"nvarchar(100) 'Descr' not null" json:"EscalateMsg-Descr"` - Status int `xorm:"int 'Status' not null" json:"EscalateMsg-Status"` - EscalateStatus int `xorm:"int 'EscalateStatus' not null" json:"EscalateMsg-EscalateStatus"` - EscalateTime grmi.DateTime `xorm:"datetime 'EscalateTime'" json:"EscalateMsg-EscalateTime"` - CtrlPara1 int `xorm:"int 'CtrlPara1' not null" json:"EscalateMsg-CtrlPara1"` - CtrlPara2 int `xorm:"int 'CtrlPara2' not null" json:"EscalateMsg-CtrlPara2"` - CtrlStr1 string `xorm:"nvarchar(255) 'CtrlStr1' not null" json:"EscalateMsg-CtrlStr1"` - CtrlStr2 string `xorm:"nvarchar(255) 'CtrlStr2' not null" json:"EscalateMsg-CtrlStr2"` - CtrlTime1 grmi.DateTime `xorm:"datetime 'CtrlTime1'" json:"EscalateMsg-CtrlTime1"` - CtrlTime2 grmi.DateTime `xorm:"datetime 'CtrlTime2'" json:"EscalateMsg-CtrlTime2"` - LastModify grmi.DateTime `xorm:"datetime 'LastModify' not null updated" json:"EscalateMsg-LastModify"` - LastUser string `xorm:"nvarchar(20) 'LastUser' not null" json:"EscalateMsg-LastUser"` - CreateTime grmi.DateTime `xorm:"datetime 'CreateTime' not null created" json:"EscalateMsg-CreateTime"` + PlantNr int `xorm:"pk int 'PlantNr'" json:"EscalateMsg-PlantNr"` + MsgId string `xorm:"pk nvarchar(40) 'MsgId'" json:"EscalateMsg-MsgId"` + EscalateModelId string `xorm:"nvarchar(40) 'EscalateModelId' not null" json:"EscalateMsg-EscalateModelId"` + EscalateLevel int `xorm:"int 'EscalateLevel' not null" json:"EscalateMsg-EscalateLevel"` + Descr string `xorm:"nvarchar(100) 'Descr' not null" json:"EscalateMsg-Descr"` + Status int `xorm:"int 'Status' not null" json:"EscalateMsg-Status"` + EscalateStatus int `xorm:"int 'EscalateStatus' not null" json:"EscalateMsg-EscalateStatus"` + EscalateTime grmi.DateTime `xorm:"datetime 'EscalateTime'" json:"EscalateMsg-EscalateTime"` + CtrlPara1 int `xorm:"int 'CtrlPara1' not null" json:"EscalateMsg-CtrlPara1"` + CtrlPara2 int `xorm:"int 'CtrlPara2' not null" json:"EscalateMsg-CtrlPara2"` + CtrlStr1 string `xorm:"nvarchar(255) 'CtrlStr1' not null" json:"EscalateMsg-CtrlStr1"` + CtrlStr2 string `xorm:"nvarchar(255) 'CtrlStr2' not null" json:"EscalateMsg-CtrlStr2"` + CtrlTime1 grmi.DateTime `xorm:"datetime 'CtrlTime1'" json:"EscalateMsg-CtrlTime1"` + CtrlTime2 grmi.DateTime `xorm:"datetime 'CtrlTime2'" json:"EscalateMsg-CtrlTime2"` + LastModify grmi.DateTime `xorm:"datetime 'LastModify' not null updated" json:"EscalateMsg-LastModify"` + LastUser string `xorm:"nvarchar(20) 'LastUser' not null" json:"EscalateMsg-LastUser"` + CreateTime grmi.DateTime `xorm:"datetime 'CreateTime' not null created" json:"EscalateMsg-CreateTime"` + EscalateMsgDetail []EscalateMsgDetail `xorm:"-" json:"EscalateMsg-EscalateMsgDetail"` } /****************************************************************************** diff --git a/services/base/implments/EscalateModelHead.service.impl.go b/services/base/implments/EscalateModelHead.service.impl.go index bbb64dc..5825e0a 100644 --- a/services/base/implments/EscalateModelHead.service.impl.go +++ b/services/base/implments/EscalateModelHead.service.impl.go @@ -9,6 +9,10 @@ import ( "leit.com/LAPP_CHEERSSON_BACKEND/grmi" meta "leit.com/LAPP_CHEERSSON_BACKEND/meta/base" model "leit.com/LAPP_CHEERSSON_BACKEND/models/base" + "leit.com/LAPP_CHEERSSON_BACKEND/utils" + "log" + "strconv" + "time" ) /****************************************************************************** @@ -42,6 +46,9 @@ var DefaultConditionOfEscalateModelHead = grmi.NewCondition( nil, ) +//创建一个模板缓存type +var headMsgConfig map[string]model.EscalateModelHead + /****************************************************************************** * * @Description : EscalateModelHead的默认分页查询条件 @@ -86,7 +93,31 @@ type EscalateModelHeadServiceImplement struct { * ******************************************************************************/ func NewEscalateModelHeadServiceImplement() *EscalateModelHeadServiceImplement { - return &EscalateModelHeadServiceImplement{} + + escalateModelHeadServiceImplement := &EscalateModelHeadServiceImplement{} + //创建队列 + utils.EmailSendFoudnMap["escalatemodelhead"] = escalateModelHeadServiceImplement + utils.EmailSendStruct["escalatemodelhead"] = utils.NewEmailSendStruct(10, "escalatemodelhead", 10) + headMsgConfig = make(map[string]model.EscalateModelHead) + //将配置放入map中 + engine := db.Eloquent.Master() + session := engine.NewSession() + escalateModelHeadDAO := dal.NewEscalateModelHeadDAO(session, model.PlantNr, "system") + _headMsgConfig, err := escalateModelHeadDAO.SelectAll() + if err != nil { + panic("邮件配置导入失败") + } + headMsgConfig = _headMsgConfig + //将未完成信息写入队列 + escalateMsgDao := dal.NewEscalateMsgDAO(session, model.PlantNr, "system") + urlParameters := make(map[string]string) + urlParameters["Status"] = "2" + predicates, err := DefaultConditionOfEscalateMsg.BuildPredicates(urlParameters) + msgList, err := escalateMsgDao.Select(predicates, nil) + for _, _v := range msgList { + utils.EmailSendStruct["escalatemodelhead"].SeyChannels(utils.ChannelsInfo{Key: _v.MsgId, Info: _v}) + } + return escalateModelHeadServiceImplement } /****************************************************************************** @@ -520,3 +551,120 @@ func (impl *EscalateModelHeadServiceImplement) Update(user *global.User, entitie } return nil } + +/** +处理消息 +*/ +func (impl *EscalateModelHeadServiceImplement) ManageFunction(info utils.ChannelsInfo) (bool, utils.ChannelsInfo) { + data, ok := info.Info.(model.EscalateMsg) + if !ok { + //写入日志 + log.Println("消息体类型错误跳过消息") + return false, utils.ChannelsInfo{} + } + //获取下一次需要发送的用户 + var nextUser model.EscalateModelLevellst + for _, _v := range headMsgConfig[data.EscalateModelId].EscalateModelLevellst { + if _v.EscalateLevel > data.EscalateLevel { + nextUser = _v + break + } + } + if nextUser.EscalateLevel == 0 { + //没有更高级的 无需在发送邮件 + return false, info + } + //判断消息是不是需要再次发送 当前时间小于下次要发送的时间 下次要发送的时间为下一等级间隔时间 + 当前发送时间 + + var sendTime time.Time + switch nextUser.TimeUom { + case "y": + sendTime = data.EscalateTime.Restore().AddDate(nextUser.EscalateLeadTime, 0, 0) + break + case "m": + sendTime = data.EscalateTime.Restore().AddDate(0, nextUser.EscalateLeadTime, 0) + break + case "d": + sendTime = data.EscalateTime.Restore().AddDate(0, 0, nextUser.EscalateLeadTime) + break + case "h": + sendTime = data.EscalateTime.Restore().Add(time.Duration(nextUser.EscalateLeadTime) * 1 * time.Hour) + break + case "i": + sendTime = data.EscalateTime.Restore().Add(time.Duration(nextUser.EscalateLeadTime) * 1 * time.Minute) + break + case "s": + sendTime = data.EscalateTime.Restore().Add(time.Duration(nextUser.EscalateLeadTime) * 1 * time.Second) + break + default: + log.Println("错误的时间定义") + return false, info + } + if time.Now().Before(sendTime) { + //当前时间在下次发送时间之后 无需发送等待处理 + return true, info + } + //需要对下一级发送邮件 + data.EscalateLevel = nextUser.EscalateLevel + data.EscalateTime = grmi.DateTime(time.Now()) + //将修改提交到数据库 + engine := db.Eloquent.Master() + session := engine.NewSession() + defer session.Close() + err := session.Begin() + escalateMsgDao := dal.NewEscalateMsgDAO(session, model.PlantNr, "system") + err = escalateMsgDao.UpdateOne(&data) + if err != nil { + session.Rollback() + //写入日志 + log.Println("修改数据库错误跳过信息") + return false, utils.ChannelsInfo{} + } + //写入发送消息明细表 + var detailData []model.EscalateMsgDetail + userDao := dal.NewUserDAO(session, model.PlantNr, "system") + for _, _v := range nextUser.EscalateModelLevelDetail { + detailData = append(detailData, model.EscalateMsgDetail{ + PlantNr: model.PlantNr, + MsgId: data.MsgId, + Pos: _v.Pos, + InformUserId: _v.InformUserId, + InformType: _v.InformType, + MsgHead: _v.MsgHead, + MsgBody: _v.MsgBody, + MsgSignature: _v.MsgSignature, + Status: 2, + EscalateTime: grmi.DateTime(time.Now()), + ReplyTime: grmi.DateTime(time.Now()), + }) + userInfo, _ := userDao.SelectOne(_v.InformUserId) + address := headMsgConfig[data.EscalateModelId].EMailServerHost + ":" + strconv.FormatInt(int64(headMsgConfig[data.EscalateModelId].EMailServerPort), 10) + //发送邮件 + err = utils.SendMail( + headMsgConfig[data.EscalateModelId].FromEmail, + headMsgConfig[data.EscalateModelId].EmailPwd, + address, + userInfo.Email, + "system", + _v.MsgHead, + _v.MsgBody, + "", + ) + } + escalateMsgDetailDao := dal.NewEscalateMsgDetailDAO(session, model.PlantNr, "system") + err = escalateMsgDetailDao.Insert(&detailData) + if err != nil { + session.Rollback() + log.Println("明细表添加错误 停止发送") + return false, info + } + + err = session.Commit() + if err != nil { + log.Println("事务提交失败 停止发送") + return false, info + } + info.Info = data + + return true, info +} diff --git a/services/base/implments/EscalateMsg.service.impl.go b/services/base/implments/EscalateMsg.service.impl.go index 215a15a..ca7b02a 100644 --- a/services/base/implments/EscalateMsg.service.impl.go +++ b/services/base/implments/EscalateMsg.service.impl.go @@ -160,6 +160,15 @@ func (impl *EscalateMsgServiceImplement) SelectOne(user *global.User, msgId stri if err != nil { return nil, err } + //查询对应详细 + detailDao := dal.NewEscalateMsgDetailDAO(session, user.PlantNr, user.UserId) + dataList, err := detailDao.Select([]grmi.Predicate{ + meta.EscalateMsgDetail_MsgId.NewPredicate(grmi.Equal, result.MsgId), + }, nil) + if err != nil { + return nil, err + } + result.EscalateMsgDetail = dataList return result, nil } diff --git a/utils/email.go b/utils/email.go new file mode 100644 index 0000000..24794f6 --- /dev/null +++ b/utils/email.go @@ -0,0 +1,193 @@ +package utils + +import ( + "leit.com/LAPP_CHEERSSON_BACKEND/utils/mysmtp" + "log" + "reflect" + "strings" + "sync" + "time" +) + +var EmailSendStruct = make(map[string]*emailSendStruct) +var EmailSendFoudnMap = make(map[string]interface{}) + +type emailSendStruct struct { + channels chan ChannelsInfo + channelCount int + successMap map[string]int //完成状态 如果map中包含消息下角标表明消息已完成发送 + successMapMutex sync.Mutex + manageCount int //需要开启消费者的数量 + manageStartCount int //已经开启消费者的数量 + manageMaxCount int //最大开启数量 + manageAverage int //开启,关闭一条信道基数 + serviceName string //使用业务层 +} + +func NewEmailSendStruct(maxCount int, serviceName string, manageAverage int) *emailSendStruct { + model := &emailSendStruct{} + model.channels = make(chan ChannelsInfo) + model.channelCount = 0 + model.manageCount = 1 + model.manageStartCount = 1 + model.manageMaxCount = maxCount + model.successMap = make(map[string]int) + model.successMapMutex = sync.Mutex{} + model.serviceName = serviceName + model.manageAverage = manageAverage + //开启主信道消费 + go model.startSend() + //开启动态信道消费 + go model.dynamic() + return model +} + +/*** +返回消息数量 +*/ +func (_this *emailSendStruct) GetChannelCount() int { + return _this.channelCount +} + +/*** +第一次写入信道 +*/ +func (_this *emailSendStruct) SeyChannels(info ChannelsInfo) { + _this.channels <- info + _this.channelCount++ + if _this.channelCount > (_this.manageStartCount+1)*_this.manageAverage && _this.manageCount < _this.manageMaxCount { + //需要开启一条新通道 + _this.manageCount++ + } + if _this.channelCount < (_this.manageStartCount-1)*_this.manageAverage { + _this.manageCount-- + } +} + +/*** +将数据重新放入信道 +*/ +func (_this *emailSendStruct) seyChannels(info ChannelsInfo) { + _this.channels <- info +} + +//完成状态 如果map中包含消息下角标表明消息已完成发送 +func (_this *emailSendStruct) SetSuccessMap(key string) { + _this.successMapMutex.Lock() + defer _this.successMapMutex.Unlock() + _this.successMap[key] = 1 +} + +//如果读取到完成信息直接销毁掉map +func (_this *emailSendStruct) getSuccessMap(key string) bool { + _this.successMapMutex.Lock() + defer _this.successMapMutex.Unlock() + _, ok := _this.successMap[key] + if ok { + delete(_this.successMap, key) + _this.channelCount-- + } + return ok +} + +//开始处理消息 +func (_this *emailSendStruct) startSend() { + for { + info := <-_this.channels + _this.manageFunction(info) + } + +} + +//动态信道 +func (_this *emailSendStruct) dynamic() { + for { + if _this.manageCount > _this.manageStartCount { + //需要开启一条信道处理信息 + _this.manageStartCount++ + go func(_i int) { + log.Println("动态队列开启:", _i) + for { + if _this.manageCount < _this.manageStartCount && _this.manageCount < _i { + log.Println("动态队列关闭", _i) + break + } + info := <-_this.channels + _this.manageFunction(info) + + } + _this.manageStartCount-- + }(_this.manageStartCount) + } + time.Sleep(2 * time.Second) + } +} + +/** +调用处理方法 +*/ +func (_this *emailSendStruct) manageFunction(info ChannelsInfo) { + if _this.getSuccessMap(info.Key) { + //信息处理完成下一条消息 + return + } + refUser := ReflectApi(_this.serviceName, EmailSendFoudnMap) + setNameMethod := refUser.MethodByName("ManageFunction") + args := []reflect.Value{ + reflect.ValueOf(info), + } //构造一个类型为reflect.Value的切片 + isErr, reflectValue := ReflectApiCall(&setNameMethod, args) + if isErr || (!isErr && reflectValue == nil) { + panic("服务注册异常") + } + var ok bool + var isRepetition bool + for key, value := range reflectValue { + switch key { + case 0: + isRepetition, ok = value.Interface().(bool) + break + case 1: + info, ok = value.Interface().(ChannelsInfo) + break + } + if !ok { + panic("服务返回参数注册异常") + } + } + if isRepetition { + //等待5S后放回信道中 + _this.seyChannels(info) + } + return +} + +type ChannelsInfo struct { + Key string + Info interface{} +} + +/*! +username 发送者邮件 +password 授权码 +host 主机地址 smtp.qq.com:587 或 smtp.qq.com:25 +to 接收邮箱 多个接收邮箱使用 ; 隔开 +name 发送人名称 +subject 发送主题 +body 发送内容 +mailType 发送邮件内容类型 +*/ +func SendMail(username, password, host, to, name, subject, body, mailType string) error { + hp := strings.Split(host, ":") + auth := mysmtp.LoginAuth(username, password, hp[0]) + var contentType string + if mailType == "html" { + contentType = "Content-Type: text/" + mailType + "; charset=UTF-8" + } else { + contentType = "Content-Type: text/plain" + "; charset=UTF-8" + } + msg := []byte("To: " + to + "\r\nFrom: " + name + "<" + username + ">\r\nSubject: " + subject + "\r\n" + contentType + "\r\n\r\n" + body) + sendTo := strings.Split(to, ";") + err := mysmtp.SendMail(host, auth, username, sendTo, msg) + return err +} diff --git a/utils/mysmtp/auth.go b/utils/mysmtp/auth.go new file mode 100644 index 0000000..aaf5c90 --- /dev/null +++ b/utils/mysmtp/auth.go @@ -0,0 +1,110 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mysmtp + +import ( + "crypto/hmac" + "crypto/md5" + "errors" + "fmt" +) + +// Auth is implemented by an SMTP authentication mechanism. +type Auth interface { + // Start begins an authentication with a server. + // It returns the name of the authentication protocol + // and optionally data to include in the initial AUTH message + // sent to the server. It can return proto == "" to indicate + // that the authentication should be skipped. + // If it returns a non-nil error, the SMTP client aborts + // the authentication attempt and closes the connection. + Start(server *ServerInfo) (proto string, toServer []byte, err error) + + // Next continues the authentication. The server has just sent + // the fromServer data. If more is true, the server expects a + // response, which Next should return as toServer; otherwise + // Next should return toServer == nil. + // If Next returns a non-nil error, the SMTP client aborts + // the authentication attempt and closes the connection. + Next(fromServer []byte, more bool) (toServer []byte, err error) +} + +// ServerInfo records information about an SMTP server. +type ServerInfo struct { + Name string // SMTP server name + TLS bool // using TLS, with valid certificate for Name + Auth []string // advertised authentication mechanisms +} + +type plainAuth struct { + identity, username, password string + host string +} + +// PlainAuth returns an Auth that implements the PLAIN authentication +// mechanism as defined in RFC 4616. The returned Auth uses the given +// username and password to authenticate to host and act as identity. +// Usually identity should be the empty string, to act as username. +// +// PlainAuth will only send the credentials if the connection is using TLS +// or is connected to localhost. Otherwise authentication will fail with an +// error, without sending the credentials. +func PlainAuth(identity, username, password, host string) Auth { + return &plainAuth{identity, username, password, host} +} + +func isLocalhost(name string) bool { + return name == "localhost" || name == "127.0.0.1" || name == "::1" +} + +func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) { + // Must have TLS, or else localhost server. + // Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo. + // In particular, it doesn't matter if the server advertises PLAIN auth. + // That might just be the attacker saying + // "it's ok, you can trust me with your password." + if !server.TLS && !isLocalhost(server.Name) { + return "", nil, errors.New("unencrypted connection") + } + if server.Name != a.host { + return "", nil, errors.New("wrong host name") + } + resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password) + return "PLAIN", resp, nil +} + +func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + // We've already sent everything. + return nil, errors.New("unexpected server challenge") + } + return nil, nil +} + +type cramMD5Auth struct { + username, secret string +} + +// CRAMMD5Auth returns an Auth that implements the CRAM-MD5 authentication +// mechanism as defined in RFC 2195. +// The returned Auth uses the given username and secret to authenticate +// to the server using the challenge-response mechanism. +func CRAMMD5Auth(username, secret string) Auth { + return &cramMD5Auth{username, secret} +} + +func (a *cramMD5Auth) Start(server *ServerInfo) (string, []byte, error) { + return "CRAM-MD5", nil, nil +} + +func (a *cramMD5Auth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + d := hmac.New(md5.New, []byte(a.secret)) + d.Write(fromServer) + s := make([]byte, 0, d.Size()) + return []byte(fmt.Sprintf("%s %x", a.username, d.Sum(s))), nil + } + return nil, nil +} diff --git a/utils/mysmtp/mysmtp.go b/utils/mysmtp/mysmtp.go new file mode 100644 index 0000000..64f9347 --- /dev/null +++ b/utils/mysmtp/mysmtp.go @@ -0,0 +1,47 @@ +package mysmtp + +import "errors" + +/* + auth login +*/ +type loginAuth struct { + username, password string + host string +} + +/* + auth login 验证 +*/ +func LoginAuth(username, password, host string) Auth { + return &loginAuth{username, password, host} +} + +/* + 初步验证服务器信息,输入账号 +*/ +func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) { + // 如果不是安全连接,也不是本地的服务器,报错,不允许不安全的连接 + if !server.TLS && !isLocalhost(server.Name) { + return "", nil, errors.New("unencrypted connection") + } + // 如果服务器信息和 Auth 对象的服务器信息不一致,报错 + if server.Name != a.host { + return "", nil, errors.New("wrong host name") + } + // 验证时需要的账号 + resp := []byte(a.username) + // "auth login" 命令 + return "LOGIN", resp, nil +} + +/* + 进一步进行验证,输入密码 +*/ +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + // 如果服务器需要更多验证,报错 + if more { + return []byte(a.password), nil + } + return nil, nil +} diff --git a/utils/mysmtp/smtp.go b/utils/mysmtp/smtp.go new file mode 100644 index 0000000..6b3e054 --- /dev/null +++ b/utils/mysmtp/smtp.go @@ -0,0 +1,423 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321. +// It also implements the following extensions: +// 8BITMIME RFC 1652 +// AUTH RFC 2554 +// STARTTLS RFC 3207 +// Additional extensions may be handled by clients. +// +// The smtp package is frozen and is not accepting new features. +// Some external packages provide more functionality. See: +// +// https://godoc.org/?q=smtp +package mysmtp + +import ( + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/textproto" + "strings" +) + +// A Client represents a client connection to an SMTP server. +type Client struct { + // Text is the textproto.Conn used by the Client. It is exported to allow for + // clients to add extensions. + Text *textproto.Conn + // keep a reference to the connection so it can be used to create a TLS + // connection later + conn net.Conn + // whether the Client is using TLS + tls bool + serverName string + // map of supported extensions + ext map[string]string + // supported auth mechanisms + auth []string + localName string // the name to use in HELO/EHLO + didHello bool // whether we've said HELO/EHLO + helloError error // the error from the hello +} + +// Dial returns a new Client connected to an SMTP server at addr. +// The addr must include a port, as in "mail.example.com:smtp". +func Dial(addr string) (*Client, error) { + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + host, _, _ := net.SplitHostPort(addr) + return NewClient(conn, host) +} + +// NewClient returns a new Client using an existing connection and host as a +// server name to be used when authenticating. +func NewClient(conn net.Conn, host string) (*Client, error) { + text := textproto.NewConn(conn) + _, _, err := text.ReadResponse(220) + if err != nil { + text.Close() + return nil, err + } + c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"} + _, c.tls = conn.(*tls.Conn) + return c, nil +} + +// Close closes the connection. +func (c *Client) Close() error { + return c.Text.Close() +} + +// hello runs a hello exchange if needed. +func (c *Client) hello() error { + if !c.didHello { + c.didHello = true + err := c.ehlo() + if err != nil { + c.helloError = c.helo() + } + } + return c.helloError +} + +// Hello sends a HELO or EHLO to the server as the given host name. +// Calling this method is only necessary if the client needs control +// over the host name used. The client will introduce itself as "localhost" +// automatically otherwise. If Hello is called, it must be called before +// any of the other methods. +func (c *Client) Hello(localName string) error { + if err := validateLine(localName); err != nil { + return err + } + if c.didHello { + return errors.New("smtp: Hello called after other methods") + } + c.localName = localName + return c.hello() +} + +// cmd is a convenience function that sends a command and returns the response +func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) { + id, err := c.Text.Cmd(format, args...) + if err != nil { + return 0, "", err + } + c.Text.StartResponse(id) + defer c.Text.EndResponse(id) + code, msg, err := c.Text.ReadResponse(expectCode) + return code, msg, err +} + +// helo sends the HELO greeting to the server. It should be used only when the +// server does not support ehlo. +func (c *Client) helo() error { + c.ext = nil + _, _, err := c.cmd(250, "HELO %s", c.localName) + return err +} + +// ehlo sends the EHLO (extended hello) greeting to the server. It +// should be the preferred greeting for servers that support it. +func (c *Client) ehlo() error { + _, msg, err := c.cmd(250, "EHLO %s", c.localName) + if err != nil { + return err + } + ext := make(map[string]string) + extList := strings.Split(msg, "\n") + if len(extList) > 1 { + extList = extList[1:] + for _, line := range extList { + args := strings.SplitN(line, " ", 2) + if len(args) > 1 { + ext[args[0]] = args[1] + } else { + ext[args[0]] = "" + } + } + } + if mechs, ok := ext["AUTH"]; ok { + c.auth = strings.Split(mechs, " ") + } + c.ext = ext + return err +} + +// StartTLS sends the STARTTLS command and encrypts all further communication. +// Only servers that advertise the STARTTLS extension support this function. +func (c *Client) StartTLS(config *tls.Config) error { + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(220, "STARTTLS") + if err != nil { + return err + } + c.conn = tls.Client(c.conn, config) + c.Text = textproto.NewConn(c.conn) + c.tls = true + return c.ehlo() +} + +// TLSConnectionState returns the client's TLS connection state. +// The return values are their zero values if StartTLS did +// not succeed. +func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) { + tc, ok := c.conn.(*tls.Conn) + if !ok { + return + } + return tc.ConnectionState(), true +} + +// Verify checks the validity of an email address on the server. +// If Verify returns nil, the address is valid. A non-nil return +// does not necessarily indicate an invalid address. Many servers +// will not verify addresses for security reasons. +func (c *Client) Verify(addr string) error { + if err := validateLine(addr); err != nil { + return err + } + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(250, "VRFY %s", addr) + return err +} + +// Auth authenticates a client using the provided authentication mechanism. +// A failed authentication closes the connection. +// Only servers that advertise the AUTH extension support this function. +func (c *Client) Auth(a Auth) error { + if err := c.hello(); err != nil { + return err + } + encoding := base64.StdEncoding + mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth}) + if err != nil { + c.Quit() + return err + } + resp64 := make([]byte, encoding.EncodedLen(len(resp))) + encoding.Encode(resp64, resp) + code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64))) + for err == nil { + var msg []byte + switch code { + case 334: + msg, err = encoding.DecodeString(msg64) + case 235: + // the last message isn't base64 because it isn't a challenge + msg = []byte(msg64) + default: + err = &textproto.Error{Code: code, Msg: msg64} + } + if err == nil { + resp, err = a.Next(msg, code == 334) + } + if err != nil { + // abort the AUTH + c.cmd(501, "*") + c.Quit() + break + } + if resp == nil { + break + } + resp64 = make([]byte, encoding.EncodedLen(len(resp))) + encoding.Encode(resp64, resp) + code, msg64, err = c.cmd(0, string(resp64)) + } + return err +} + +// Mail issues a MAIL command to the server using the provided email address. +// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME +// parameter. +// This initiates a mail transaction and is followed by one or more Rcpt calls. +func (c *Client) Mail(from string) error { + if err := validateLine(from); err != nil { + return err + } + if err := c.hello(); err != nil { + return err + } + cmdStr := "MAIL FROM:<%s>" + if c.ext != nil { + if _, ok := c.ext["8BITMIME"]; ok { + cmdStr += " BODY=8BITMIME" + } + } + _, _, err := c.cmd(250, cmdStr, from) + return err +} + +// Rcpt issues a RCPT command to the server using the provided email address. +// A call to Rcpt must be preceded by a call to Mail and may be followed by +// a Data call or another Rcpt call. +func (c *Client) Rcpt(to string) error { + if err := validateLine(to); err != nil { + return err + } + _, _, err := c.cmd(25, "RCPT TO:<%s>", to) + return err +} + +type dataCloser struct { + c *Client + io.WriteCloser +} + +func (d *dataCloser) Close() error { + d.WriteCloser.Close() + _, _, err := d.c.Text.ReadResponse(250) + return err +} + +// Data issues a DATA command to the server and returns a writer that +// can be used to write the mail headers and body. The caller should +// close the writer before calling any more methods on c. A call to +// Data must be preceded by one or more calls to Rcpt. +func (c *Client) Data() (io.WriteCloser, error) { + _, _, err := c.cmd(354, "DATA") + if err != nil { + return nil, err + } + return &dataCloser{c, c.Text.DotWriter()}, nil +} + +var testHookStartTLS func(*tls.Config) // nil, except for tests + +// SendMail connects to the server at addr, switches to TLS if +// possible, authenticates with the optional mechanism a if possible, +// and then sends an email from address from, to addresses to, with +// message msg. +// The addr must include a port, as in "mail.example.com:smtp". +// +// The addresses in the to parameter are the SMTP RCPT addresses. +// +// The msg parameter should be an RFC 822-style email with headers +// first, a blank line, and then the message body. The lines of msg +// should be CRLF terminated. The msg headers should usually include +// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc" +// messages is accomplished by including an email address in the to +// parameter but not including it in the msg headers. +// +// The SendMail function and the net/smtp package are low-level +// mechanisms and provide no support for DKIM signing, MIME +// attachments (see the mime/multipart package), or other mail +// functionality. Higher-level packages exist outside of the standard +// library. +func SendMail(addr string, a Auth, from string, to []string, msg []byte) error { + if err := validateLine(from); err != nil { + return err + } + for _, recp := range to { + if err := validateLine(recp); err != nil { + return err + } + } + c, err := Dial(addr) + if err != nil { + return err + } + defer c.Close() + if err = c.hello(); err != nil { + return err + } + if ok, _ := c.Extension("STARTTLS"); ok { + // 跳过证书验证 + config := &tls.Config{ServerName: c.serverName, InsecureSkipVerify: true} + if testHookStartTLS != nil { + testHookStartTLS(config) + } + if err = c.StartTLS(config); err != nil { + return err + } + } + if err = c.Mail(from); err != nil { + return err + } + for _, addr := range to { + if err = c.Rcpt(addr); err != nil { + return err + } + } + w, err := c.Data() + if err != nil { + return err + } + _, err = w.Write(msg) + if err != nil { + return err + } + err = w.Close() + if err != nil { + return err + } + return c.Quit() +} + +// Extension reports whether an extension is support by the server. +// The extension name is case-insensitive. If the extension is supported, +// Extension also returns a string that contains any parameters the +// server specifies for the extension. +func (c *Client) Extension(ext string) (bool, string) { + if err := c.hello(); err != nil { + return false, "" + } + if c.ext == nil { + return false, "" + } + ext = strings.ToUpper(ext) + param, ok := c.ext[ext] + return ok, param +} + +// Reset sends the RSET command to the server, aborting the current mail +// transaction. +func (c *Client) Reset() error { + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(250, "RSET") + return err +} + +// Noop sends the NOOP command to the server. It does nothing but check +// that the connection to the server is okay. +func (c *Client) Noop() error { + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(250, "NOOP") + return err +} + +// Quit sends the QUIT command and closes the connection to the server. +func (c *Client) Quit() error { + if err := c.hello(); err != nil { + return err + } + _, _, err := c.cmd(221, "QUIT") + if err != nil { + return err + } + return c.Text.Close() +} + +// validateLine checks to see if a line has CR or LF as per RFC 5321 +func validateLine(line string) error { + if strings.ContainsAny(line, "\n\r") { + return errors.New("smtp: A line must not contain CR or LF") + } + return nil +} diff --git a/utils/reflect.go b/utils/reflect.go new file mode 100644 index 0000000..c66a94b --- /dev/null +++ b/utils/reflect.go @@ -0,0 +1,34 @@ +package utils + +import ( + "reflect" +) + +/*** +从map中获取反射对象 +*/ +func ReflectApi(index string, _map map[string]interface{}) *reflect.Value { + data := _map[index] + if data == nil { + return nil + } + refUser := reflect.ValueOf(data) + return &refUser +} + +/*** +调用反射方法 当反射对象为空 或者不存在反射方法 返回true 成功返回false +*/ +func ReflectApiCall(class *reflect.Value, value []reflect.Value) (bool, []reflect.Value) { + defer func() { + if err := recover(); err != nil { + return + } + }() + + if class == nil { + return true, nil + } + data := class.Call(value) + return false, data +}