增加邮件通知

pull/21/merge
ouqiang 2017-05-01 01:12:07 +08:00
parent 9a7b53c031
commit a7977894d6
41 changed files with 7361 additions and 83 deletions

View File

@ -3,7 +3,7 @@
## 功能特性
* 定时任务统一调度和管理
* 支持任务CURD
* 支持任务CURD
* crontab时间表达式支持秒级任务定义
* 任务执行失败重试设置
* 任务超时设置

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
// 更新

View File

@ -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()
}
}

84
modules/notify/mail.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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{"&amp;", "&lt;", "&gt;"}
content = utils.ReplaceStrings(content, specialChars, replaceChars)
return fmt.Sprintf(`{"text":"%s","username":"监控"}`, content)
return fmt.Sprintf(`{"text":"%s","username":"监控", "channel":"%s"}`, content, channel)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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;
}

View File

@ -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",

View File

@ -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

View File

@ -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 {

View File

@ -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,

View File

@ -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 {

192
templates/setting/mail.html Normal file
View File

@ -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}}}&nbsp;&nbsp;&nbsp;<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" . }}}

View File

@ -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>

View File

@ -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}}}&nbsp;&nbsp;&nbsp;<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" . }}}

View File

@ -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"
});

20
vendor/github.com/go-gomail/gomail/CHANGELOG.md generated vendored Normal file
View File

@ -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.

20
vendor/github.com/go-gomail/gomail/CONTRIBUTING.md generated vendored Normal file
View File

@ -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.

20
vendor/github.com/go-gomail/gomail/LICENSE generated vendored Normal file
View File

@ -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.

92
vendor/github.com/go-gomail/gomail/README.md generated vendored Normal file
View File

@ -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.

49
vendor/github.com/go-gomail/gomail/auth.go generated vendored Normal file
View File

@ -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)
}
}

5
vendor/github.com/go-gomail/gomail/doc.go generated vendored Normal file
View File

@ -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

322
vendor/github.com/go-gomail/gomail/message.go generated vendored Normal file
View File

@ -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)
}

21
vendor/github.com/go-gomail/gomail/mime.go generated vendored Normal file
View File

@ -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
)

25
vendor/github.com/go-gomail/gomail/mime_go14.go generated vendored Normal file
View File

@ -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
}
)

116
vendor/github.com/go-gomail/gomail/send.go generated vendored Normal file
View File

@ -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
}

202
vendor/github.com/go-gomail/gomail/smtp.go generated vendored Normal file
View File

@ -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
}

306
vendor/github.com/go-gomail/gomail/writeto.go generated vendored Normal file
View File

@ -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

20
vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE generated vendored Normal file
View File

@ -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.

View File

@ -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

View File

@ -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
}

26
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go generated vendored Normal file
View File

@ -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)
}

View File

@ -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:
}
}

121
vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go generated vendored Normal file
View File

@ -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
}

166
vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go generated vendored Normal file
View File

@ -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'
}

16
vendor/vendor.json vendored
View File

@ -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"