From 5beb06b2bd9c0726187c70d767a03e363b7204e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E8=B6=85=E8=B6=8A?= <993921@qq.com>
Date: Sun, 16 Jan 2022 11:29:01 +0800
Subject: [PATCH 1/4] feat: notification; support dingtalk robot AND email
---
conf/app.conf | 10 +-
go.mod | 3 +
go.sum | 6 +
internal/api/pipeline.go | 4 +
.../github.com/go-gomail/gomail/.travis.yml | 9 +
.../github.com/go-gomail/gomail/CHANGELOG.md | 20 ++
.../go-gomail/gomail/CONTRIBUTING.md | 20 ++
vendor/github.com/go-gomail/gomail/LICENSE | 20 ++
vendor/github.com/go-gomail/gomail/README.md | 92 +++++
vendor/github.com/go-gomail/gomail/auth.go | 49 +++
vendor/github.com/go-gomail/gomail/doc.go | 5 +
vendor/github.com/go-gomail/gomail/message.go | 322 ++++++++++++++++++
vendor/github.com/go-gomail/gomail/mime.go | 21 ++
.../github.com/go-gomail/gomail/mime_go14.go | 25 ++
vendor/github.com/go-gomail/gomail/send.go | 116 +++++++
vendor/github.com/go-gomail/gomail/smtp.go | 202 +++++++++++
vendor/github.com/go-gomail/gomail/writeto.go | 306 +++++++++++++++++
.../alexcesaro/quotedprintable.v3/LICENSE | 20 ++
.../alexcesaro/quotedprintable.v3/README.md | 16 +
.../quotedprintable.v3/encodedword.go | 279 +++++++++++++++
.../alexcesaro/quotedprintable.v3/pool.go | 26 ++
.../quotedprintable.v3/pool_go12.go | 24 ++
.../alexcesaro/quotedprintable.v3/reader.go | 121 +++++++
.../alexcesaro/quotedprintable.v3/writer.go | 166 +++++++++
vendor/modules.txt | 10 +-
25 files changed, 1889 insertions(+), 3 deletions(-)
create mode 100644 vendor/github.com/go-gomail/gomail/.travis.yml
create mode 100644 vendor/github.com/go-gomail/gomail/CHANGELOG.md
create mode 100644 vendor/github.com/go-gomail/gomail/CONTRIBUTING.md
create mode 100644 vendor/github.com/go-gomail/gomail/LICENSE
create mode 100644 vendor/github.com/go-gomail/gomail/README.md
create mode 100644 vendor/github.com/go-gomail/gomail/auth.go
create mode 100644 vendor/github.com/go-gomail/gomail/doc.go
create mode 100644 vendor/github.com/go-gomail/gomail/message.go
create mode 100644 vendor/github.com/go-gomail/gomail/mime.go
create mode 100644 vendor/github.com/go-gomail/gomail/mime_go14.go
create mode 100644 vendor/github.com/go-gomail/gomail/send.go
create mode 100644 vendor/github.com/go-gomail/gomail/smtp.go
create mode 100644 vendor/github.com/go-gomail/gomail/writeto.go
create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
create mode 100644 vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
diff --git a/conf/app.conf b/conf/app.conf
index c5b5d904..22786c17 100644
--- a/conf/app.conf
+++ b/conf/app.conf
@@ -41,4 +41,12 @@ configPath = ./conf/k8sconfig
# build/deploy callback
[atomci]
-url = http://localhost:8080
\ No newline at end of file
+url = http://localhost:8080
+
+# notification config
+[notification]
+ding = "https://oapi.dingtalk.com/robot/send?access_token=faketoken"
+smtpHost = "smtp.host"
+smtpPort = 465
+smtpAccount = "fake@mail.com"
+smtpPassword = "pwd"
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 16a59007..62cd1c4a 100644
--- a/go.mod
+++ b/go.mod
@@ -38,6 +38,7 @@ require (
github.com/ghodss/yaml v1.0.0
github.com/go-atomci/go-scm v1.13.2-0.20210629010829-147be8a9bdd3
github.com/go-atomci/workflow v0.0.0-20211126090842-208f180b47ab
+ github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/go-sql-driver/mysql v1.5.0
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/go-cmp v0.5.5 // indirect
@@ -57,7 +58,9 @@ require (
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
golang.org/x/text v0.3.5 // indirect
google.golang.org/appengine v1.6.7 // indirect
+ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
+ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/api v0.18.0
diff --git a/go.sum b/go.sum
index f76da852..aa1942e7 100644
--- a/go.sum
+++ b/go.sum
@@ -199,6 +199,8 @@ github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+s
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E=
+github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.2.1 h1:mbP3BPfsULz5DuI3ejHuAypAbcg38Xv5T7eEHp3+XAE=
github.com/go-ldap/ldap/v3 v3.2.1/go.mod h1:phWI+JSJ/eGvABjJxU7bT7CBv03KfS0e16+bQxLtjMw=
@@ -1013,6 +1015,8 @@ google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1024,6 +1028,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
diff --git a/internal/api/pipeline.go b/internal/api/pipeline.go
index e25f5d1f..9dc74875 100644
--- a/internal/api/pipeline.go
+++ b/internal/api/pipeline.go
@@ -17,6 +17,7 @@ limitations under the License.
package api
import (
+ "github.com/go-atomci/atomci/internal/core/notification/impl"
"github.com/go-atomci/atomci/internal/core/pipelinemgr"
"github.com/go-atomci/atomci/internal/core/publish"
"github.com/go-atomci/atomci/internal/middleware/log"
@@ -118,6 +119,9 @@ func (p *PipelineController) RunStepCallback() {
log.Log.Error("RunStep callback, update publish Order occur error: %s", err.Error())
return
}
+
+ go notification.Send(publishID, publishStatus)
+
p.Data["json"] = NewResult(true, nil, "")
p.ServeJSON()
}
diff --git a/vendor/github.com/go-gomail/gomail/.travis.yml b/vendor/github.com/go-gomail/gomail/.travis.yml
new file mode 100644
index 00000000..48915e73
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/.travis.yml
@@ -0,0 +1,9 @@
+language: go
+
+go:
+ - 1.2
+ - 1.3
+ - 1.4
+ - 1.5
+ - 1.6
+ - tip
diff --git a/vendor/github.com/go-gomail/gomail/CHANGELOG.md b/vendor/github.com/go-gomail/gomail/CHANGELOG.md
new file mode 100644
index 00000000..a797ab4c
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/CHANGELOG.md
@@ -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.
diff --git a/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md b/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md
new file mode 100644
index 00000000..d5601c25
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md
@@ -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.
diff --git a/vendor/github.com/go-gomail/gomail/LICENSE b/vendor/github.com/go-gomail/gomail/LICENSE
new file mode 100644
index 00000000..5f5c12af
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/LICENSE
@@ -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.
diff --git a/vendor/github.com/go-gomail/gomail/README.md b/vendor/github.com/go-gomail/gomail/README.md
new file mode 100644
index 00000000..b3be9e14
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/README.md
@@ -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.
diff --git a/vendor/github.com/go-gomail/gomail/auth.go b/vendor/github.com/go-gomail/gomail/auth.go
new file mode 100644
index 00000000..d28b83ab
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/auth.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/go-gomail/gomail/doc.go b/vendor/github.com/go-gomail/gomail/doc.go
new file mode 100644
index 00000000..a8f5091f
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/doc.go
@@ -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
diff --git a/vendor/github.com/go-gomail/gomail/message.go b/vendor/github.com/go-gomail/gomail/message.go
new file mode 100644
index 00000000..4bffb1e7
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/message.go
@@ -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)
+}
diff --git a/vendor/github.com/go-gomail/gomail/mime.go b/vendor/github.com/go-gomail/gomail/mime.go
new file mode 100644
index 00000000..194d4a76
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/mime.go
@@ -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
+)
diff --git a/vendor/github.com/go-gomail/gomail/mime_go14.go b/vendor/github.com/go-gomail/gomail/mime_go14.go
new file mode 100644
index 00000000..3dc26aa2
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/mime_go14.go
@@ -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
+ }
+)
diff --git a/vendor/github.com/go-gomail/gomail/send.go b/vendor/github.com/go-gomail/gomail/send.go
new file mode 100644
index 00000000..9115ebe7
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/send.go
@@ -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
+}
diff --git a/vendor/github.com/go-gomail/gomail/smtp.go b/vendor/github.com/go-gomail/gomail/smtp.go
new file mode 100644
index 00000000..2aa49c8b
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/smtp.go
@@ -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
+}
diff --git a/vendor/github.com/go-gomail/gomail/writeto.go b/vendor/github.com/go-gomail/gomail/writeto.go
new file mode 100644
index 00000000..9fb6b86e
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/writeto.go
@@ -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
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
new file mode 100644
index 00000000..5f5c12af
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
@@ -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.
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
new file mode 100644
index 00000000..98ddf829
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
@@ -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
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
new file mode 100644
index 00000000..cfd02617
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
@@ -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
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
new file mode 100644
index 00000000..24283c52
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
@@ -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)
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
new file mode 100644
index 00000000..d335b4ab
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
@@ -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:
+ }
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
new file mode 100644
index 00000000..955edca2
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
@@ -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
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
new file mode 100644
index 00000000..43359d51
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
@@ -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'
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index b368cd4f..3b43efae 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -57,6 +57,9 @@ github.com/go-atomci/go-scm/scm/transport
github.com/go-atomci/workflow
github.com/go-atomci/workflow/jenkins
github.com/go-atomci/workflow/jenkins/templates
+# github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
+## explicit
+github.com/go-gomail/gomail
# github.com/go-ldap/ldap/v3 v3.2.1
github.com/go-ldap/ldap/v3
# github.com/go-sql-driver/mysql v1.5.0
@@ -117,8 +120,6 @@ github.com/pmezard/go-difflib/difflib
# github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644
## explicit
github.com/shiena/ansicolor
-# github.com/spf13/cobra v1.0.0
-## explicit
# github.com/spf13/pflag v1.0.5
github.com/spf13/pflag
# github.com/stretchr/testify v1.7.0
@@ -200,8 +201,13 @@ google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/types/known/anypb
google.golang.org/protobuf/types/known/durationpb
google.golang.org/protobuf/types/known/timestamppb
+# gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc
+## explicit
+gopkg.in/alexcesaro/quotedprintable.v3
# gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b
## explicit
+# gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
+## explicit
# gopkg.in/inf.v0 v0.9.1
gopkg.in/inf.v0
# gopkg.in/yaml.v2 v2.4.0
From 8352279f1466d1f22359f11bcdf0913a61aa7a0d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E8=B6=85=E8=B6=8A?= <993921@qq.com>
Date: Sun, 16 Jan 2022 12:03:53 +0800
Subject: [PATCH 2/4] feat: notification; support dingtalk robot AND email
---
README.md | 7 ++
internal/core/notification/impl/dingrobot.go | 88 +++++++++++++++++++
internal/core/notification/impl/email.go | 78 ++++++++++++++++
internal/core/notification/impl/notify.go | 51 +++++++++++
.../core/notification/impl/notifyTemplate.go | 75 ++++++++++++++++
internal/core/notification/types/messages.go | 79 +++++++++++++++++
6 files changed, 378 insertions(+)
create mode 100644 internal/core/notification/impl/dingrobot.go
create mode 100644 internal/core/notification/impl/email.go
create mode 100644 internal/core/notification/impl/notify.go
create mode 100644 internal/core/notification/impl/notifyTemplate.go
create mode 100644 internal/core/notification/types/messages.go
diff --git a/README.md b/README.md
index fb8b3312..671c6140 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,13 @@ AtomCI 致力于让中小企业快速落地Kubernetes,代码均已开源, __
# conf/app.conf
[DB]
url = root:root@tcp(127.0.0.1:3306)/atomci?charset=utf8mb4
+
+[notification]
+ding = 钉钉机器人
+smtpHost = SMTP服务器
+smtpPort = 465
+smtpAccount = 邮件账号
+smtpPassword = 邮件密码
```
> 注: 对于`[ldap]`,`[jwt]`, `[atomci]`可以参照附录-『配置说明』进行修改
diff --git a/internal/core/notification/impl/dingrobot.go b/internal/core/notification/impl/dingrobot.go
new file mode 100644
index 00000000..c7fea613
--- /dev/null
+++ b/internal/core/notification/impl/dingrobot.go
@@ -0,0 +1,88 @@
+package notification
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+
+ "github.com/astaxie/beego"
+ messages "github.com/go-atomci/atomci/internal/core/notification/types"
+)
+
+type DingRobot struct{}
+
+func DingRobotHandler() INotify {
+ notifyHandler := &DingRobot{}
+ return notifyHandler
+}
+
+func dingEventMessage(template INotifyTemplate, result messages.StepCallbackResult) messages.EventMessage {
+
+ robotHost := beego.AppConfig.String("notification::ding")
+
+ var buf bytes.Buffer
+ template.GenSubject(&buf, result)
+ template.GenSubject(&buf, result)
+ template.GenFooter(&buf, result)
+
+ markdownText := &messages.MarkdownMessage{
+ MsgType: messages.MarkDown,
+ At: messages.MessageAt{
+ AtMobiles: []string{},
+ IsAtAll: false,
+ },
+ MarkdownText: messages.MessageMarkdown{
+ Title: "流水线通知",
+ Text: buf.String(),
+ },
+ }
+
+ dingMsg := &messages.DingMessage{
+ RobotHost: []string{robotHost},
+ EventMessage: markdownText,
+ }
+
+ msg := messages.EventMessage{
+ Ding: dingMsg,
+ }
+
+ return msg
+}
+
+func (dingtalk *DingRobot) Send(result messages.StepCallbackResult) error {
+
+ template := &DingRobotMarkdownTemplate{}
+
+ message := dingEventMessage(template, result)
+
+ body, err := json.Marshal(message.Ding.EventMessage)
+ if err != nil {
+ return fmt.Errorf("序列化消息失败 err:%s message:%+v", err, message)
+ }
+
+ for _, host := range message.Ding.RobotHost {
+ res, err := http.Post(host, "application/json", bytes.NewBuffer(body))
+ if err != nil {
+ fmt.Println("钉钉消息发送失败 ", err)
+ return fmt.Errorf("钉钉消息发送失败 err:%s", err)
+ }
+ if res != nil && res.Body != nil {
+ defer res.Body.Close()
+ content, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return fmt.Errorf("读取钉钉响应失败 err:%s", err)
+ }
+ if res.StatusCode == http.StatusFound {
+ return fmt.Errorf("机器人可能已被限流 请注意发送频率不要过高 会自行恢复")
+ }
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("钉钉消息发送失败 err:%s", content)
+ }
+ return nil
+ }
+ }
+
+ return fmt.Errorf("钉钉响应为空")
+}
diff --git a/internal/core/notification/impl/email.go b/internal/core/notification/impl/email.go
new file mode 100644
index 00000000..28691927
--- /dev/null
+++ b/internal/core/notification/impl/email.go
@@ -0,0 +1,78 @@
+package notification
+
+import (
+ "bytes"
+ "github.com/astaxie/beego/logs"
+ "strings"
+
+ "github.com/astaxie/beego"
+ messages "github.com/go-atomci/atomci/internal/core/notification/types"
+ "github.com/go-gomail/gomail"
+)
+
+type Email struct{}
+
+func EmailHandler() INotify {
+ notifyHandler := &Email{}
+ return notifyHandler
+}
+
+func emailEventMessage(template INotifyTemplate, result messages.StepCallbackResult) messages.EventMessage {
+
+ smtpHost := beego.AppConfig.String("notification::smtpHost")
+ smtpAccount := beego.AppConfig.String("notification::smtpAccount")
+ smtpPassword := beego.AppConfig.String("notification::smtpPassword")
+ smtpPort, _ := beego.AppConfig.Int("notification::smtpPort")
+
+ var buf bytes.Buffer
+ subject := template.GenSubject(&buf, result)
+ buf.Reset()
+ template.GenContent(&buf, result)
+ template.GenFooter(&buf, result)
+
+ mailMessage := &messages.MailMessage{
+ SmtpPort: smtpPort,
+ SmtpHost: smtpHost,
+ SmtpAccount: smtpAccount,
+ SmtpPassword: smtpPassword,
+ Body: buf.String(),
+ Subject: subject,
+ }
+ msg := messages.EventMessage{
+ Mail: mailMessage,
+ }
+
+ return msg
+}
+
+func (email *Email) Send(result messages.StepCallbackResult) error {
+
+ template := &EmailTemplate{}
+
+ message := emailEventMessage(template, result)
+
+ body := message.Mail.Body
+ body = strings.Replace(body, "\n", "
", -1)
+
+ subject := message.Mail.Subject
+
+ m := gomail.NewMessage()
+ m.SetHeader("From", message.Mail.SmtpAccount)
+ m.SetHeader("To", message.Mail.SmtpAccount)
+ m.SetHeader("Subject", subject)
+ m.SetBody("text/html", body)
+
+ d := gomail.NewDialer(message.Mail.SmtpHost, message.Mail.SmtpPort, message.Mail.SmtpAccount, message.Mail.SmtpPassword)
+
+ defer func() {
+ if r := recover(); r != nil {
+ logs.Error("%v", r)
+ }
+ }()
+
+ if err := d.DialAndSend(m); err != nil {
+ panic(err)
+ }
+
+ return nil
+}
diff --git a/internal/core/notification/impl/notify.go b/internal/core/notification/impl/notify.go
new file mode 100644
index 00000000..69e27979
--- /dev/null
+++ b/internal/core/notification/impl/notify.go
@@ -0,0 +1,51 @@
+package notification
+
+import (
+ "github.com/astaxie/beego"
+ messages "github.com/go-atomci/atomci/internal/core/notification/types"
+ "github.com/go-atomci/atomci/internal/core/publish"
+)
+
+type INotify interface {
+ Send(m messages.StepCallbackResult) error
+}
+
+func NewHandlers() []INotify {
+
+ ding := beego.AppConfig.String("notification::ding")
+ smtp := beego.AppConfig.String("notification::smtpHost")
+
+ var handlers []INotify
+
+ if len(ding) > 0 {
+ _ = append(handlers, DingRobotHandler())
+ }
+
+ if len(smtp) > 0 {
+ _ = append(handlers, EmailHandler())
+ }
+
+ return handlers
+}
+
+func Send(publishId int64, status int64) {
+
+ pm := publish.NewPublishManager()
+
+ pub, _ := pm.GetPublishInfo(publishId)
+
+ handlers := NewHandlers()
+
+ callbackResult := messages.StepCallbackResult{
+ PublishName: pub.Name,
+ StageName: pub.StageName,
+ StepName: pub.Step,
+ Status: status,
+ }
+
+ if handlers != nil {
+ for _, handler := range handlers {
+ go handler.Send(callbackResult)
+ }
+ }
+}
diff --git a/internal/core/notification/impl/notifyTemplate.go b/internal/core/notification/impl/notifyTemplate.go
new file mode 100644
index 00000000..abe200ed
--- /dev/null
+++ b/internal/core/notification/impl/notifyTemplate.go
@@ -0,0 +1,75 @@
+package notification
+
+import (
+ "bytes"
+
+ messages "github.com/go-atomci/atomci/internal/core/notification/types"
+)
+
+type INotifyTemplate interface {
+ GenSubject(buf *bytes.Buffer, m messages.StepCallbackResult) string
+ GenContent(buf *bytes.Buffer, m messages.StepCallbackResult) string
+ GenFooter(buf *bytes.Buffer, m messages.StepCallbackResult) string
+}
+
+type DingRobotMarkdownTemplate struct{}
+
+func (temp *DingRobotMarkdownTemplate) GenSubject(buf *bytes.Buffer, m messages.StepCallbackResult) string {
+
+ buf.WriteString("## ")
+ buf.WriteString(messages.StatusCodeToChinese(m.Status))
+
+ return buf.String()
+}
+
+func (temp *DingRobotMarkdownTemplate) GenContent(buf *bytes.Buffer, m messages.StepCallbackResult) string {
+
+ buf.WriteString(m.PublishName)
+ buf.WriteString("\r\n\r\n")
+ buf.WriteString(m.StageName)
+ buf.WriteString("\r\n\r\n")
+ buf.WriteString(m.StepName)
+
+ return buf.String()
+}
+
+func (temp *DingRobotMarkdownTemplate) GenFooter(buf *bytes.Buffer, m messages.StepCallbackResult) string {
+
+ buf.WriteString("\r\n\r\n> by AtomCI")
+
+ return buf.String()
+}
+
+type EmailTemplate struct{}
+
+func (temp *EmailTemplate) GenSubject(buf *bytes.Buffer, m messages.StepCallbackResult) string {
+
+ buf.WriteString("流水线")
+ buf.WriteString(m.PublishName)
+ buf.WriteString("构建")
+ buf.WriteString(messages.StatusCodeToChinese(m.Status))
+
+ return buf.String()
+}
+
+func (temp *EmailTemplate) GenContent(buf *bytes.Buffer, m messages.StepCallbackResult) string {
+
+ buf.WriteString("
by AtomCI
") + + return buf.String() +} diff --git a/internal/core/notification/types/messages.go b/internal/core/notification/types/messages.go new file mode 100644 index 00000000..9fbaaf68 --- /dev/null +++ b/internal/core/notification/types/messages.go @@ -0,0 +1,79 @@ +package types + +import ( + "fmt" + "github.com/go-atomci/atomci/internal/models" +) + +const ( + Text MessageType = "text" + MarkDown MessageType = "markdown" +) + +type MessageAt struct { + AtMobiles []string `json:"atmobiles"` + IsAtAll bool `json:"isAtAll"` +} + +type MessageType string + +type MessageContent struct { + Content string `json:"content"` +} + +type MessageMarkdown struct { + Title string `json:"title"` + Text string `json:"text"` +} + +// PlainMessage 消息格式,钉钉专用 +type PlainMessage struct { + MsgType MessageType `json:"msgtype"` + At MessageAt `json:"at"` + Text MessageContent `json:"text"` +} + +// MarkdownMessage 消息格式,钉钉专用 +type MarkdownMessage struct { + MsgType MessageType `json:"msgtype"` + At MessageAt `json:"at"` + MarkdownText MessageMarkdown `json:"markdown"` +} + +// MailMessage 邮件消息 +type MailMessage struct { + SmtpPort int + SmtpHost string + SmtpAccount string + SmtpPassword string + Body string + Subject string +} + +type DingMessage struct { + RobotHost []string + EventMessage interface{} +} + +type EventMessage struct { + Mail *MailMessage + Ding *DingMessage +} + +type StepCallbackResult struct { + StageName string + PublishName string + StepName string + Status int64 +} + +func StatusCodeToChinese(status int64) string { + switch status { + case models.Success: + return "成功" + case models.Failed: + return "失败" + default: + return fmt.Sprintf("未知状态:%d", status) + } +} From c53ce98e4313f2d8b6bfaa39d2160e03ce0c9156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E8=B6=85=E8=B6=8A?= <993921@qq.com> Date: Sun, 16 Jan 2022 15:08:28 +0800 Subject: [PATCH 3/4] bugfix; add function tests; --- internal/core/notification/impl/dingrobot.go | 2 +- internal/core/notification/notify_test.go | 25 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 internal/core/notification/notify_test.go diff --git a/internal/core/notification/impl/dingrobot.go b/internal/core/notification/impl/dingrobot.go index c7fea613..eb890127 100644 --- a/internal/core/notification/impl/dingrobot.go +++ b/internal/core/notification/impl/dingrobot.go @@ -24,7 +24,7 @@ func dingEventMessage(template INotifyTemplate, result messages.StepCallbackResu var buf bytes.Buffer template.GenSubject(&buf, result) - template.GenSubject(&buf, result) + template.GenContent(&buf, result) template.GenFooter(&buf, result) markdownText := &messages.MarkdownMessage{ diff --git a/internal/core/notification/notify_test.go b/internal/core/notification/notify_test.go new file mode 100644 index 00000000..fe645031 --- /dev/null +++ b/internal/core/notification/notify_test.go @@ -0,0 +1,25 @@ +package notification + +import ( + notification "github.com/go-atomci/atomci/internal/core/notification/impl" + messages "github.com/go-atomci/atomci/internal/core/notification/types" + "testing" +) + +func TestNotifyEmail(t *testing.T) { + notification.EmailHandler().Send(messages.StepCallbackResult{ + StageName: "", + PublishName: "", + StepName: "", + Status: 1, + }) +} + +func TestNotifyDingRobot(t *testing.T) { + notification.DingRobotHandler().Send(messages.StepCallbackResult{ + StageName: "aa", + PublishName: "bb", + StepName: "cc", + Status: 0, + }) +} From d841e8ec094320366927b5b48411bdaad6c76100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E8=B6=85=E8=B6=8A?= <993921@qq.com> Date: Sun, 16 Jan 2022 15:31:38 +0800 Subject: [PATCH 4/4] =?UTF-8?q?issue:=20=E5=A2=9E=E5=8A=A0notification?= =?UTF-8?q?=E5=90=AF=E7=94=A8=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ conf/app.conf | 3 +++ internal/core/notification/impl/notify.go | 8 ++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 671c6140..fde0e8e4 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,10 @@ AtomCI 致力于让中小企业快速落地Kubernetes,代码均已开源, __ url = root:root@tcp(127.0.0.1:3306)/atomci?charset=utf8mb4 [notification] +dingEnable = 1 # 启用钉钉通知;0:不启用,1:启用 ding = 钉钉机器人 + +mailEnable = 1 # 启用邮件通知;0:不启用,1:启用 smtpHost = SMTP服务器 smtpPort = 465 smtpAccount = 邮件账号 diff --git a/conf/app.conf b/conf/app.conf index 22786c17..393fff17 100644 --- a/conf/app.conf +++ b/conf/app.conf @@ -45,7 +45,10 @@ url = http://localhost:8080 # notification config [notification] +dingEnable = 1 ding = "https://oapi.dingtalk.com/robot/send?access_token=faketoken" + +mailEnable = 1 smtpHost = "smtp.host" smtpPort = 465 smtpAccount = "fake@mail.com" diff --git a/internal/core/notification/impl/notify.go b/internal/core/notification/impl/notify.go index 69e27979..6de9c794 100644 --- a/internal/core/notification/impl/notify.go +++ b/internal/core/notification/impl/notify.go @@ -12,16 +12,16 @@ type INotify interface { func NewHandlers() []INotify { - ding := beego.AppConfig.String("notification::ding") - smtp := beego.AppConfig.String("notification::smtpHost") + dingEnable, _ := beego.AppConfig.Int("notification::dingEnable") + mailEnable, _ := beego.AppConfig.Int("notification::mailEnable") var handlers []INotify - if len(ding) > 0 { + if dingEnable > 0 { _ = append(handlers, DingRobotHandler()) } - if len(smtp) > 0 { + if mailEnable > 0 { _ = append(handlers, EmailHandler()) }