mirror of https://github.com/ouqiang/gocron
增加邮件通知
parent
9a7b53c031
commit
a7977894d6
|
@ -3,7 +3,7 @@
|
|||
|
||||
## 功能特性
|
||||
* 定时任务统一调度和管理
|
||||
* 支持对任务CURD
|
||||
* 支持任务CURD
|
||||
* crontab时间表达式,支持秒级任务定义
|
||||
* 任务执行失败重试设置
|
||||
* 任务超时设置
|
||||
|
|
|
@ -29,10 +29,7 @@ func (migration *Migration) Exec(dbName string) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
err := setting.InitBasicField()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setting.InitBasicField()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,44 +1,174 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
Id int16 `xorm:"smallint pk autoincr"`
|
||||
Id int `xorm:"int pk autoincr"`
|
||||
Code string `xorm:"varchar(32) notnull"`
|
||||
Key string `xorm:"varchar(64) notnull"`
|
||||
Value string `xorm:"varchar(4096) notnull default '' "`
|
||||
}
|
||||
|
||||
const SlackCode = "slack"
|
||||
const SlackKey = "url"
|
||||
const SlackUrlKey = "url"
|
||||
const SlackChannelKey = "channel"
|
||||
|
||||
const MailCode = "mail"
|
||||
const MailServerKey = "server"
|
||||
const MailUserKey = "user"
|
||||
|
||||
// 初始化基本字段 邮件、slack等
|
||||
func (setting *Setting) InitBasicField() (error) {
|
||||
setting.Code = "slack";
|
||||
setting.Key = "url"
|
||||
setting.Value = ""
|
||||
_, err := Db.Insert(setting)
|
||||
func (setting *Setting) InitBasicField() {
|
||||
setting.Code = SlackCode;
|
||||
setting.Key = SlackUrlKey
|
||||
Db.Insert(setting)
|
||||
|
||||
return err
|
||||
setting.Id = 0
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailServerKey
|
||||
Db.Insert(setting)
|
||||
}
|
||||
|
||||
func (setting *Setting) SlackUrl() (string, error) {
|
||||
setting.slackCondition()
|
||||
_, err := Db.Get(setting)
|
||||
// region slack配置
|
||||
|
||||
return setting.Value, err
|
||||
type Slack struct {
|
||||
Url string
|
||||
Channels []Channel
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
Id int
|
||||
Name string
|
||||
}
|
||||
|
||||
func (setting *Setting) Slack() (Slack, error) {
|
||||
list := make([]Setting, 0)
|
||||
err := Db.Where("code = ?", SlackCode).Find(&list)
|
||||
slack := Slack{Url:"", Channels:make([]Channel, 0)}
|
||||
if err != nil {
|
||||
return slack, err
|
||||
}
|
||||
|
||||
setting.formatSlack(list, &slack)
|
||||
|
||||
return slack, err
|
||||
}
|
||||
|
||||
func (setting *Setting) formatSlack(list []Setting, slack *Slack) {
|
||||
for _, v := range list {
|
||||
if v.Key == SlackUrlKey {
|
||||
slack.Url = v.Value
|
||||
continue
|
||||
}
|
||||
|
||||
slack.Channels = append(slack.Channels, Channel{
|
||||
v.Id, v.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 更新slack webhook url
|
||||
func (setting *Setting) UpdateSlackUrl(url string) (int64, error) {
|
||||
setting.slackCondition()
|
||||
setting.Value = url
|
||||
|
||||
return setting.UpdateBean()
|
||||
return Db.Cols("value").Update(setting, Setting{Code:SlackCode, Key:SlackUrlKey})
|
||||
}
|
||||
|
||||
func (setting *Setting) slackCondition() {
|
||||
|
||||
// 创建slack渠道
|
||||
func (setting *Setting) CreateChannel(channel string) (int64, error) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackKey
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
|
||||
return Db.Insert(setting)
|
||||
}
|
||||
|
||||
func (setting *Setting) UpdateBean() (int64, error) {
|
||||
return Db.Cols("code,key,value").Update(setting)
|
||||
}
|
||||
func (setting *Setting) IsChannelExist(channel string) (bool) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Value = channel
|
||||
|
||||
count, _ := Db.Count(setting)
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// 删除slack渠道
|
||||
func (setting *Setting) RemoveChannel(id int) (int64, error) {
|
||||
setting.Code = SlackCode
|
||||
setting.Key = SlackChannelKey
|
||||
setting.Id = id
|
||||
return Db.Delete(setting)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
type Mail struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
MailUsers []MailUser
|
||||
}
|
||||
|
||||
type MailUser struct {
|
||||
Id int
|
||||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
// region 邮件配置
|
||||
func (setting *Setting) Mail() (Mail, error) {
|
||||
list := make([]Setting, 0)
|
||||
err := Db.Where("code = ?", MailCode).Find(&list)
|
||||
mail := Mail{MailUsers:make([]MailUser, 0)}
|
||||
if err != nil {
|
||||
return mail, err
|
||||
}
|
||||
|
||||
setting.formatMail(list, &mail)
|
||||
|
||||
return mail, err
|
||||
}
|
||||
|
||||
func (setting *Setting) formatMail(list []Setting, mail *Mail) {
|
||||
mailUser := MailUser{}
|
||||
for _, v := range list {
|
||||
if v.Key == MailServerKey {
|
||||
json.Unmarshal([]byte(v.Value), mail)
|
||||
continue
|
||||
}
|
||||
json.Unmarshal([]byte(v.Value), &mailUser)
|
||||
mailUser.Id = v.Id
|
||||
mail.MailUsers = append(mail.MailUsers, mailUser)
|
||||
}
|
||||
}
|
||||
|
||||
func (setting *Setting) UpdateMailServer(config string) (int64, error) {
|
||||
setting.Value = config
|
||||
return Db.Cols("value").Update(setting, Setting{Code:MailCode, Key:MailServerKey})
|
||||
}
|
||||
|
||||
func (setting *Setting) CreateMailUser(username, email string) (int64, error) {
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailUserKey
|
||||
mailUser := MailUser{0, username, email}
|
||||
jsonByte, err := json.Marshal(mailUser)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
setting.Value = string(jsonByte)
|
||||
|
||||
return Db.Insert(setting)
|
||||
}
|
||||
|
||||
func (setting *Setting) RemoveMailUser(id int) (int64, error) {
|
||||
setting.Code = MailCode
|
||||
setting.Key = MailUserKey
|
||||
setting.Id = id
|
||||
return Db.Delete(setting)
|
||||
}
|
||||
// endregion
|
|
@ -25,6 +25,9 @@ type Task struct {
|
|||
Multi int8 `xorm:"tinyint notnull default 1"` // 是否允许多实例运行
|
||||
RetryTimes int8 `xorm:"tinyint notnull default 0"` // 重试次数
|
||||
HostId int16 `xorm:"smallint notnull default 0"` // SSH host id,
|
||||
NotifyStatus int8 `xorm:"smallint notnull default 1"` // 任务执行结束是否通知 0: 不通知 1: 失败通知 2: 执行结束通知
|
||||
NotifyType int8 `xorm:"smallint notnull default 0"` // 通知类型 1: 邮件 2: slack
|
||||
NotifyReceiverId string `xorm:"varchar(256) notnull default '' "` // 通知接受者ID, setting表主键ID,多个ID逗号分隔
|
||||
Remark string `xorm:"varchar(100) notnull default ''"` // 备注
|
||||
Created time.Time `xorm:"datetime notnull created"` // 创建时间
|
||||
Deleted time.Time `xorm:"datetime deleted"` // 删除时间
|
||||
|
@ -58,7 +61,7 @@ func (task *Task) Create() (insertId int, err error) {
|
|||
}
|
||||
|
||||
func (task *Task) UpdateBean(id int) (int64, error) {
|
||||
return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,status").Update(task)
|
||||
return Db.ID(id).Cols("name,spec,protocol,command,timeout,multi,retry_times,host_id,remark,status,notify_status,notify_type,notify_receiver_id").Update(task)
|
||||
}
|
||||
|
||||
// 更新
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/ouqiang/gocron/modules/setting"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"runtime"
|
||||
"github.com/ouqiang/gocron/modules/notify"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -37,8 +36,6 @@ func InitEnv() {
|
|||
if Installed {
|
||||
InitDb()
|
||||
InitResource()
|
||||
settingModel := new(models.Setting)
|
||||
notify.SlackUrl, _ = settingModel.SlackUrl()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package notify
|
||||
|
||||
import (
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"time"
|
||||
"github.com/go-gomail/gomail"
|
||||
)
|
||||
|
||||
// @author qiang.ou<qingqianludao@gmail.com>
|
||||
// @date 2017/5/1-00:19
|
||||
|
||||
|
||||
type Mail struct {
|
||||
|
||||
}
|
||||
|
||||
func (mail *Mail) Send(msg Message) {
|
||||
model := new(models.Setting)
|
||||
mailSetting, err := model.Mail()
|
||||
logger.Debugf("%+v", mailSetting)
|
||||
if err != nil {
|
||||
logger.Error("#mail#从数据库获取mail配置失败", err)
|
||||
return
|
||||
}
|
||||
if mailSetting.Host == "" {
|
||||
logger.Error("#mail#Host为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.Port == 0 {
|
||||
logger.Error("#mail#Port为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.User == "" {
|
||||
logger.Error("#mail#User为空")
|
||||
return
|
||||
}
|
||||
if mailSetting.Password == "" {
|
||||
logger.Error("#mail#Password为空")
|
||||
return
|
||||
}
|
||||
toUsers := mail.getActiveMailUsers(mailSetting, msg)
|
||||
mail.send(mailSetting, toUsers, msg)
|
||||
}
|
||||
|
||||
func (mail *Mail) send(mailSetting models.Mail, toUsers []string, msg Message) {
|
||||
body := msg["content"].(string)
|
||||
body = strings.Replace(body, "\n", "<br>", -1)
|
||||
gomailMessage := gomail.NewMessage()
|
||||
gomailMessage.SetHeader("From", mailSetting.User)
|
||||
gomailMessage.SetHeader("To", toUsers...)
|
||||
gomailMessage.SetHeader("Subject", "gocron-定时任务监控通知")
|
||||
gomailMessage.SetBody("text/html", body)
|
||||
mailer := gomail.NewPlainDialer(mailSetting.Host, mailSetting.Port,
|
||||
mailSetting.User, mailSetting.Password)
|
||||
maxTimes := 3
|
||||
i := 0
|
||||
for i < maxTimes {
|
||||
err := mailer.DialAndSend(gomailMessage)
|
||||
if err == nil {
|
||||
break;
|
||||
}
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Error("mail#发送消息失败#%s#消息内容-%s", err.Error(), msg["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mail *Mail) getActiveMailUsers(mailSetting models.Mail, msg Message) []string {
|
||||
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
|
||||
users := []string{}
|
||||
for _, v := range(mailSetting.MailUsers) {
|
||||
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
|
||||
users = append(users, v.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
|
@ -2,10 +2,10 @@ package notify
|
|||
|
||||
import (
|
||||
"time"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var SlackUrl string
|
||||
|
||||
type Message map[string]interface{}
|
||||
|
||||
type Notifiable interface {
|
||||
|
@ -24,10 +24,29 @@ func Push(msg Message) {
|
|||
}
|
||||
|
||||
func run() {
|
||||
slack := new(Slack)
|
||||
for msg := range queue {
|
||||
// 根据任务配置发送通知
|
||||
go slack.Send(msg)
|
||||
taskType, taskTypeOk := msg["task_type"]
|
||||
_, taskReceiverIdOk := msg["task_receiver_id"]
|
||||
_, nameOk := msg["name"]
|
||||
_, outputOk := msg["output"]
|
||||
_, statusOk := msg["status"]
|
||||
if !taskTypeOk || !taskReceiverIdOk || !nameOk || !outputOk || !statusOk {
|
||||
logger.Errorf("#notify#参数不完整#%+v", msg)
|
||||
continue
|
||||
}
|
||||
msg["content"] = fmt.Sprintf("============\n============\n============\n任务名称: %s\n状态: %s\n输出:\n %s\n", msg["name"], msg["status"], msg["output"])
|
||||
logger.Debugf("%+v", msg)
|
||||
switch(taskType.(int8)) {
|
||||
case 1:
|
||||
// 邮件
|
||||
mail := Mail{}
|
||||
go mail.Send(msg)
|
||||
case 2:
|
||||
// Slack
|
||||
slack := Slack{}
|
||||
go slack.Send(msg)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
|
@ -6,45 +6,73 @@ import (
|
|||
"github.com/ouqiang/gocron/modules/httpclient"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Slack struct {}
|
||||
|
||||
func (slack *Slack) Send(msg Message) {
|
||||
name, nameOk := msg["name"]
|
||||
statusName, statusOk := msg["status"]
|
||||
content, contentOk := msg["output"]
|
||||
if SlackUrl == "" {
|
||||
logger.Error("slack#webhooks-url为空")
|
||||
return;
|
||||
}
|
||||
if !nameOk || !statusOk || !contentOk {
|
||||
logger.Error("slack#消息字段不存在")
|
||||
model := new(models.Setting)
|
||||
slackSetting, err := model.Slack()
|
||||
if err != nil {
|
||||
logger.Error("#slack#从数据库获取slack配置失败", err)
|
||||
return
|
||||
}
|
||||
body := fmt.Sprintf("============\n============\n============\n任务名称: %s\n状态: %s\n输出:\n %s\n", name, statusName, content)
|
||||
formatBody := slack.format(body)
|
||||
if slackSetting.Url == "" {
|
||||
logger.Error("#slack#webhook-url为空")
|
||||
return
|
||||
}
|
||||
if len(slackSetting.Channels) == 0 {
|
||||
logger.Error("#slack#channels配置为空")
|
||||
return
|
||||
}
|
||||
logger.Debugf("%+v", slackSetting)
|
||||
channels := slack.getActiveSlackChannels(slackSetting, msg)
|
||||
logger.Debugf("%+v", channels)
|
||||
for _, channel := range(channels) {
|
||||
slack.send(msg, slackSetting.Url, channel)
|
||||
}
|
||||
}
|
||||
|
||||
func (slack *Slack) send(msg Message, slackUrl string, channel string) {
|
||||
formatBody := slack.format(msg["content"].(string), channel)
|
||||
timeout := 30
|
||||
maxTimes := 3
|
||||
i := 0
|
||||
for i < maxTimes {
|
||||
resp := httpclient.PostBody(SlackUrl, formatBody, timeout)
|
||||
resp := httpclient.PostBody(slackUrl, formatBody, timeout)
|
||||
if resp.StatusCode == 200 {
|
||||
break;
|
||||
}
|
||||
i += 1
|
||||
time.Sleep(2 * time.Second)
|
||||
if i < maxTimes {
|
||||
logger.Error("slack#发送消息失败#%s#消息内容-%s", resp.Body, body)
|
||||
logger.Error("slack#发送消息失败#%s#消息内容-%s", resp.Body, msg["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (slack *Slack) getActiveSlackChannels(slackSetting models.Slack, msg Message) []string {
|
||||
taskReceiverIds := strings.Split(msg["task_receiver_id"].(string), ",")
|
||||
channels := []string{}
|
||||
for _, v := range(slackSetting.Channels) {
|
||||
if utils.InStringSlice(taskReceiverIds, strconv.Itoa(v.Id)) {
|
||||
channels = append(channels, v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
// 格式化消息内容
|
||||
func (slack *Slack) format(content string) string {
|
||||
func (slack *Slack) format(content string, channel string) string {
|
||||
content = utils.EscapeJson(content)
|
||||
specialChars := []string{"&", "<", ">"}
|
||||
replaceChars := []string{"&", "<", ">"}
|
||||
content = utils.ReplaceStrings(content, specialChars, replaceChars)
|
||||
|
||||
return fmt.Sprintf(`{"text":"%s","username":"监控"}`, content)
|
||||
return fmt.Sprintf(`{"text":"%s","username":"监控", "channel":"%s"}`, content, channel)
|
||||
}
|
|
@ -24,6 +24,15 @@ const ServerError = 4
|
|||
const SuccessContent = "操作成功"
|
||||
const FailureContent = "操作失败"
|
||||
|
||||
func JsonResponseByErr(err error) string {
|
||||
json := JsonResponse{}
|
||||
if err != nil {
|
||||
return json.CommonFailure(FailureContent, err)
|
||||
}
|
||||
|
||||
return json.Success(SuccessContent, nil)
|
||||
}
|
||||
|
||||
func (j *JsonResponse) Success(message string, data interface{}) string {
|
||||
return j.response(ResponseSuccess, message, data)
|
||||
}
|
||||
|
|
|
@ -89,6 +89,16 @@ func ReplaceStrings(s string, old []string, replace []string) string {
|
|||
return s
|
||||
}
|
||||
|
||||
func InStringSlice(slice []string, element string) bool {
|
||||
for _, v := range slice {
|
||||
if v == element {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 转义json特殊字符
|
||||
func EscapeJson(s string) string {
|
||||
specialChars := []string{"\\", "\b","\f", "\n", "\r", "\t", "\"",}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,6 +6,9 @@ function Util() {
|
|||
var util = {};
|
||||
var SUCCESS = 0; // 操作成功
|
||||
var FAILURE_MESSAGE = '操作失败';
|
||||
util.alertSuccess = function() {
|
||||
swal("操作成功", '保存成功', 'success');
|
||||
};
|
||||
// ajax成功处理
|
||||
util.ajaxSuccess = function(response, callback) {
|
||||
if (response.code === undefined) {
|
||||
|
@ -86,6 +89,13 @@ function Util() {
|
|||
return fields;
|
||||
};
|
||||
|
||||
util.renderTemplate = function($element, data) {
|
||||
var template = Handlebars.compile($($element).html());
|
||||
var html = template(data);
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
return util;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/ouqiang/gocron/routers/tasklog"
|
||||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/go-macaron/csrf"
|
||||
"github.com/go-macaron/toolbox"
|
||||
"strings"
|
||||
"github.com/ouqiang/gocron/modules/app"
|
||||
|
@ -17,6 +16,7 @@ import (
|
|||
"github.com/ouqiang/gocron/routers/user"
|
||||
"github.com/go-macaron/gzip"
|
||||
"github.com/ouqiang/gocron/routers/setting"
|
||||
"github.com/go-macaron/csrf"
|
||||
)
|
||||
|
||||
// 静态文件目录
|
||||
|
@ -39,6 +39,8 @@ func Register(m *macaron.Macaron) {
|
|||
m.Get("/login", user.Login)
|
||||
m.Post("/login", user.ValidateLogin)
|
||||
m.Get("/logout", user.Logout)
|
||||
m.Get("/editPassword", user.EditPassword)
|
||||
m.Post("/editPassword", user.UpdatePassword)
|
||||
})
|
||||
|
||||
// 任务
|
||||
|
@ -66,12 +68,22 @@ func Register(m *macaron.Macaron) {
|
|||
})
|
||||
|
||||
// 管理
|
||||
m.Group("/admin", func() {
|
||||
m.Group("/setting/", func() {
|
||||
m.Get("/slack", setting.EditSlack)
|
||||
m.Post("/slack", setting.StoreSlack)
|
||||
m.Group("/setting", func() {
|
||||
m.Group("/slack", func() {
|
||||
m.Get("/", setting.Slack)
|
||||
m.Get("/edit", setting.EditSlack)
|
||||
m.Post("/url", setting.UpdateSlackUrl)
|
||||
m.Post("/channel", setting.CreateSlackChannel)
|
||||
m.Post("/channel/remove/:id", setting.RemoveSlackChannel)
|
||||
})
|
||||
}, adminAuth)
|
||||
m.Group("/mail", func() {
|
||||
m.Get("/", setting.Mail)
|
||||
m.Get("/edit", setting.EditMail)
|
||||
m.Post("/server", binding.Bind(setting.MailServerForm{}), setting.UpdateMailServer)
|
||||
m.Post("/user", setting.CreateMailUser)
|
||||
m.Post("/user/remove/:id", setting.RemoveMailUser)
|
||||
})
|
||||
})
|
||||
|
||||
// 404错误
|
||||
m.NotFound(func(ctx *macaron.Context) {
|
||||
|
@ -100,7 +112,9 @@ func Register(m *macaron.Macaron) {
|
|||
func RegisterMiddleware(m *macaron.Macaron) {
|
||||
m.Use(macaron.Logger())
|
||||
m.Use(macaron.Recovery())
|
||||
m.Use(gzip.Gziper())
|
||||
if macaron.Env != macaron.DEV {
|
||||
m.Use(gzip.Gziper())
|
||||
}
|
||||
m.Use(macaron.Static(StaticDir))
|
||||
m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
Directory: "templates",
|
||||
|
|
|
@ -5,27 +5,126 @@ import (
|
|||
"github.com/ouqiang/gocron/modules/utils"
|
||||
"github.com/ouqiang/gocron/models"
|
||||
"github.com/ouqiang/gocron/modules/logger"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
|
||||
// region slack
|
||||
|
||||
func EditSlack(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "slack配置"
|
||||
ctx.Data["Title"] = "Slack配置"
|
||||
settingModel := new(models.Setting)
|
||||
url, err := settingModel.SlackUrl()
|
||||
slack, err := settingModel.Slack()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["SlackUrl"] = url
|
||||
ctx.Data["Slack"] = slack
|
||||
ctx.HTML(200, "setting/slack")
|
||||
}
|
||||
|
||||
func StoreSlack(ctx *macaron.Context) string {
|
||||
func Slack(ctx *macaron.Context) string {
|
||||
settingModel := new(models.Setting)
|
||||
slack, err := settingModel.Slack()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return json.Success("", slack)
|
||||
}
|
||||
|
||||
func UpdateSlackUrl(ctx *macaron.Context) string {
|
||||
url := ctx.QueryTrim("url")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateSlackUrl(url)
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func CreateSlackChannel(ctx *macaron.Context) string {
|
||||
channel := ctx.QueryTrim("channel")
|
||||
settingModel := new(models.Setting)
|
||||
if settingModel.IsChannelExist(channel) {
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return json.CommonFailure("Channel已存在")
|
||||
}
|
||||
_, err := settingModel.CreateChannel(channel)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func RemoveSlackChannel(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.RemoveChannel(id)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region 邮件
|
||||
|
||||
func EditMail(ctx *macaron.Context) {
|
||||
ctx.Data["Title"] = "邮件配置"
|
||||
settingModel := new(models.Setting)
|
||||
mail, err := settingModel.Mail()
|
||||
if err != nil {
|
||||
return json.CommonFailure(utils.FailureContent, err)
|
||||
logger.Error(err)
|
||||
}
|
||||
ctx.Data["Mail"] = mail
|
||||
ctx.HTML(200, "setting/mail")
|
||||
}
|
||||
|
||||
func Mail(ctx *macaron.Context) string {
|
||||
settingModel := new(models.Setting)
|
||||
mail, err := settingModel.Mail()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
return json.Success(utils.SuccessContent, nil)
|
||||
}
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return json.Success("", mail)
|
||||
}
|
||||
|
||||
|
||||
type MailServerForm struct {
|
||||
Host string `binding:"Required;MaxSize(100)"`
|
||||
Port int `binding:"Required;Range(1-65535)"`
|
||||
User string `binding:"Required;MaxSize(64);Email"`
|
||||
Password string `binding:"Required;MaxSize(64)"`
|
||||
}
|
||||
|
||||
func UpdateMailServer(ctx *macaron.Context, form MailServerForm) string {
|
||||
jsonByte, _ := json.Marshal(form)
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.UpdateMailServer(string(jsonByte))
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func CreateMailUser(ctx *macaron.Context) string {
|
||||
username := ctx.QueryTrim("username")
|
||||
email := ctx.QueryTrim("email")
|
||||
settingModel := new(models.Setting)
|
||||
if username == "" || email == "" {
|
||||
json := utils.JsonResponse{}
|
||||
|
||||
return json.CommonFailure("用户名、邮箱均不能为空")
|
||||
}
|
||||
_, err := settingModel.CreateMailUser(username, email)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
func RemoveMailUser(ctx *macaron.Context) string {
|
||||
id := ctx.ParamsInt(":id")
|
||||
settingModel := new(models.Setting)
|
||||
_, err := settingModel.RemoveMailUser(id)
|
||||
|
||||
return utils.JsonResponseByErr(err)
|
||||
}
|
||||
|
||||
// endregion
|
|
@ -25,6 +25,9 @@ type TaskForm struct {
|
|||
HostId int16
|
||||
Remark string
|
||||
Status models.Status `binding:"In(1,2)"`
|
||||
NotifyStatus int8 `binding:In(1,2,3)`
|
||||
NotifyType int8 `binding:In(1,2)`
|
||||
NotifyReceiverId string
|
||||
}
|
||||
|
||||
// 首页
|
||||
|
@ -113,7 +116,6 @@ func Store(ctx *macaron.Context, form TaskForm) string {
|
|||
} else {
|
||||
taskModel.HostId = 0
|
||||
}
|
||||
|
||||
taskModel.Name = form.Name
|
||||
taskModel.Protocol = form.Protocol
|
||||
taskModel.Command = form.Command
|
||||
|
@ -128,7 +130,13 @@ func Store(ctx *macaron.Context, form TaskForm) string {
|
|||
if taskModel.Multi != 1 {
|
||||
taskModel.Multi = 0
|
||||
}
|
||||
taskModel.NotifyStatus = form.NotifyStatus - 1
|
||||
taskModel.NotifyType = form.NotifyType - 1
|
||||
taskModel.NotifyReceiverId = form.NotifyReceiverId
|
||||
taskModel.Spec = form.Spec
|
||||
if taskModel.NotifyStatus > 0 && taskModel.NotifyReceiverId == "" {
|
||||
return json.CommonFailure("请至少选择一个接收者", err)
|
||||
}
|
||||
if id == 0 {
|
||||
id, err = taskModel.Create()
|
||||
} else {
|
||||
|
|
|
@ -228,13 +228,27 @@ func createJob(taskModel models.TaskHost) cron.FuncJob {
|
|||
}
|
||||
|
||||
var statusName string
|
||||
var enableNotify bool = true
|
||||
// 未开启通知
|
||||
if taskModel.NotifyStatus == 0 {
|
||||
enableNotify = false;
|
||||
} else if taskModel.NotifyStatus == 1 && taskResult.Err == nil {
|
||||
// 执行失败才发送通知
|
||||
enableNotify = false
|
||||
}
|
||||
if taskResult.Err != nil {
|
||||
statusName = "失败"
|
||||
} else {
|
||||
statusName = "成功"
|
||||
}
|
||||
if !enableNotify {
|
||||
return
|
||||
}
|
||||
// 发送通知
|
||||
msg := notify.Message{
|
||||
"name": taskModel.Task.Name,
|
||||
"task_type": taskModel.NotifyType,
|
||||
"task_receiver_id": taskModel.NotifyReceiverId,
|
||||
"name": taskModel.Task.Name,
|
||||
"output": taskResult.Result,
|
||||
"status": statusName,
|
||||
"taskId": taskModel.Id,
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
<script type="text/javascript" src="/resource/javascript/form.min.js"></script>
|
||||
<script type="text/javascript" src="/resource/sweetalert/sweetalert.min.js"></script>
|
||||
<script type="text/javascript" src="/resource/javascript/vue.min.js"></script>
|
||||
<script type="text/javascript" src="/resource/javascript/handlebars-v4.0.5.js"></script>
|
||||
<script type="text/javascript" src="/resource/javascript/main.js"></script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
{{{ template "common/header" . }}}
|
||||
<div class="ui grid">
|
||||
{{{template "setting/menu" .}}}
|
||||
<div class="twelve wide column">
|
||||
<div class="pageHeader">
|
||||
<div class="segment">
|
||||
<h3 class="ui dividing header">
|
||||
<div class="content">
|
||||
{{{.Title}}}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<form class="ui form fluid vertical segment mail-server">
|
||||
<div class="content">邮件服务器配置</div><br>
|
||||
<div class="four fields">
|
||||
<div class="field">
|
||||
<label>
|
||||
主机名
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="host" value="{{{.Mail.Host}}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
端口
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="port" value="{{{if gt .Mail.Port 0}}}{{{.Mail.Port}}}{{{end}}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
用户名
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="user" value="{{{.Mail.User}}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
密码
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="password" value="{{{.Mail.Password}}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button">保存</button>
|
||||
<br><br><br>
|
||||
<div>
|
||||
<div class="content">邮箱用户</div><p></p>
|
||||
<div class="fields">
|
||||
{{{range $i, $v := .Mail.MailUsers}}}
|
||||
<div class="field">
|
||||
<div class="ui segment">
|
||||
{{{.Username}}}-{{{.Email}}} <div class="ui blue button" onclick="removeMailUser({{{.Id}}})">删除</div>
|
||||
</div>
|
||||
</div>
|
||||
{{{end}}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="ui facebook button" onclick="createMailUser();">新增用户</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small modal">
|
||||
<div class="header">新增用户</div>
|
||||
<div class="content">
|
||||
<form class="ui form mail-user">
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>
|
||||
用户名
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>
|
||||
邮箱地址
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="email">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button">保存</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$('.mail-server').form(
|
||||
{
|
||||
onSuccess: function(event, fields) {
|
||||
util.post('/setting/mail/server',
|
||||
fields,
|
||||
function(code, message) {
|
||||
util.alertSuccess();
|
||||
}
|
||||
);
|
||||
return false;
|
||||
},
|
||||
fields: {
|
||||
host: {
|
||||
identifier : 'host',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入有效主机名'
|
||||
}
|
||||
]
|
||||
},
|
||||
port: {
|
||||
identifier : 'port',
|
||||
rules: [
|
||||
{
|
||||
type : 'integer[1..65535]',
|
||||
prompt : '请输入有效端口'
|
||||
}
|
||||
]
|
||||
},
|
||||
user: {
|
||||
identifier : 'user',
|
||||
rules: [
|
||||
{
|
||||
type : 'email',
|
||||
prompt : '请输入有效用户名'
|
||||
}
|
||||
]
|
||||
},
|
||||
password: {
|
||||
identifier : 'password',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入密码'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
inline : true
|
||||
});
|
||||
|
||||
$('.mail-user').form(
|
||||
{
|
||||
onSuccess: function(event, fields) {
|
||||
util.post('/setting/mail/user',
|
||||
fields,
|
||||
function(code, message) {
|
||||
util.alertSuccess();
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
return false;
|
||||
},
|
||||
fields: {
|
||||
username: {
|
||||
identifier : 'username',
|
||||
rules: [
|
||||
{
|
||||
type : 'empty',
|
||||
prompt : '请输入用户名'
|
||||
}
|
||||
]
|
||||
},
|
||||
email: {
|
||||
identifier : 'email',
|
||||
rules: [
|
||||
{
|
||||
type : 'email',
|
||||
prompt : '请输入有效的邮箱地址'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
inline : true
|
||||
});
|
||||
|
||||
function createMailUser() {
|
||||
$('.ui.modal').modal('show');
|
||||
}
|
||||
|
||||
function removeMailUser(id) {
|
||||
util.post('/setting/mail/user/remove/' + id, {}, function(code, message) {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
|
@ -1,9 +1,12 @@
|
|||
<div class="three wide column">
|
||||
<div class="verticalMenu">
|
||||
<div class="ui vertical pointing menu fluid">
|
||||
<a class="{{{if eq .URI "/admin/setting/slack"}}}active teal{{{end}}} item" href="/admin/setting/slack">
|
||||
<a class="{{{if eq .URI "/setting/slack/edit"}}}active teal{{{end}}} item" href="/setting/slack/edit">
|
||||
<i class="slack icon"></i> Slack配置
|
||||
</a>
|
||||
<a class="{{{if eq .URI "/setting/mail/edit"}}}active teal{{{end}}} item" href="/setting/mail/edit">
|
||||
<i class="slack icon"></i> 邮件配置
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -12,32 +12,77 @@
|
|||
</div>
|
||||
</div>
|
||||
<form class="ui form fluid vertical segment">
|
||||
<div class="two fields">
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
<div class="content">Slack WebHook URL</div>
|
||||
</label>
|
||||
<div class="ui small input">
|
||||
<input type="text" name="url" value="{{{.SlackUrl}}}">
|
||||
<input type="text" id="url" value="{{{.Slack.Url}}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui primary button" @click="updateUrl">保存</div>
|
||||
|
||||
<br><br><br>
|
||||
<div>
|
||||
<div class="content">Slack Channel(配置任务通知时,可选择多Channel)</div><p></p>
|
||||
<div class="fields">
|
||||
{{{range $i, $v := .Slack.Channels}}}
|
||||
<div class="field">
|
||||
<div class="ui segment">
|
||||
{{{.Name}}} <div class="ui blue button" @click="removeChannel({{{.Id}}})">删除</div>
|
||||
</div>
|
||||
</div>
|
||||
{{{end}}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui primary submit button">保存</div>
|
||||
<div class="ui facebook button" @click="createChannel">新增Channel</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var $uiForm = $('.ui.form');
|
||||
$($uiForm).form(
|
||||
{
|
||||
onSuccess: function(event, fields) {
|
||||
util.post('/admin/setting/slack', fields, function(code, message) {
|
||||
location.reload();
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
<script type="text/javascript">
|
||||
new Vue({
|
||||
el: '.ui.form',
|
||||
methods: {
|
||||
updateUrl: function() {
|
||||
var url = $('#url').val();
|
||||
util.post('/setting/slack/url', {"url": url}, function(code, message) {
|
||||
util.alertSuccess();
|
||||
});
|
||||
},
|
||||
createChannel: function() {
|
||||
swal({
|
||||
title: "新增Channel",
|
||||
type: "input",
|
||||
showCancelButton: true,
|
||||
closeOnConfirm: false,
|
||||
animation: "slide-from-top"
|
||||
},
|
||||
function(inputValue){
|
||||
if (inputValue === false) return false;
|
||||
|
||||
if (inputValue === "") {
|
||||
swal.showInputError("请输入Channel");
|
||||
return false
|
||||
}
|
||||
|
||||
util.post('/setting/slack/channel',
|
||||
{"channel": inputValue},
|
||||
function(code, message) {
|
||||
util.alertSuccess();
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
},
|
||||
removeChannel: function(id) {
|
||||
util.post('/setting/slack/channel/remove/' + id, {}, function(code, message) {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{{ template "common/footer" . }}}
|
|
@ -89,9 +89,9 @@
|
|||
<label>命令</label>
|
||||
<div class="ui blue message">
|
||||
根据选择的协议输入相应的命令 <br>
|
||||
系统命令 - windows: ipconfig /all linux: ifconfig -a <br>
|
||||
系统命令 - ifconfig -a <br>
|
||||
SSH - netstat -natpu <br>
|
||||
HTTP - http://golang.org <br>
|
||||
HTTP - URL地址 例: http://golang.org <br>
|
||||
</div>
|
||||
<textarea rows="5" name="command">{{{.Task.Command}}}</textarea>
|
||||
</div>
|
||||
|
@ -109,8 +109,8 @@
|
|||
<div class="field">
|
||||
<label>任务重试次数</label>
|
||||
<div class="ui blue message">
|
||||
无法连接远程主机,shell返回值非0, http响应码非200等异常返回,可重试<br>
|
||||
重试时间间隔 重试次数 * 分钟, 按1分钟、2分钟、3分钟.....的间隔进行重试<br>
|
||||
无法连接远程主机,shell返回值非0, http响应码非200等异常返回,可重复执行任务,
|
||||
重试时间间隔 重试次数 * 分钟, 按1分钟、2分钟、3分钟.....的间隔进行重试
|
||||
取值范围1-10, 默认0,不重试
|
||||
</div>
|
||||
<input type="text" name="retry_times" value="{{{.Task.RetryTimes}}}">
|
||||
|
@ -140,6 +140,27 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>任务通知</label>
|
||||
<select name="notify_status" id="task-status">
|
||||
<option value="1"{{{if .Task}}} {{{if eq .Task.NotifyStatus 0}}}selected{{{end}}} {{{end}}}>不通知</option>
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.NotifyStatus 1}}}selected{{{end}}} {{{end}}}>失败通知</option>
|
||||
<option value="3" {{{if .Task}}} {{{if eq .Task.NotifyStatus 2}}}selected{{{end}}} {{{end}}}>执行结束通知</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields" style="display: none" id="task-notify-type">
|
||||
<div class="field" >
|
||||
<label>通知类型</label>
|
||||
<select name="notify_type">
|
||||
<option value="1"{{{if .Task}}} {{{if eq .Task.NotifyType 0}}}selected{{{end}}} {{{end}}}>请选择</option>
|
||||
<option value="2" {{{if .Task}}} {{{if eq .Task.NotifyType 1}}}selected{{{end}}} {{{end}}}>邮件</option>
|
||||
<option value="3" {{{if .Task}}} {{{if eq .Task.NotifyType 2}}}selected{{{end}}} {{{end}}}>Slack</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline fields" style="display: none" id="receiver-id"></div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>备注</label>
|
||||
|
@ -151,16 +172,107 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script type="x-handlerbar-template" id="mail-template">
|
||||
{{#each MailUsers}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="receiver[]" {{#if checked}}checked{{/if}} value="{{Id}}" />
|
||||
<label>{{Username}}-{{Email}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a class="ui blue button" href="/setting/mail/edit">邮箱配置</a><br><br>
|
||||
{{/each}}
|
||||
</script>
|
||||
|
||||
<script type="x-handlervar-template" id="slack-template">
|
||||
{{#each Channels}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="receiver[]" {{#if checked}}checked{{/if}} value="{{Id}}" />
|
||||
<label>{{Name}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a class="ui blue button" href="/setting/slack/edit">Slack配置</a>
|
||||
{{/each}}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
changeProtocol();
|
||||
showNotify();
|
||||
});
|
||||
|
||||
$('#protocol').change(function() {
|
||||
changeProtocol();
|
||||
});
|
||||
|
||||
$('#task-status').change(function() {
|
||||
var selected = $(this).val();
|
||||
if (selected == 1) {
|
||||
$('#task-notify-type').hide();
|
||||
$('#receiver-id').hide();
|
||||
$('#task-notify-type').find('select').val('1');
|
||||
return;
|
||||
}
|
||||
$('#task-notify-type').show();
|
||||
});
|
||||
|
||||
$('#task-notify-type').change(function() {
|
||||
changeNotify();
|
||||
});
|
||||
|
||||
function showNotify() {
|
||||
var notifyStatus = {{{.Task.NotifyStatus}}};
|
||||
if (notifyStatus > 0) {
|
||||
$('#task-notify-type').show();
|
||||
}
|
||||
var notifyReceiverIds = '{{{.Task.NotifyReceiverId}}}'.split(',');
|
||||
changeNotify(notifyReceiverIds);
|
||||
}
|
||||
|
||||
function changeNotify(notifyReceiverIds) {
|
||||
var selectedId = $('#task-notify-type').find('select').val();
|
||||
if (selectedId == 1) {
|
||||
$('#receiver-id').hide();
|
||||
$('#receiver-id').html('');
|
||||
return;
|
||||
}
|
||||
if (selectedId == 2) {
|
||||
showMail(notifyReceiverIds);
|
||||
} else if (selectedId == 3) {
|
||||
showSlack(notifyReceiverIds);
|
||||
}
|
||||
$('#receiver-id').show();
|
||||
}
|
||||
|
||||
function showMail(notifyReceiverIds) {
|
||||
util.get("/setting/mail", function(code, message, data) {
|
||||
renderReceiver(notifyReceiverIds, $('#mail-template'), data, 'MailUsers');
|
||||
})
|
||||
}
|
||||
|
||||
function showSlack(notifyReceiverIds) {
|
||||
util.get("/setting/slack", function(code, message, data) {
|
||||
renderReceiver(notifyReceiverIds, $('#slack-template'), data, 'Channels');
|
||||
})
|
||||
}
|
||||
|
||||
function renderReceiver(notifyReceiverIds, $element, data, key) {
|
||||
if (notifyReceiverIds !== undefined && notifyReceiverIds) {
|
||||
console.log(data[key]);
|
||||
for (i in data[key]) {
|
||||
if ($.inArray(data[key][i].Id + '', notifyReceiverIds) != -1) {
|
||||
data[key][i].checked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
var html = util.renderTemplate($($element), data);
|
||||
$('#receiver-id').html(html);
|
||||
$('.ui.checkbox').checkbox();
|
||||
}
|
||||
|
||||
function changeProtocol() {
|
||||
var protocol = $('#protocol').val();
|
||||
if (protocol == 2) {
|
||||
|
@ -174,11 +286,40 @@
|
|||
$('.ui.checkbox')
|
||||
.checkbox()
|
||||
;
|
||||
|
||||
function validateNotify() {
|
||||
var selectedId = $('#task-status').val();
|
||||
if (selectedId == 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var checkedLength = $('#receiver-id input:checked').length;
|
||||
if (checkedLength == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseNotifyReceiver() {
|
||||
var receivers = [];
|
||||
$('#receiver-id input:checked').each(function() {
|
||||
receivers.push($(this).val());
|
||||
});
|
||||
|
||||
return receivers.join(",");
|
||||
}
|
||||
|
||||
var $uiForm = $('.ui.form');
|
||||
registerSelectFormValidation("selectProtocol", $uiForm, $('#protocol'), 'protocol');
|
||||
$($uiForm).form(
|
||||
{
|
||||
onSuccess: function(event, fields) {
|
||||
if (!validateNotify()) {
|
||||
swal('错误提示', '请至少选择一个接收者', 'error');
|
||||
return false;
|
||||
}
|
||||
fields.notify_receiver_id = parseNotifyReceiver();
|
||||
util.post('/task/store', fields, function(code, message) {
|
||||
location.href = "/task"
|
||||
});
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [2.0.0] - 2015-09-02
|
||||
|
||||
- Mailer has been removed. It has been replaced by Dialer and Sender.
|
||||
- `File` type and the `CreateFile` and `OpenFile` functions have been removed.
|
||||
- `Message.Attach` and `Message.Embed` have a new signature.
|
||||
- `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter`
|
||||
instead.
|
||||
- `Message.Export` has been removed. `Message.WriteTo` can be used instead.
|
||||
- `Message.DelHeader` has been removed.
|
||||
- The `Bcc` header field is no longer sent. It is far more simpler and
|
||||
efficient: the same message is sent to all recipients instead of sending a
|
||||
different email to each Bcc address.
|
||||
- LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN
|
||||
authentication mechanism when needed.
|
||||
- Go 1.2 is now required instead of Go 1.3. No external dependency are used when
|
||||
using Go 1.5.
|
|
@ -0,0 +1,20 @@
|
|||
Thank you for contributing to Gomail! Here are a few guidelines:
|
||||
|
||||
## Bugs
|
||||
|
||||
If you think you found a bug, create an issue and supply the minimum amount
|
||||
of code triggering the bug so it can be reproduced.
|
||||
|
||||
|
||||
## Fixing a bug
|
||||
|
||||
If you want to fix a bug, you can send a pull request. It should contains a
|
||||
new test or update an existing one to cover that bug.
|
||||
|
||||
|
||||
## New feature proposal
|
||||
|
||||
If you think Gomail lacks a feature, you can open an issue or send a pull
|
||||
request. I want to keep Gomail code and API as simple as possible so please
|
||||
describe your needs so we can discuss whether this feature should be added to
|
||||
Gomail or not.
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Alexandre Cesaro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,92 @@
|
|||
# Gomail
|
||||
[![Build Status](https://travis-ci.org/go-gomail/gomail.svg?branch=v2)](https://travis-ci.org/go-gomail/gomail) [![Code Coverage](http://gocover.io/_badge/gopkg.in/gomail.v2)](http://gocover.io/gopkg.in/gomail.v2) [![Documentation](https://godoc.org/gopkg.in/gomail.v2?status.svg)](https://godoc.org/gopkg.in/gomail.v2)
|
||||
|
||||
## Introduction
|
||||
|
||||
Gomail is a simple and efficient package to send emails. It is well tested and
|
||||
documented.
|
||||
|
||||
Gomail can only send emails using an SMTP server. But the API is flexible and it
|
||||
is easy to implement other methods for sending emails using a local Postfix, an
|
||||
API, etc.
|
||||
|
||||
It is versioned using [gopkg.in](https://gopkg.in) so I promise
|
||||
there will never be backward incompatible changes within each version.
|
||||
|
||||
It requires Go 1.2 or newer. With Go 1.5, no external dependencies are used.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
Gomail supports:
|
||||
- Attachments
|
||||
- Embedded images
|
||||
- HTML and text templates
|
||||
- Automatic encoding of special characters
|
||||
- SSL and TLS
|
||||
- Sending multiple emails with the same SMTP connection
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
https://godoc.org/gopkg.in/gomail.v2
|
||||
|
||||
|
||||
## Download
|
||||
|
||||
go get gopkg.in/gomail.v2
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
See the [examples in the documentation](https://godoc.org/gopkg.in/gomail.v2#example-package).
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### x509: certificate signed by unknown authority
|
||||
|
||||
If you get this error it means the certificate used by the SMTP server is not
|
||||
considered valid by the client running Gomail. As a quick workaround you can
|
||||
bypass the verification of the server's certificate chain and host name by using
|
||||
`SetTLSConfig`:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
// Send emails using d.
|
||||
}
|
||||
|
||||
Note, however, that this is insecure and should not be used in production.
|
||||
|
||||
|
||||
## Contribute
|
||||
|
||||
Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for
|
||||
more info.
|
||||
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
You can ask questions on the [Gomail
|
||||
thread](https://groups.google.com/d/topic/golang-nuts/jMxZHzvvEVg/discussion)
|
||||
in the Go mailing-list.
|
|
@ -0,0 +1,49 @@
|
|||
package gomail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism.
|
||||
type loginAuth struct {
|
||||
username string
|
||||
password string
|
||||
host string
|
||||
}
|
||||
|
||||
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
||||
if !server.TLS {
|
||||
advertised := false
|
||||
for _, mechanism := range server.Auth {
|
||||
if mechanism == "LOGIN" {
|
||||
advertised = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !advertised {
|
||||
return "", nil, errors.New("gomail: unencrypted connection")
|
||||
}
|
||||
}
|
||||
if server.Name != a.host {
|
||||
return "", nil, errors.New("gomail: wrong host name")
|
||||
}
|
||||
return "LOGIN", nil, nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if !more {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.Equal(fromServer, []byte("Username:")):
|
||||
return []byte(a.username), nil
|
||||
case bytes.Equal(fromServer, []byte("Password:")):
|
||||
return []byte(a.password), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// Package gomail provides a simple interface to compose emails and to mail them
|
||||
// efficiently.
|
||||
//
|
||||
// More info on Github: https://github.com/go-gomail/gomail
|
||||
package gomail
|
|
@ -0,0 +1,322 @@
|
|||
package gomail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Message represents an email.
|
||||
type Message struct {
|
||||
header header
|
||||
parts []*part
|
||||
attachments []*file
|
||||
embedded []*file
|
||||
charset string
|
||||
encoding Encoding
|
||||
hEncoder mimeEncoder
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
type header map[string][]string
|
||||
|
||||
type part struct {
|
||||
contentType string
|
||||
copier func(io.Writer) error
|
||||
encoding Encoding
|
||||
}
|
||||
|
||||
// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
|
||||
// by default.
|
||||
func NewMessage(settings ...MessageSetting) *Message {
|
||||
m := &Message{
|
||||
header: make(header),
|
||||
charset: "UTF-8",
|
||||
encoding: QuotedPrintable,
|
||||
}
|
||||
|
||||
m.applySettings(settings)
|
||||
|
||||
if m.encoding == Base64 {
|
||||
m.hEncoder = bEncoding
|
||||
} else {
|
||||
m.hEncoder = qEncoding
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Reset resets the message so it can be reused. The message keeps its previous
|
||||
// settings so it is in the same state that after a call to NewMessage.
|
||||
func (m *Message) Reset() {
|
||||
for k := range m.header {
|
||||
delete(m.header, k)
|
||||
}
|
||||
m.parts = nil
|
||||
m.attachments = nil
|
||||
m.embedded = nil
|
||||
}
|
||||
|
||||
func (m *Message) applySettings(settings []MessageSetting) {
|
||||
for _, s := range settings {
|
||||
s(m)
|
||||
}
|
||||
}
|
||||
|
||||
// A MessageSetting can be used as an argument in NewMessage to configure an
|
||||
// email.
|
||||
type MessageSetting func(m *Message)
|
||||
|
||||
// SetCharset is a message setting to set the charset of the email.
|
||||
func SetCharset(charset string) MessageSetting {
|
||||
return func(m *Message) {
|
||||
m.charset = charset
|
||||
}
|
||||
}
|
||||
|
||||
// SetEncoding is a message setting to set the encoding of the email.
|
||||
func SetEncoding(enc Encoding) MessageSetting {
|
||||
return func(m *Message) {
|
||||
m.encoding = enc
|
||||
}
|
||||
}
|
||||
|
||||
// Encoding represents a MIME encoding scheme like quoted-printable or base64.
|
||||
type Encoding string
|
||||
|
||||
const (
|
||||
// QuotedPrintable represents the quoted-printable encoding as defined in
|
||||
// RFC 2045.
|
||||
QuotedPrintable Encoding = "quoted-printable"
|
||||
// Base64 represents the base64 encoding as defined in RFC 2045.
|
||||
Base64 Encoding = "base64"
|
||||
// Unencoded can be used to avoid encoding the body of an email. The headers
|
||||
// will still be encoded using quoted-printable encoding.
|
||||
Unencoded Encoding = "8bit"
|
||||
)
|
||||
|
||||
// SetHeader sets a value to the given header field.
|
||||
func (m *Message) SetHeader(field string, value ...string) {
|
||||
m.encodeHeader(value)
|
||||
m.header[field] = value
|
||||
}
|
||||
|
||||
func (m *Message) encodeHeader(values []string) {
|
||||
for i := range values {
|
||||
values[i] = m.encodeString(values[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) encodeString(value string) string {
|
||||
return m.hEncoder.Encode(m.charset, value)
|
||||
}
|
||||
|
||||
// SetHeaders sets the message headers.
|
||||
func (m *Message) SetHeaders(h map[string][]string) {
|
||||
for k, v := range h {
|
||||
m.SetHeader(k, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAddressHeader sets an address to the given header field.
|
||||
func (m *Message) SetAddressHeader(field, address, name string) {
|
||||
m.header[field] = []string{m.FormatAddress(address, name)}
|
||||
}
|
||||
|
||||
// FormatAddress formats an address and a name as a valid RFC 5322 address.
|
||||
func (m *Message) FormatAddress(address, name string) string {
|
||||
if name == "" {
|
||||
return address
|
||||
}
|
||||
|
||||
enc := m.encodeString(name)
|
||||
if enc == name {
|
||||
m.buf.WriteByte('"')
|
||||
for i := 0; i < len(name); i++ {
|
||||
b := name[i]
|
||||
if b == '\\' || b == '"' {
|
||||
m.buf.WriteByte('\\')
|
||||
}
|
||||
m.buf.WriteByte(b)
|
||||
}
|
||||
m.buf.WriteByte('"')
|
||||
} else if hasSpecials(name) {
|
||||
m.buf.WriteString(bEncoding.Encode(m.charset, name))
|
||||
} else {
|
||||
m.buf.WriteString(enc)
|
||||
}
|
||||
m.buf.WriteString(" <")
|
||||
m.buf.WriteString(address)
|
||||
m.buf.WriteByte('>')
|
||||
|
||||
addr := m.buf.String()
|
||||
m.buf.Reset()
|
||||
return addr
|
||||
}
|
||||
|
||||
func hasSpecials(text string) bool {
|
||||
for i := 0; i < len(text); i++ {
|
||||
switch c := text[i]; c {
|
||||
case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDateHeader sets a date to the given header field.
|
||||
func (m *Message) SetDateHeader(field string, date time.Time) {
|
||||
m.header[field] = []string{m.FormatDate(date)}
|
||||
}
|
||||
|
||||
// FormatDate formats a date as a valid RFC 5322 date.
|
||||
func (m *Message) FormatDate(date time.Time) string {
|
||||
return date.Format(time.RFC1123Z)
|
||||
}
|
||||
|
||||
// GetHeader gets a header field.
|
||||
func (m *Message) GetHeader(field string) []string {
|
||||
return m.header[field]
|
||||
}
|
||||
|
||||
// SetBody sets the body of the message. It replaces any content previously set
|
||||
// by SetBody, AddAlternative or AddAlternativeWriter.
|
||||
func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
|
||||
m.parts = []*part{m.newPart(contentType, newCopier(body), settings)}
|
||||
}
|
||||
|
||||
// AddAlternative adds an alternative part to the message.
|
||||
//
|
||||
// It is commonly used to send HTML emails that default to the plain text
|
||||
// version for backward compatibility. AddAlternative appends the new part to
|
||||
// the end of the message. So the plain text part should be added before the
|
||||
// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative
|
||||
func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
|
||||
m.AddAlternativeWriter(contentType, newCopier(body), settings...)
|
||||
}
|
||||
|
||||
func newCopier(s string) func(io.Writer) error {
|
||||
return func(w io.Writer) error {
|
||||
_, err := io.WriteString(w, s)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// AddAlternativeWriter adds an alternative part to the message. It can be
|
||||
// useful with the text/template or html/template packages.
|
||||
func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
|
||||
m.parts = append(m.parts, m.newPart(contentType, f, settings))
|
||||
}
|
||||
|
||||
func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part {
|
||||
p := &part{
|
||||
contentType: contentType,
|
||||
copier: f,
|
||||
encoding: m.encoding,
|
||||
}
|
||||
|
||||
for _, s := range settings {
|
||||
s(p)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// A PartSetting can be used as an argument in Message.SetBody,
|
||||
// Message.AddAlternative or Message.AddAlternativeWriter to configure the part
|
||||
// added to a message.
|
||||
type PartSetting func(*part)
|
||||
|
||||
// SetPartEncoding sets the encoding of the part added to the message. By
|
||||
// default, parts use the same encoding than the message.
|
||||
func SetPartEncoding(e Encoding) PartSetting {
|
||||
return PartSetting(func(p *part) {
|
||||
p.encoding = e
|
||||
})
|
||||
}
|
||||
|
||||
type file struct {
|
||||
Name string
|
||||
Header map[string][]string
|
||||
CopyFunc func(w io.Writer) error
|
||||
}
|
||||
|
||||
func (f *file) setHeader(field, value string) {
|
||||
f.Header[field] = []string{value}
|
||||
}
|
||||
|
||||
// A FileSetting can be used as an argument in Message.Attach or Message.Embed.
|
||||
type FileSetting func(*file)
|
||||
|
||||
// SetHeader is a file setting to set the MIME header of the message part that
|
||||
// contains the file content.
|
||||
//
|
||||
// Mandatory headers are automatically added if they are not set when sending
|
||||
// the email.
|
||||
func SetHeader(h map[string][]string) FileSetting {
|
||||
return func(f *file) {
|
||||
for k, v := range h {
|
||||
f.Header[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rename is a file setting to set the name of the attachment if the name is
|
||||
// different than the filename on disk.
|
||||
func Rename(name string) FileSetting {
|
||||
return func(f *file) {
|
||||
f.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// SetCopyFunc is a file setting to replace the function that runs when the
|
||||
// message is sent. It should copy the content of the file to the io.Writer.
|
||||
//
|
||||
// The default copy function opens the file with the given filename, and copy
|
||||
// its content to the io.Writer.
|
||||
func SetCopyFunc(f func(io.Writer) error) FileSetting {
|
||||
return func(fi *file) {
|
||||
fi.CopyFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) appendFile(list []*file, name string, settings []FileSetting) []*file {
|
||||
f := &file{
|
||||
Name: filepath.Base(name),
|
||||
Header: make(map[string][]string),
|
||||
CopyFunc: func(w io.Writer) error {
|
||||
h, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(w, h); err != nil {
|
||||
h.Close()
|
||||
return err
|
||||
}
|
||||
return h.Close()
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range settings {
|
||||
s(f)
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
return []*file{f}
|
||||
}
|
||||
|
||||
return append(list, f)
|
||||
}
|
||||
|
||||
// Attach attaches the files to the email.
|
||||
func (m *Message) Attach(filename string, settings ...FileSetting) {
|
||||
m.attachments = m.appendFile(m.attachments, filename, settings)
|
||||
}
|
||||
|
||||
// Embed embeds the images to the email.
|
||||
func (m *Message) Embed(filename string, settings ...FileSetting) {
|
||||
m.embedded = m.appendFile(m.embedded, filename, settings)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// +build go1.5
|
||||
|
||||
package gomail
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"mime/quotedprintable"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var newQPWriter = quotedprintable.NewWriter
|
||||
|
||||
type mimeEncoder struct {
|
||||
mime.WordEncoder
|
||||
}
|
||||
|
||||
var (
|
||||
bEncoding = mimeEncoder{mime.BEncoding}
|
||||
qEncoding = mimeEncoder{mime.QEncoding}
|
||||
lastIndexByte = strings.LastIndexByte
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
// +build !go1.5
|
||||
|
||||
package gomail
|
||||
|
||||
import "gopkg.in/alexcesaro/quotedprintable.v3"
|
||||
|
||||
var newQPWriter = quotedprintable.NewWriter
|
||||
|
||||
type mimeEncoder struct {
|
||||
quotedprintable.WordEncoder
|
||||
}
|
||||
|
||||
var (
|
||||
bEncoding = mimeEncoder{quotedprintable.BEncoding}
|
||||
qEncoding = mimeEncoder{quotedprintable.QEncoding}
|
||||
lastIndexByte = func(s string, c byte) int {
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
|
||||
if s[i] == c {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
)
|
|
@ -0,0 +1,116 @@
|
|||
package gomail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
)
|
||||
|
||||
// Sender is the interface that wraps the Send method.
|
||||
//
|
||||
// Send sends an email to the given addresses.
|
||||
type Sender interface {
|
||||
Send(from string, to []string, msg io.WriterTo) error
|
||||
}
|
||||
|
||||
// SendCloser is the interface that groups the Send and Close methods.
|
||||
type SendCloser interface {
|
||||
Sender
|
||||
Close() error
|
||||
}
|
||||
|
||||
// A SendFunc is a function that sends emails to the given addresses.
|
||||
//
|
||||
// The SendFunc type is an adapter to allow the use of ordinary functions as
|
||||
// email senders. If f is a function with the appropriate signature, SendFunc(f)
|
||||
// is a Sender object that calls f.
|
||||
type SendFunc func(from string, to []string, msg io.WriterTo) error
|
||||
|
||||
// Send calls f(from, to, msg).
|
||||
func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error {
|
||||
return f(from, to, msg)
|
||||
}
|
||||
|
||||
// Send sends emails using the given Sender.
|
||||
func Send(s Sender, msg ...*Message) error {
|
||||
for i, m := range msg {
|
||||
if err := send(s, m); err != nil {
|
||||
return fmt.Errorf("gomail: could not send email %d: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func send(s Sender, m *Message) error {
|
||||
from, err := m.getFrom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
to, err := m.getRecipients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Send(from, to, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) getFrom() (string, error) {
|
||||
from := m.header["Sender"]
|
||||
if len(from) == 0 {
|
||||
from = m.header["From"]
|
||||
if len(from) == 0 {
|
||||
return "", errors.New(`gomail: invalid message, "From" field is absent`)
|
||||
}
|
||||
}
|
||||
|
||||
return parseAddress(from[0])
|
||||
}
|
||||
|
||||
func (m *Message) getRecipients() ([]string, error) {
|
||||
n := 0
|
||||
for _, field := range []string{"To", "Cc", "Bcc"} {
|
||||
if addresses, ok := m.header[field]; ok {
|
||||
n += len(addresses)
|
||||
}
|
||||
}
|
||||
list := make([]string, 0, n)
|
||||
|
||||
for _, field := range []string{"To", "Cc", "Bcc"} {
|
||||
if addresses, ok := m.header[field]; ok {
|
||||
for _, a := range addresses {
|
||||
addr, err := parseAddress(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = addAddress(list, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func addAddress(list []string, addr string) []string {
|
||||
for _, a := range list {
|
||||
if addr == a {
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
return append(list, addr)
|
||||
}
|
||||
|
||||
func parseAddress(field string) (string, error) {
|
||||
addr, err := mail.ParseAddress(field)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("gomail: invalid address %q: %v", field, err)
|
||||
}
|
||||
return addr.Address, nil
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
package gomail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Dialer is a dialer to an SMTP server.
|
||||
type Dialer struct {
|
||||
// Host represents the host of the SMTP server.
|
||||
Host string
|
||||
// Port represents the port of the SMTP server.
|
||||
Port int
|
||||
// Username is the username to use to authenticate to the SMTP server.
|
||||
Username string
|
||||
// Password is the password to use to authenticate to the SMTP server.
|
||||
Password string
|
||||
// Auth represents the authentication mechanism used to authenticate to the
|
||||
// SMTP server.
|
||||
Auth smtp.Auth
|
||||
// SSL defines whether an SSL connection is used. It should be false in
|
||||
// most cases since the authentication mechanism should use the STARTTLS
|
||||
// extension instead.
|
||||
SSL bool
|
||||
// TSLConfig represents the TLS configuration used for the TLS (when the
|
||||
// STARTTLS extension is used) or SSL connection.
|
||||
TLSConfig *tls.Config
|
||||
// LocalName is the hostname sent to the SMTP server with the HELO command.
|
||||
// By default, "localhost" is sent.
|
||||
LocalName string
|
||||
}
|
||||
|
||||
// NewDialer returns a new SMTP Dialer. The given parameters are used to connect
|
||||
// to the SMTP server.
|
||||
func NewDialer(host string, port int, username, password string) *Dialer {
|
||||
return &Dialer{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
SSL: port == 465,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPlainDialer returns a new SMTP Dialer. The given parameters are used to
|
||||
// connect to the SMTP server.
|
||||
//
|
||||
// Deprecated: Use NewDialer instead.
|
||||
func NewPlainDialer(host string, port int, username, password string) *Dialer {
|
||||
return NewDialer(host, port, username, password)
|
||||
}
|
||||
|
||||
// Dial dials and authenticates to an SMTP server. The returned SendCloser
|
||||
// should be closed when done using it.
|
||||
func (d *Dialer) Dial() (SendCloser, error) {
|
||||
conn, err := netDialTimeout("tcp", addr(d.Host, d.Port), 10*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.SSL {
|
||||
conn = tlsClient(conn, d.tlsConfig())
|
||||
}
|
||||
|
||||
c, err := smtpNewClient(conn, d.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.LocalName != "" {
|
||||
if err := c.Hello(d.LocalName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !d.SSL {
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
if err := c.StartTLS(d.tlsConfig()); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d.Auth == nil && d.Username != "" {
|
||||
if ok, auths := c.Extension("AUTH"); ok {
|
||||
if strings.Contains(auths, "CRAM-MD5") {
|
||||
d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password)
|
||||
} else if strings.Contains(auths, "LOGIN") &&
|
||||
!strings.Contains(auths, "PLAIN") {
|
||||
d.Auth = &loginAuth{
|
||||
username: d.Username,
|
||||
password: d.Password,
|
||||
host: d.Host,
|
||||
}
|
||||
} else {
|
||||
d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d.Auth != nil {
|
||||
if err = c.Auth(d.Auth); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &smtpSender{c, d}, nil
|
||||
}
|
||||
|
||||
func (d *Dialer) tlsConfig() *tls.Config {
|
||||
if d.TLSConfig == nil {
|
||||
return &tls.Config{ServerName: d.Host}
|
||||
}
|
||||
return d.TLSConfig
|
||||
}
|
||||
|
||||
func addr(host string, port int) string {
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
|
||||
// DialAndSend opens a connection to the SMTP server, sends the given emails and
|
||||
// closes the connection.
|
||||
func (d *Dialer) DialAndSend(m ...*Message) error {
|
||||
s, err := d.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
return Send(s, m...)
|
||||
}
|
||||
|
||||
type smtpSender struct {
|
||||
smtpClient
|
||||
d *Dialer
|
||||
}
|
||||
|
||||
func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
|
||||
if err := c.Mail(from); err != nil {
|
||||
if err == io.EOF {
|
||||
// This is probably due to a timeout, so reconnect and try again.
|
||||
sc, derr := c.d.Dial()
|
||||
if derr == nil {
|
||||
if s, ok := sc.(*smtpSender); ok {
|
||||
*c = *s
|
||||
return c.Send(from, to, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range to {
|
||||
if err := c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = msg.WriteTo(w); err != nil {
|
||||
w.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func (c *smtpSender) Close() error {
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Stubbed out for tests.
|
||||
var (
|
||||
netDialTimeout = net.DialTimeout
|
||||
tlsClient = tls.Client
|
||||
smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
|
||||
return smtp.NewClient(conn, host)
|
||||
}
|
||||
)
|
||||
|
||||
type smtpClient interface {
|
||||
Hello(string) error
|
||||
Extension(string) (bool, string)
|
||||
StartTLS(*tls.Config) error
|
||||
Auth(smtp.Auth) error
|
||||
Mail(string) error
|
||||
Rcpt(string) error
|
||||
Data() (io.WriteCloser, error)
|
||||
Quit() error
|
||||
Close() error
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
package gomail
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WriteTo implements io.WriterTo. It dumps the whole message into w.
|
||||
func (m *Message) WriteTo(w io.Writer) (int64, error) {
|
||||
mw := &messageWriter{w: w}
|
||||
mw.writeMessage(m)
|
||||
return mw.n, mw.err
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeMessage(m *Message) {
|
||||
if _, ok := m.header["Mime-Version"]; !ok {
|
||||
w.writeString("Mime-Version: 1.0\r\n")
|
||||
}
|
||||
if _, ok := m.header["Date"]; !ok {
|
||||
w.writeHeader("Date", m.FormatDate(now()))
|
||||
}
|
||||
w.writeHeaders(m.header)
|
||||
|
||||
if m.hasMixedPart() {
|
||||
w.openMultipart("mixed")
|
||||
}
|
||||
|
||||
if m.hasRelatedPart() {
|
||||
w.openMultipart("related")
|
||||
}
|
||||
|
||||
if m.hasAlternativePart() {
|
||||
w.openMultipart("alternative")
|
||||
}
|
||||
for _, part := range m.parts {
|
||||
w.writePart(part, m.charset)
|
||||
}
|
||||
if m.hasAlternativePart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(m.embedded, false)
|
||||
if m.hasRelatedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(m.attachments, true)
|
||||
if m.hasMixedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) hasMixedPart() bool {
|
||||
return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1
|
||||
}
|
||||
|
||||
func (m *Message) hasRelatedPart() bool {
|
||||
return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1
|
||||
}
|
||||
|
||||
func (m *Message) hasAlternativePart() bool {
|
||||
return len(m.parts) > 1
|
||||
}
|
||||
|
||||
type messageWriter struct {
|
||||
w io.Writer
|
||||
n int64
|
||||
writers [3]*multipart.Writer
|
||||
partWriter io.Writer
|
||||
depth uint8
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *messageWriter) openMultipart(mimeType string) {
|
||||
mw := multipart.NewWriter(w)
|
||||
contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
|
||||
w.writers[w.depth] = mw
|
||||
|
||||
if w.depth == 0 {
|
||||
w.writeHeader("Content-Type", contentType)
|
||||
w.writeString("\r\n")
|
||||
} else {
|
||||
w.createPart(map[string][]string{
|
||||
"Content-Type": {contentType},
|
||||
})
|
||||
}
|
||||
w.depth++
|
||||
}
|
||||
|
||||
func (w *messageWriter) createPart(h map[string][]string) {
|
||||
w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h)
|
||||
}
|
||||
|
||||
func (w *messageWriter) closeMultipart() {
|
||||
if w.depth > 0 {
|
||||
w.writers[w.depth-1].Close()
|
||||
w.depth--
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writePart(p *part, charset string) {
|
||||
w.writeHeaders(map[string][]string{
|
||||
"Content-Type": {p.contentType + "; charset=" + charset},
|
||||
"Content-Transfer-Encoding": {string(p.encoding)},
|
||||
})
|
||||
w.writeBody(p.copier, p.encoding)
|
||||
}
|
||||
|
||||
func (w *messageWriter) addFiles(files []*file, isAttachment bool) {
|
||||
for _, f := range files {
|
||||
if _, ok := f.Header["Content-Type"]; !ok {
|
||||
mediaType := mime.TypeByExtension(filepath.Ext(f.Name))
|
||||
if mediaType == "" {
|
||||
mediaType = "application/octet-stream"
|
||||
}
|
||||
f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`)
|
||||
}
|
||||
|
||||
if _, ok := f.Header["Content-Transfer-Encoding"]; !ok {
|
||||
f.setHeader("Content-Transfer-Encoding", string(Base64))
|
||||
}
|
||||
|
||||
if _, ok := f.Header["Content-Disposition"]; !ok {
|
||||
var disp string
|
||||
if isAttachment {
|
||||
disp = "attachment"
|
||||
} else {
|
||||
disp = "inline"
|
||||
}
|
||||
f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`)
|
||||
}
|
||||
|
||||
if !isAttachment {
|
||||
if _, ok := f.Header["Content-ID"]; !ok {
|
||||
f.setHeader("Content-ID", "<"+f.Name+">")
|
||||
}
|
||||
}
|
||||
w.writeHeaders(f.Header)
|
||||
w.writeBody(f.CopyFunc, Base64)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) Write(p []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, errors.New("gomail: cannot write as writer is in error")
|
||||
}
|
||||
|
||||
var n int
|
||||
n, w.err = w.w.Write(p)
|
||||
w.n += int64(n)
|
||||
return n, w.err
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeString(s string) {
|
||||
n, _ := io.WriteString(w.w, s)
|
||||
w.n += int64(n)
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeader(k string, v ...string) {
|
||||
w.writeString(k)
|
||||
if len(v) == 0 {
|
||||
w.writeString(":\r\n")
|
||||
return
|
||||
}
|
||||
w.writeString(": ")
|
||||
|
||||
// Max header line length is 78 characters in RFC 5322 and 76 characters
|
||||
// in RFC 2047. So for the sake of simplicity we use the 76 characters
|
||||
// limit.
|
||||
charsLeft := 76 - len(k) - len(": ")
|
||||
|
||||
for i, s := range v {
|
||||
// If the line is already too long, insert a newline right away.
|
||||
if charsLeft < 1 {
|
||||
if i == 0 {
|
||||
w.writeString("\r\n ")
|
||||
} else {
|
||||
w.writeString(",\r\n ")
|
||||
}
|
||||
charsLeft = 75
|
||||
} else if i != 0 {
|
||||
w.writeString(", ")
|
||||
charsLeft -= 2
|
||||
}
|
||||
|
||||
// While the header content is too long, fold it by inserting a newline.
|
||||
for len(s) > charsLeft {
|
||||
s = w.writeLine(s, charsLeft)
|
||||
charsLeft = 75
|
||||
}
|
||||
w.writeString(s)
|
||||
if i := lastIndexByte(s, '\n'); i != -1 {
|
||||
charsLeft = 75 - (len(s) - i - 1)
|
||||
} else {
|
||||
charsLeft -= len(s)
|
||||
}
|
||||
}
|
||||
w.writeString("\r\n")
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeLine(s string, charsLeft int) string {
|
||||
// If there is already a newline before the limit. Write the line.
|
||||
if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft {
|
||||
w.writeString(s[:i+1])
|
||||
return s[i+1:]
|
||||
}
|
||||
|
||||
for i := charsLeft - 1; i >= 0; i-- {
|
||||
if s[i] == ' ' {
|
||||
w.writeString(s[:i])
|
||||
w.writeString("\r\n ")
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// We could not insert a newline cleanly so look for a space or a newline
|
||||
// even if it is after the limit.
|
||||
for i := 75; i < len(s); i++ {
|
||||
if s[i] == ' ' {
|
||||
w.writeString(s[:i])
|
||||
w.writeString("\r\n ")
|
||||
return s[i+1:]
|
||||
}
|
||||
if s[i] == '\n' {
|
||||
w.writeString(s[:i+1])
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Too bad, no space or newline in the whole string. Just write everything.
|
||||
w.writeString(s)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeaders(h map[string][]string) {
|
||||
if w.depth == 0 {
|
||||
for k, v := range h {
|
||||
if k != "Bcc" {
|
||||
w.writeHeader(k, v...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.createPart(h)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) {
|
||||
var subWriter io.Writer
|
||||
if w.depth == 0 {
|
||||
w.writeString("\r\n")
|
||||
subWriter = w.w
|
||||
} else {
|
||||
subWriter = w.partWriter
|
||||
}
|
||||
|
||||
if enc == Base64 {
|
||||
wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
|
||||
w.err = f(wc)
|
||||
wc.Close()
|
||||
} else if enc == Unencoded {
|
||||
w.err = f(subWriter)
|
||||
} else {
|
||||
wc := newQPWriter(subWriter)
|
||||
w.err = f(wc)
|
||||
wc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// As required by RFC 2045, 6.7. (page 21) for quoted-printable, and
|
||||
// RFC 2045, 6.8. (page 25) for base64.
|
||||
const maxLineLen = 76
|
||||
|
||||
// base64LineWriter limits text encoded in base64 to 76 characters per line
|
||||
type base64LineWriter struct {
|
||||
w io.Writer
|
||||
lineLen int
|
||||
}
|
||||
|
||||
func newBase64LineWriter(w io.Writer) *base64LineWriter {
|
||||
return &base64LineWriter{w: w}
|
||||
}
|
||||
|
||||
func (w *base64LineWriter) Write(p []byte) (int, error) {
|
||||
n := 0
|
||||
for len(p)+w.lineLen > maxLineLen {
|
||||
w.w.Write(p[:maxLineLen-w.lineLen])
|
||||
w.w.Write([]byte("\r\n"))
|
||||
p = p[maxLineLen-w.lineLen:]
|
||||
n += maxLineLen - w.lineLen
|
||||
w.lineLen = 0
|
||||
}
|
||||
|
||||
w.w.Write(p)
|
||||
w.lineLen += len(p)
|
||||
|
||||
return n + len(p), nil
|
||||
}
|
||||
|
||||
// Stubbed out for testing.
|
||||
var now = time.Now
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Alexandre Cesaro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,16 @@
|
|||
# quotedprintable
|
||||
|
||||
## Introduction
|
||||
|
||||
Package quotedprintable implements quoted-printable and message header encoding
|
||||
as specified by RFC 2045 and RFC 2047.
|
||||
|
||||
It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes
|
||||
the new functions of package `mime` concerning RFC 2047.
|
||||
|
||||
This code has minor changes with the standard library code in order to work
|
||||
with Go 1.0 and newer.
|
||||
|
||||
## Documentation
|
||||
|
||||
https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3
|
|
@ -0,0 +1,279 @@
|
|||
package quotedprintable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A WordEncoder is a RFC 2047 encoded-word encoder.
|
||||
type WordEncoder byte
|
||||
|
||||
const (
|
||||
// BEncoding represents Base64 encoding scheme as defined by RFC 2045.
|
||||
BEncoding = WordEncoder('b')
|
||||
// QEncoding represents the Q-encoding scheme as defined by RFC 2047.
|
||||
QEncoding = WordEncoder('q')
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word")
|
||||
)
|
||||
|
||||
// Encode returns the encoded-word form of s. If s is ASCII without special
|
||||
// characters, it is returned unchanged. The provided charset is the IANA
|
||||
// charset name of s. It is case insensitive.
|
||||
func (e WordEncoder) Encode(charset, s string) string {
|
||||
if !needsEncoding(s) {
|
||||
return s
|
||||
}
|
||||
return e.encodeWord(charset, s)
|
||||
}
|
||||
|
||||
func needsEncoding(s string) bool {
|
||||
for _, b := range s {
|
||||
if (b < ' ' || b > '~') && b != '\t' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// encodeWord encodes a string into an encoded-word.
|
||||
func (e WordEncoder) encodeWord(charset, s string) string {
|
||||
buf := getBuffer()
|
||||
defer putBuffer(buf)
|
||||
|
||||
buf.WriteString("=?")
|
||||
buf.WriteString(charset)
|
||||
buf.WriteByte('?')
|
||||
buf.WriteByte(byte(e))
|
||||
buf.WriteByte('?')
|
||||
|
||||
if e == BEncoding {
|
||||
w := base64.NewEncoder(base64.StdEncoding, buf)
|
||||
io.WriteString(w, s)
|
||||
w.Close()
|
||||
} else {
|
||||
enc := make([]byte, 3)
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case b == ' ':
|
||||
buf.WriteByte('_')
|
||||
case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_':
|
||||
buf.WriteByte(b)
|
||||
default:
|
||||
enc[0] = '='
|
||||
enc[1] = upperhex[b>>4]
|
||||
enc[2] = upperhex[b&0x0f]
|
||||
buf.Write(enc)
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteString("?=")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
const upperhex = "0123456789ABCDEF"
|
||||
|
||||
// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
|
||||
type WordDecoder struct {
|
||||
// CharsetReader, if non-nil, defines a function to generate
|
||||
// charset-conversion readers, converting from the provided
|
||||
// charset into UTF-8.
|
||||
// Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets
|
||||
// are handled by default.
|
||||
// One of the the CharsetReader's result values must be non-nil.
|
||||
CharsetReader func(charset string, input io.Reader) (io.Reader, error)
|
||||
}
|
||||
|
||||
// Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word,
|
||||
// word is returned unchanged.
|
||||
func (d *WordDecoder) Decode(word string) (string, error) {
|
||||
fields := strings.Split(word, "?") // TODO: remove allocation?
|
||||
if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 {
|
||||
return "", errInvalidWord
|
||||
}
|
||||
|
||||
content, err := decode(fields[2][0], fields[3])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := getBuffer()
|
||||
defer putBuffer(buf)
|
||||
|
||||
if err := d.convert(buf, fields[1], content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// DecodeHeader decodes all encoded-words of the given string. It returns an
|
||||
// error if and only if CharsetReader of d returns an error.
|
||||
func (d *WordDecoder) DecodeHeader(header string) (string, error) {
|
||||
// If there is no encoded-word, returns before creating a buffer.
|
||||
i := strings.Index(header, "=?")
|
||||
if i == -1 {
|
||||
return header, nil
|
||||
}
|
||||
|
||||
buf := getBuffer()
|
||||
defer putBuffer(buf)
|
||||
|
||||
buf.WriteString(header[:i])
|
||||
header = header[i:]
|
||||
|
||||
betweenWords := false
|
||||
for {
|
||||
start := strings.Index(header, "=?")
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
cur := start + len("=?")
|
||||
|
||||
i := strings.Index(header[cur:], "?")
|
||||
if i == -1 {
|
||||
break
|
||||
}
|
||||
charset := header[cur : cur+i]
|
||||
cur += i + len("?")
|
||||
|
||||
if len(header) < cur+len("Q??=") {
|
||||
break
|
||||
}
|
||||
encoding := header[cur]
|
||||
cur++
|
||||
|
||||
if header[cur] != '?' {
|
||||
break
|
||||
}
|
||||
cur++
|
||||
|
||||
j := strings.Index(header[cur:], "?=")
|
||||
if j == -1 {
|
||||
break
|
||||
}
|
||||
text := header[cur : cur+j]
|
||||
end := cur + j + len("?=")
|
||||
|
||||
content, err := decode(encoding, text)
|
||||
if err != nil {
|
||||
betweenWords = false
|
||||
buf.WriteString(header[:start+2])
|
||||
header = header[start+2:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Write characters before the encoded-word. White-space and newline
|
||||
// characters separating two encoded-words must be deleted.
|
||||
if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) {
|
||||
buf.WriteString(header[:start])
|
||||
}
|
||||
|
||||
if err := d.convert(buf, charset, content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
header = header[end:]
|
||||
betweenWords = true
|
||||
}
|
||||
|
||||
if len(header) > 0 {
|
||||
buf.WriteString(header)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func decode(encoding byte, text string) ([]byte, error) {
|
||||
switch encoding {
|
||||
case 'B', 'b':
|
||||
return base64.StdEncoding.DecodeString(text)
|
||||
case 'Q', 'q':
|
||||
return qDecode(text)
|
||||
}
|
||||
return nil, errInvalidWord
|
||||
}
|
||||
|
||||
func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error {
|
||||
switch {
|
||||
case strings.EqualFold("utf-8", charset):
|
||||
buf.Write(content)
|
||||
case strings.EqualFold("iso-8859-1", charset):
|
||||
for _, c := range content {
|
||||
buf.WriteRune(rune(c))
|
||||
}
|
||||
case strings.EqualFold("us-ascii", charset):
|
||||
for _, c := range content {
|
||||
if c >= utf8.RuneSelf {
|
||||
buf.WriteRune(unicode.ReplacementChar)
|
||||
} else {
|
||||
buf.WriteByte(c)
|
||||
}
|
||||
}
|
||||
default:
|
||||
if d.CharsetReader == nil {
|
||||
return fmt.Errorf("mime: unhandled charset %q", charset)
|
||||
}
|
||||
r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = buf.ReadFrom(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least
|
||||
// one byte of non-whitespace.
|
||||
func hasNonWhitespace(s string) bool {
|
||||
for _, b := range s {
|
||||
switch b {
|
||||
// Encoded-words can only be separated by linear white spaces which does
|
||||
// not include vertical tabs (\v).
|
||||
case ' ', '\t', '\n', '\r':
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// qDecode decodes a Q encoded string.
|
||||
func qDecode(s string) ([]byte, error) {
|
||||
dec := make([]byte, len(s))
|
||||
n := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch c := s[i]; {
|
||||
case c == '_':
|
||||
dec[n] = ' '
|
||||
case c == '=':
|
||||
if i+2 >= len(s) {
|
||||
return nil, errInvalidWord
|
||||
}
|
||||
b, err := readHexByte(s[i+1], s[i+2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dec[n] = b
|
||||
i += 2
|
||||
case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t':
|
||||
dec[n] = c
|
||||
default:
|
||||
return nil, errInvalidWord
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
return dec[:n], nil
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// +build go1.3
|
||||
|
||||
package quotedprintable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func getBuffer() *bytes.Buffer {
|
||||
return bufPool.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
func putBuffer(buf *bytes.Buffer) {
|
||||
if buf.Len() > 1024 {
|
||||
return
|
||||
}
|
||||
buf.Reset()
|
||||
bufPool.Put(buf)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// +build !go1.3
|
||||
|
||||
package quotedprintable
|
||||
|
||||
import "bytes"
|
||||
|
||||
var ch = make(chan *bytes.Buffer, 32)
|
||||
|
||||
func getBuffer() *bytes.Buffer {
|
||||
select {
|
||||
case buf := <-ch:
|
||||
return buf
|
||||
default:
|
||||
}
|
||||
return new(bytes.Buffer)
|
||||
}
|
||||
|
||||
func putBuffer(buf *bytes.Buffer) {
|
||||
buf.Reset()
|
||||
select {
|
||||
case ch <- buf:
|
||||
default:
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
// Package quotedprintable implements quoted-printable encoding as specified by
|
||||
// RFC 2045.
|
||||
package quotedprintable
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Reader is a quoted-printable decoder.
|
||||
type Reader struct {
|
||||
br *bufio.Reader
|
||||
rerr error // last read error
|
||||
line []byte // to be consumed before more of br
|
||||
}
|
||||
|
||||
// NewReader returns a quoted-printable reader, decoding from r.
|
||||
func NewReader(r io.Reader) *Reader {
|
||||
return &Reader{
|
||||
br: bufio.NewReader(r),
|
||||
}
|
||||
}
|
||||
|
||||
func fromHex(b byte) (byte, error) {
|
||||
switch {
|
||||
case b >= '0' && b <= '9':
|
||||
return b - '0', nil
|
||||
case b >= 'A' && b <= 'F':
|
||||
return b - 'A' + 10, nil
|
||||
// Accept badly encoded bytes.
|
||||
case b >= 'a' && b <= 'f':
|
||||
return b - 'a' + 10, nil
|
||||
}
|
||||
return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b)
|
||||
}
|
||||
|
||||
func readHexByte(a, b byte) (byte, error) {
|
||||
var hb, lb byte
|
||||
var err error
|
||||
if hb, err = fromHex(a); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if lb, err = fromHex(b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return hb<<4 | lb, nil
|
||||
}
|
||||
|
||||
func isQPDiscardWhitespace(r rune) bool {
|
||||
switch r {
|
||||
case '\n', '\r', ' ', '\t':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
crlf = []byte("\r\n")
|
||||
lf = []byte("\n")
|
||||
softSuffix = []byte("=")
|
||||
)
|
||||
|
||||
// Read reads and decodes quoted-printable data from the underlying reader.
|
||||
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||
// Deviations from RFC 2045:
|
||||
// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
|
||||
// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
|
||||
// with other broken QP encoders & decoders.
|
||||
for len(p) > 0 {
|
||||
if len(r.line) == 0 {
|
||||
if r.rerr != nil {
|
||||
return n, r.rerr
|
||||
}
|
||||
r.line, r.rerr = r.br.ReadSlice('\n')
|
||||
|
||||
// Does the line end in CRLF instead of just LF?
|
||||
hasLF := bytes.HasSuffix(r.line, lf)
|
||||
hasCR := bytes.HasSuffix(r.line, crlf)
|
||||
wholeLine := r.line
|
||||
r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
|
||||
if bytes.HasSuffix(r.line, softSuffix) {
|
||||
rightStripped := wholeLine[len(r.line):]
|
||||
r.line = r.line[:len(r.line)-1]
|
||||
if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
|
||||
r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped)
|
||||
}
|
||||
} else if hasLF {
|
||||
if hasCR {
|
||||
r.line = append(r.line, '\r', '\n')
|
||||
} else {
|
||||
r.line = append(r.line, '\n')
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
b := r.line[0]
|
||||
|
||||
switch {
|
||||
case b == '=':
|
||||
if len(r.line[1:]) < 2 {
|
||||
return n, io.ErrUnexpectedEOF
|
||||
}
|
||||
b, err = readHexByte(r.line[1], r.line[2])
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
r.line = r.line[2:] // 2 of the 3; other 1 is done below
|
||||
case b == '\t' || b == '\r' || b == '\n':
|
||||
break
|
||||
case b < ' ' || b > '~':
|
||||
return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b)
|
||||
}
|
||||
p[0] = b
|
||||
p = p[1:]
|
||||
r.line = r.line[1:]
|
||||
n++
|
||||
}
|
||||
return n, nil
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package quotedprintable
|
||||
|
||||
import "io"
|
||||
|
||||
const lineMaxLen = 76
|
||||
|
||||
// A Writer is a quoted-printable writer that implements io.WriteCloser.
|
||||
type Writer struct {
|
||||
// Binary mode treats the writer's input as pure binary and processes end of
|
||||
// line bytes as binary data.
|
||||
Binary bool
|
||||
|
||||
w io.Writer
|
||||
i int
|
||||
line [78]byte
|
||||
cr bool
|
||||
}
|
||||
|
||||
// NewWriter returns a new Writer that writes to w.
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{w: w}
|
||||
}
|
||||
|
||||
// Write encodes p using quoted-printable encoding and writes it to the
|
||||
// underlying io.Writer. It limits line length to 76 characters. The encoded
|
||||
// bytes are not necessarily flushed until the Writer is closed.
|
||||
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||
for i, b := range p {
|
||||
switch {
|
||||
// Simple writes are done in batch.
|
||||
case b >= '!' && b <= '~' && b != '=':
|
||||
continue
|
||||
case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'):
|
||||
continue
|
||||
}
|
||||
|
||||
if i > n {
|
||||
if err := w.write(p[n:i]); err != nil {
|
||||
return n, err
|
||||
}
|
||||
n = i
|
||||
}
|
||||
|
||||
if err := w.encode(b); err != nil {
|
||||
return n, err
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
if n == len(p) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if err := w.write(p[n:]); err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Close closes the Writer, flushing any unwritten data to the underlying
|
||||
// io.Writer, but does not close the underlying io.Writer.
|
||||
func (w *Writer) Close() error {
|
||||
if err := w.checkLastByte(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.flush()
|
||||
}
|
||||
|
||||
// write limits text encoded in quoted-printable to 76 characters per line.
|
||||
func (w *Writer) write(p []byte) error {
|
||||
for _, b := range p {
|
||||
if b == '\n' || b == '\r' {
|
||||
// If the previous byte was \r, the CRLF has already been inserted.
|
||||
if w.cr && b == '\n' {
|
||||
w.cr = false
|
||||
continue
|
||||
}
|
||||
|
||||
if b == '\r' {
|
||||
w.cr = true
|
||||
}
|
||||
|
||||
if err := w.checkLastByte(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.insertCRLF(); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if w.i == lineMaxLen-1 {
|
||||
if err := w.insertSoftLineBreak(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.line[w.i] = b
|
||||
w.i++
|
||||
w.cr = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Writer) encode(b byte) error {
|
||||
if lineMaxLen-1-w.i < 3 {
|
||||
if err := w.insertSoftLineBreak(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.line[w.i] = '='
|
||||
w.line[w.i+1] = upperhex[b>>4]
|
||||
w.line[w.i+2] = upperhex[b&0x0f]
|
||||
w.i += 3
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkLastByte encodes the last buffered byte if it is a space or a tab.
|
||||
func (w *Writer) checkLastByte() error {
|
||||
if w.i == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
b := w.line[w.i-1]
|
||||
if isWhitespace(b) {
|
||||
w.i--
|
||||
if err := w.encode(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Writer) insertSoftLineBreak() error {
|
||||
w.line[w.i] = '='
|
||||
w.i++
|
||||
|
||||
return w.insertCRLF()
|
||||
}
|
||||
|
||||
func (w *Writer) insertCRLF() error {
|
||||
w.line[w.i] = '\r'
|
||||
w.line[w.i+1] = '\n'
|
||||
w.i += 2
|
||||
|
||||
return w.flush()
|
||||
}
|
||||
|
||||
func (w *Writer) flush() error {
|
||||
if _, err := w.w.Write(w.line[:w.i]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.i = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func isWhitespace(b byte) bool {
|
||||
return b == ' ' || b == '\t'
|
||||
}
|
|
@ -56,6 +56,12 @@
|
|||
"revision": "34aa9bcfff225db756df097fc49ffd35b68412bb",
|
||||
"revisionTime": "2016-05-29T15:11:07Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "DB+/DpKJUO7dR+MyQVchJ+yyHoI=",
|
||||
"path": "github.com/go-gomail/gomail",
|
||||
"revision": "81ebce5c23dfd25c6c67194b37d3dd3f338c98b1",
|
||||
"revisionTime": "2016-04-11T21:29:32Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "OkqfwXeTVoiIxNMDA7HKvmrCDw8=",
|
||||
"path": "github.com/go-macaron/binding",
|
||||
|
@ -144,6 +150,12 @@
|
|||
"revision": "ffcf1bedda3b04ebb15a168a59800a73d6dc0f4d",
|
||||
"revisionTime": "2017-03-29T01:43:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "6IzzHO9p32aHJhMYMwijccDUIVA=",
|
||||
"path": "gopkg.in/alexcesaro/quotedprintable.v3",
|
||||
"revision": "2caba252f4dc53eaf6b553000885530023f54623",
|
||||
"revisionTime": "2015-07-16T17:19:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "v5M2URDb0vJwelofRjiIsml0Jts=",
|
||||
"path": "gopkg.in/ini.v1",
|
||||
|
@ -165,6 +177,10 @@
|
|||
{
|
||||
"path": "https://code.google.com/p/mahonia",
|
||||
"revision": ""
|
||||
},
|
||||
{
|
||||
"path": "https://github.com/go-gomail/gomail",
|
||||
"revision": ""
|
||||
}
|
||||
],
|
||||
"rootPath": "github.com/ouqiang/gocron"
|
||||
|
|
Loading…
Reference in New Issue