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

流水线: ") + buf.WriteString(m.PublishName) + buf.WriteString("

阶段: ") + buf.WriteString(m.StageName) + buf.WriteString("

步骤: ") + buf.WriteString(m.StepName) + buf.WriteString("

") + buf.WriteString(messages.StatusCodeToChinese(m.Status)) + buf.WriteString("

") + + return buf.String() +} + +func (temp *EmailTemplate) GenFooter(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()) }