From df373cdaf6ccd4c8c4df9a4bd44e7ea587845e1a Mon Sep 17 00:00:00 2001 From: Jian Qiu Date: Mon, 17 Oct 2022 14:30:03 +0800 Subject: [PATCH] feature: api for sending emails with attachments (#15201) Co-authored-by: Qiu Jian --- .gitignore | 2 + cmd/climc/shell/notifyv2/emailqueues.go | 29 ++ go.mod | 2 + go.sum | 4 + pkg/apis/notify/emailqueue.go | 107 ++++++ pkg/cloudcommon/db/baselog.go | 147 +++++++ pkg/cloudcommon/db/opslog.go | 99 +---- pkg/logger/models/actionlog.go | 44 +-- .../modules/notify/mod_emailqueues.go | 32 ++ pkg/mcclient/options/notify/emailqueues.go | 96 +++++ pkg/notify/models/config.go | 16 + pkg/notify/models/emailqueue.go | 284 ++++++++++++++ pkg/notify/models/emailqueuestatus.go | 63 +++ pkg/notify/models/event.go | 22 +- pkg/notify/models/notification.go | 8 +- pkg/notify/sender/doc.go | 15 + pkg/notify/sender/email.go | 110 ++++++ pkg/notify/sender/worker.go | 23 ++ pkg/notify/service/handlers.go | 5 + pkg/webconsole/models/commandlog.go | 13 +- .../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/gopkg.in/mail.v2/.gitignore | 17 + vendor/gopkg.in/mail.v2/.travis.yml | 25 ++ vendor/gopkg.in/mail.v2/CHANGELOG.md | 88 +++++ vendor/gopkg.in/mail.v2/CONTRIBUTING.md | 20 + vendor/gopkg.in/mail.v2/LICENSE | 20 + vendor/gopkg.in/mail.v2/README.md | 129 +++++++ vendor/gopkg.in/mail.v2/auth.go | 49 +++ vendor/gopkg.in/mail.v2/doc.go | 6 + vendor/gopkg.in/mail.v2/errors.go | 16 + vendor/gopkg.in/mail.v2/message.go | 359 ++++++++++++++++++ vendor/gopkg.in/mail.v2/mime.go | 21 + vendor/gopkg.in/mail.v2/mime_go14.go | 25 ++ vendor/gopkg.in/mail.v2/send.go | 116 ++++++ vendor/gopkg.in/mail.v2/smtp.go | 292 ++++++++++++++ vendor/gopkg.in/mail.v2/writeto.go | 313 +++++++++++++++ vendor/modules.txt | 6 + 43 files changed, 3124 insertions(+), 151 deletions(-) create mode 100644 cmd/climc/shell/notifyv2/emailqueues.go create mode 100644 pkg/apis/notify/emailqueue.go create mode 100644 pkg/cloudcommon/db/baselog.go create mode 100644 pkg/mcclient/modules/notify/mod_emailqueues.go create mode 100644 pkg/mcclient/options/notify/emailqueues.go create mode 100644 pkg/notify/models/emailqueue.go create mode 100644 pkg/notify/models/emailqueuestatus.go create mode 100644 pkg/notify/sender/doc.go create mode 100644 pkg/notify/sender/email.go create mode 100644 pkg/notify/sender/worker.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 create mode 100644 vendor/gopkg.in/mail.v2/.gitignore create mode 100644 vendor/gopkg.in/mail.v2/.travis.yml create mode 100644 vendor/gopkg.in/mail.v2/CHANGELOG.md create mode 100644 vendor/gopkg.in/mail.v2/CONTRIBUTING.md create mode 100644 vendor/gopkg.in/mail.v2/LICENSE create mode 100644 vendor/gopkg.in/mail.v2/README.md create mode 100644 vendor/gopkg.in/mail.v2/auth.go create mode 100644 vendor/gopkg.in/mail.v2/doc.go create mode 100644 vendor/gopkg.in/mail.v2/errors.go create mode 100644 vendor/gopkg.in/mail.v2/message.go create mode 100644 vendor/gopkg.in/mail.v2/mime.go create mode 100644 vendor/gopkg.in/mail.v2/mime_go14.go create mode 100644 vendor/gopkg.in/mail.v2/send.go create mode 100644 vendor/gopkg.in/mail.v2/smtp.go create mode 100644 vendor/gopkg.in/mail.v2/writeto.go diff --git a/.gitignore b/.gitignore index 62d8a41609..725c1bed02 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ pkg/generated # gotext extracted out.gotext.json + +pkg/notify/sender/email_test.go diff --git a/cmd/climc/shell/notifyv2/emailqueues.go b/cmd/climc/shell/notifyv2/emailqueues.go new file mode 100644 index 0000000000..9046cb8e80 --- /dev/null +++ b/cmd/climc/shell/notifyv2/emailqueues.go @@ -0,0 +1,29 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notifyv2 + +import ( + "yunion.io/x/onecloud/cmd/climc/shell" + modules "yunion.io/x/onecloud/pkg/mcclient/modules/notify" + options "yunion.io/x/onecloud/pkg/mcclient/options/notify" +) + +func init() { + cmd := shell.NewResourceCmd(&modules.EmailQueues) + cmd.List(new(options.EmailQueueListOptions)) + cmd.Create(new(options.EmailQueueCreateOptions)) + cmd.Show(new(options.EmailQueueOptions)) + cmd.Perform("send", new(options.EmailQueueSendOptions)) +} diff --git a/go.mod b/go.mod index 0f87bc3da2..28e4f16328 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,7 @@ require ( google.golang.org/protobuf v1.27.1 gopkg.in/fatih/set.v0 v0.2.1 gopkg.in/ldap.v3 v3.0.3 + gopkg.in/mail.v2 v2.3.1 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.19.3 k8s.io/apimachinery v0.19.3 @@ -247,6 +248,7 @@ require ( google.golang.org/api v0.30.0 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 954db89fa1..066a4ad260 100644 --- a/go.sum +++ b/go.sum @@ -1064,6 +1064,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 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/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1082,6 +1084,8 @@ gopkg.in/ini.v1 v1.44.0 h1:YRJzTUp0kSYWUVFF5XAbDFfyiqwsl0Vb9R8TVP5eRi0= gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ldap.v3 v3.0.3 h1:YKRHW/2sIl05JsCtx/5ZuUueFuJyoj/6+DGXe3wp6ro= gopkg.in/ldap.v3 v3.0.3/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw= +gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= +gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/urfave/cli.v1 v1.19.1/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= diff --git a/pkg/apis/notify/emailqueue.go b/pkg/apis/notify/emailqueue.go new file mode 100644 index 0000000000..9ebbd369ff --- /dev/null +++ b/pkg/apis/notify/emailqueue.go @@ -0,0 +1,107 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notify + +import ( + "time" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/apis" +) + +type SEmailMessage struct { + To []string `json:"to"` + Subject string `json:"subject"` + Body string `json:"body"` + + Attachments []SEmailAttachment `json:"attachments"` +} + +type SEmailAttachment struct { + Filename string `json:"filename"` + Mime string `json:"mime"` + + Base64Content string `json:"content"` +} + +type SEmailConfig struct { + Hostname string `json:"hostname"` + Hostport int `json:"hostport"` + Username string `json:"username"` + Password string `json:"password"` + SenderAddress string `json:"sender_address"` + SslGlobal bool `json:"ssl_global"` +} + +type EmailQueueCreateInput struct { + SEmailMessage + + // swagger: ignore + Dest string `json:"dest"` + // swagger: ignore + Content jsonutils.JSONObject `json:"content"` + + // swagger: ignore + ProjectId string `json:"project_id"` + // swagger: ignore + Project string `json:"project"` + // swagger: ignore + ProjectDomainId string `json:"project_domain_id"` + // swagger: ignore + ProjectDomain string `json:"project_domain"` + // swagger: ignore + UserId string `json:"user_id"` + // swagger: ignore + User string `json:"user"` + // swagger: ignore + DomainId string `json:"domain_id"` + // swagger: ignore + Domain string `json:"domain"` + // swagger: ignore + Roles string `json:"roles"` + + SessionId string `json:"session_id"` +} + +type EmailQueueListInput struct { + apis.ModelBaseListInput + + Id []int `json:"id"` + To []string `json:"to"` + Subject string `json:"subject"` + SessionId []string `json:"session_id"` +} + +const ( + EmailQueued = "queued" + EmailSending = "sending" + EmailSuccess = "success" + EmailFail = "fail" +) + +type EmailQueueSendInput struct { + Sync bool `json:"sync"` +} + +type EmailQueueDetails struct { + apis.ModelBaseDetails + + SentAt time.Time `json:"sent_at"` + + Status string `json:"status"` + + Results string `json:"results"` +} diff --git a/pkg/cloudcommon/db/baselog.go b/pkg/cloudcommon/db/baselog.go new file mode 100644 index 0000000000..a3ee8c39cd --- /dev/null +++ b/pkg/cloudcommon/db/baselog.go @@ -0,0 +1,147 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package db + +import ( + "context" + "fmt" + "strconv" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/sqlchemy" + "yunion.io/x/sqlchemy/backends/clickhouse" + + "yunion.io/x/onecloud/pkg/cloudcommon/consts" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" +) + +type SLogBaseManager struct { + SModelBaseManager +} + +type SLogBase struct { + SModelBase + + Id int64 `primary:"true" auto_increment:"true" list:"user" clickhouse_partition_by:"toInt64(divide(id,100000000000))"` +} + +func NewLogBaseManager(model interface{}, table string, keyword, keywordPlural string, timeCol string, useClickHouse bool) SLogBaseManager { + if useClickHouse { + man := SLogBaseManager{NewModelBaseManagerWithDBName( + model, + table, + keyword, + keywordPlural, + ClickhouseDB, + )} + col := man.TableSpec().ColumnSpec("timeCol") + if clickCol, ok := col.(clickhouse.IClickhouseColumnSpec); ok { + clickCol.SetTTL(consts.SplitableMaxKeepMonths(), "MONTH") + } + return man + } else { + return SLogBaseManager{NewModelBaseManagerWithSplitable( + model, + table, + keyword, + keywordPlural, + "id", + timeCol, + consts.SplitableMaxDuration(), + consts.SplitableMaxKeepMonths(), + )} + } +} + +func (manager *SLogBaseManager) CreateByInsertOrUpdate() bool { + return false +} + +func CurrentTimestamp(t time.Time) int64 { + ret := int64(0) + const ( + yOffset = 10000000000000 + mOffset = 100000000000 + dOffset = 1000000000 + hOffset = 10000000 + iOffset = 100000 + sOffset = 1000 + ) + ret += int64(t.Year()) * yOffset + ret += int64(t.Month()) * mOffset + ret += int64(t.Day()) * dOffset + ret += int64(t.Hour()) * hOffset + ret += int64(t.Minute()) * iOffset + ret += int64(t.Second()) * sOffset + ret += int64(t.Nanosecond()) / 1000000 + return ret +} + +func (opslog *SLogBase) BeforeInsert() { + t := time.Now().UTC() + opslog.Id = CurrentTimestamp(t) +} + +func (opslog *SLogBase) GetId() string { + return fmt.Sprintf("%d", opslog.Id) +} + +func (self *SLogBase) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error { + return httperrors.NewForbiddenError("not allow to delete log") +} + +func (self *SLogBaseManager) FilterById(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery { + id, _ := strconv.Atoi(idStr) + return q.Equals("id", id) +} + +func (self *SLogBaseManager) FilterByNotId(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery { + id, _ := strconv.Atoi(idStr) + return q.NotEquals("id", id) +} + +func (self *SLogBaseManager) FilterByName(q *sqlchemy.SQuery, name string) *sqlchemy.SQuery { + return q +} + +func (manager *SLogBaseManager) GetPagingConfig() *SPagingConfig { + return &SPagingConfig{ + Order: sqlchemy.SQL_ORDER_DESC, + MarkerFields: []string{"id"}, + DefaultLimit: 20, + } +} + +func (lb *SLogBase) GetRecordTime() time.Time { + log.Fatalf("not implemented yet!") + return time.Time{} +} + +func (manager *SLogBaseManager) FetchById(idStr string) (IModel, error) { + return FetchById(manager.GetIModelManager(), idStr) +} + +func (l *SLogBase) ValidateUpdateData( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + data jsonutils.JSONObject, +) (jsonutils.JSONObject, error) { + return nil, errors.Wrap(httperrors.ErrForbidden, "not allow") +} diff --git a/pkg/cloudcommon/db/opslog.go b/pkg/cloudcommon/db/opslog.go index 3b50bd0e8b..68f4e74c89 100644 --- a/pkg/cloudcommon/db/opslog.go +++ b/pkg/cloudcommon/db/opslog.go @@ -19,7 +19,6 @@ import ( "database/sql" "fmt" "runtime/debug" - "strconv" "strings" "time" @@ -30,7 +29,6 @@ import ( "yunion.io/x/pkg/util/stringutils" "yunion.io/x/pkg/util/timeutils" "yunion.io/x/sqlchemy" - "yunion.io/x/sqlchemy/backends/clickhouse" "yunion.io/x/onecloud/pkg/apis" "yunion.io/x/onecloud/pkg/appsrv" @@ -42,13 +40,12 @@ import ( ) type SOpsLogManager struct { - SModelBaseManager + SLogBaseManager } type SOpsLog struct { - SModelBase + SLogBase - Id int64 `primary:"true" auto_increment:"true" list:"user" clickhouse_partition_by:"toInt64(divide(id,100000000000))"` ObjType string `width:"40" charset:"ascii" nullable:"false" list:"user" create:"required"` ObjId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"required" index:"true"` ObjName string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"required"` @@ -81,41 +78,21 @@ var _ IModel = (*SOpsLog)(nil) var opslogQueryWorkerMan *appsrv.SWorkerManager var opslogWriteWorkerMan *appsrv.SWorkerManager -func InitOpsLog() { - if consts.OpsLogWithClickhouse { - OpsLog = &SOpsLogManager{NewModelBaseManagerWithDBName( - SOpsLog{}, - "opslog_tbl", - "event", - "events", - ClickhouseDB, - )} - col := OpsLog.TableSpec().ColumnSpec("ops_time") - if clickCol, ok := col.(clickhouse.IClickhouseColumnSpec); ok { - clickCol.SetTTL(consts.SplitableMaxKeepMonths(), "MONTH") - } - } else { - OpsLog = &SOpsLogManager{NewModelBaseManagerWithSplitable( - SOpsLog{}, - "opslog_tbl", - "event", - "events", - "id", - "ops_time", - consts.SplitableMaxDuration(), - consts.SplitableMaxKeepMonths(), - )} +func NewOpsLogManager(opslog interface{}, tblName string, keyword, keywordPlural string, timeField string, clickhouse bool) SOpsLogManager { + return SOpsLogManager{ + SLogBaseManager: NewLogBaseManager(opslog, tblName, keyword, keywordPlural, timeField, clickhouse), } +} + +func InitOpsLog() { + tmp := NewOpsLogManager(SOpsLog{}, "opslog_tbl", "event", "events", "ops_time", consts.OpsLogWithClickhouse) + OpsLog = &tmp OpsLog.SetVirtualObject(OpsLog) opslogQueryWorkerMan = appsrv.NewWorkerManager("opslog_query_worker", 2, 512, true) opslogWriteWorkerMan = appsrv.NewWorkerManager("opslog_write_worker", 1, 2048, true) } -func (manager *SOpsLogManager) CreateByInsertOrUpdate() bool { - return false -} - func (manager *SOpsLogManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) { manager.SModelBaseManager.CustomizeHandlerInfo(info) @@ -125,35 +102,6 @@ func (manager *SOpsLogManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) { } } -func CurrentTimestamp(t time.Time) int64 { - ret := int64(0) - const ( - yOffset = 10000000000000 - mOffset = 100000000000 - dOffset = 1000000000 - hOffset = 10000000 - iOffset = 100000 - sOffset = 1000 - ) - ret += int64(t.Year()) * yOffset - ret += int64(t.Month()) * mOffset - ret += int64(t.Day()) * dOffset - ret += int64(t.Hour()) * hOffset - ret += int64(t.Minute()) * iOffset - ret += int64(t.Second()) * sOffset - ret += int64(t.Nanosecond()) / 1000000 - return ret -} - -func (opslog *SOpsLog) BeforeInsert() { - t := time.Now().UTC() - opslog.Id = CurrentTimestamp(t) -} - -func (opslog *SOpsLog) GetId() string { - return fmt.Sprintf("%d", opslog.Id) -} - func (opslog *SOpsLog) GetName() string { return fmt.Sprintf("%s-%s", opslog.ObjType, opslog.Action) } @@ -410,24 +358,6 @@ func (manager *SOpsLogManager) LogSyncUpdate(m IModel, uds sqlchemy.UpdateDiffs, } } -func (self *SOpsLog) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error { - return httperrors.NewForbiddenError("not allow to delete log") -} - -func (self *SOpsLogManager) FilterById(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery { - id, _ := strconv.Atoi(idStr) - return q.Equals("id", id) -} - -func (self *SOpsLogManager) FilterByNotId(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery { - id, _ := strconv.Atoi(idStr) - return q.NotEquals("id", id) -} - -func (self *SOpsLogManager) FilterByName(q *sqlchemy.SQuery, name string) *sqlchemy.SQuery { - return q -} - func (self *SOpsLogManager) FilterByOwner(q *sqlchemy.SQuery, ownerId mcclient.IIdentityProvider, scope rbacutils.TRbacScope) *sqlchemy.SQuery { if ownerId != nil { switch scope { @@ -487,14 +417,6 @@ func (manager *SOpsLogManager) ResourceScope() rbacutils.TRbacScope { return rbacutils.ScopeUser } -func (manager *SOpsLogManager) GetPagingConfig() *SPagingConfig { - return &SPagingConfig{ - Order: sqlchemy.SQL_ORDER_DESC, - MarkerFields: []string{"id"}, - DefaultLimit: 20, - } -} - func (manager *SOpsLogManager) FetchOwnerId(ctx context.Context, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) { ownerId := SOwnerId{} err := data.Unmarshal(&ownerId) @@ -533,6 +455,7 @@ func (log *SOpsLog) CustomizeCreate(ctx context.Context, return log.SModelBase.CustomizeCreate(ctx, userCred, ownerId, query, data) } +// override func (log *SOpsLog) GetRecordTime() time.Time { return log.OpsTime } diff --git a/pkg/logger/models/actionlog.go b/pkg/logger/models/actionlog.go index 1aae15e86c..b644c75a27 100644 --- a/pkg/logger/models/actionlog.go +++ b/pkg/logger/models/actionlog.go @@ -26,7 +26,6 @@ import ( "yunion.io/x/pkg/util/timeutils" "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" - "yunion.io/x/sqlchemy/backends/clickhouse" "yunion.io/x/onecloud/pkg/apis" api "yunion.io/x/onecloud/pkg/apis/logger" @@ -85,45 +84,14 @@ var logQueue = make(chan *SActionlog, 50) func InitActionLog() { InitActionWhiteList() - var initTable func(tbname string) *SActionlogManager - if consts.OpsLogWithClickhouse { - initTable = func(tbname string) *SActionlogManager { - tbl := &SActionlogManager{ - SOpsLogManager: db.SOpsLogManager{ - SModelBaseManager: db.NewModelBaseManagerWithDBName( - SActionlog{}, - tbname, - "action", - "actions", - db.ClickhouseDB, - ), - }, - } - col := tbl.TableSpec().ColumnSpec("ops_time") - if clickCol, ok := col.(clickhouse.IClickhouseColumnSpec); ok { - clickCol.SetTTL(consts.SplitableMaxKeepMonths(), "MONTH") - } - return tbl + initTable := func(tbname string) *SActionlogManager { + tbl := &SActionlogManager{ + SOpsLogManager: db.NewOpsLogManager(SActionlog{}, tbname, "action", "actions", "ops_time", consts.OpsLogWithClickhouse), } - } else { - initTable = func(tbname string) *SActionlogManager { - tbl := &SActionlogManager{ - SOpsLogManager: db.SOpsLogManager{ - SModelBaseManager: db.NewModelBaseManagerWithSplitable( - SActionlog{}, - tbname, - "action", - "actions", - "id", - "start_time", - consts.SplitableMaxDuration(), - consts.SplitableMaxKeepMonths(), - ), - }, - SRecordChecksumResourceBaseManager: *db.NewRecordChecksumResourceBaseManager(), - } - return tbl + if consts.OpsLogWithClickhouse { + tbl.SRecordChecksumResourceBaseManager = *db.NewRecordChecksumResourceBaseManager() } + return tbl } ActionLog = initTable("action_tbl") ActionLog.SetVirtualObject(ActionLog) diff --git a/pkg/mcclient/modules/notify/mod_emailqueues.go b/pkg/mcclient/modules/notify/mod_emailqueues.go new file mode 100644 index 0000000000..1d3fcd878c --- /dev/null +++ b/pkg/mcclient/modules/notify/mod_emailqueues.go @@ -0,0 +1,32 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notify + +import ( + "yunion.io/x/onecloud/pkg/mcclient/modulebase" + "yunion.io/x/onecloud/pkg/mcclient/modules" +) + +var ( + EmailQueues modulebase.ResourceManager +) + +func init() { + EmailQueues = modules.NewNotifyv2Manager("emailqueue", "emailqueues", + []string{"id", "subject", "dest", "status", "recv_at"}, + []string{}, + ) + modules.Register(&EmailQueues) +} diff --git a/pkg/mcclient/options/notify/emailqueues.go b/pkg/mcclient/options/notify/emailqueues.go new file mode 100644 index 0000000000..4d8f0cfa47 --- /dev/null +++ b/pkg/mcclient/options/notify/emailqueues.go @@ -0,0 +1,96 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package notify + +import ( + "encoding/base64" + "io/ioutil" + "path/filepath" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +type EmailQueueListOptions struct { + options.BaseListOptions + + Id []int `json:"id"` + To []string `json:"to"` + Subject string `json:"subject"` + SessionId []string `json:"session_id"` +} + +func (rl *EmailQueueListOptions) Params() (jsonutils.JSONObject, error) { + return options.ListStructToParams(rl) +} + +type EmailQueueCreateOptions struct { + SUBJECT string `help:"email subject"` + BODY string `help:"email body"` + TO []string `json:"to" help:"receiver email"` + + SessionId string `help:"session id of sending email"` + + Attach []string `help:"path to attachment"` +} + +func (rc *EmailQueueCreateOptions) Params() (jsonutils.JSONObject, error) { + input := api.EmailQueueCreateInput{} + input.To = rc.TO + input.Subject = rc.SUBJECT + body, err := ioutil.ReadFile(rc.BODY) + if err != nil { + return nil, errors.Wrap(err, "Read content") + } + input.Body = string(body) + for _, attach := range rc.Attach { + contBytes, err := ioutil.ReadFile(attach) + if err != nil { + return nil, errors.Wrapf(err, "read %s", attach) + } + input.Attachments = append(input.Attachments, api.SEmailAttachment{ + Filename: filepath.Base(attach), + Base64Content: base64.StdEncoding.EncodeToString(contBytes), + }) + } + log.Debugf("%s", jsonutils.Marshal(input)) + return jsonutils.Marshal(input), nil +} + +type EmailQueueOptions struct { + ID string `help:"Id of email queue" json:"-"` +} + +func (r *EmailQueueOptions) GetId() string { + return r.ID +} + +func (r *EmailQueueOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(r), nil +} + +type EmailQueueSendOptions struct { + EmailQueueOptions + + Sync bool `json:"sync" help:"send email synchronously"` +} + +func (r *EmailQueueSendOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(r), nil +} diff --git a/pkg/notify/models/config.go b/pkg/notify/models/config.go index 9258b63887..83f251e466 100644 --- a/pkg/notify/models/config.go +++ b/pkg/notify/models/config.go @@ -595,6 +595,22 @@ func (self *SConfigManager) GetConfigs(contactType string) ([]notifyv2.SConfig, return ret, nil } +func (manager *SConfigManager) getEmailConfig() (*api.SEmailConfig, error) { + confs, err := manager.GetConfigs(api.EMAIL) + if err != nil { + return nil, errors.Wrap(err, "GetConfigs") + } + if len(confs) == 0 { + return nil, errors.Wrap(errors.ErrNotSupported, "email not supported") + } + conf := api.SEmailConfig{} + err = jsonutils.Marshal(confs[0].Config).Unmarshal(&conf) + if err != nil { + return nil, errors.Wrap(err, "Unmarshal") + } + return &conf, nil +} + func (self *SConfigManager) SetConfig(contactType string, config notifyv2.SConfig) error { content := jsonutils.Marshal(config.Config) sConfig := &SConfig{ diff --git a/pkg/notify/models/emailqueue.go b/pkg/notify/models/emailqueue.go new file mode 100644 index 0000000000..485688a02b --- /dev/null +++ b/pkg/notify/models/emailqueue.go @@ -0,0 +1,284 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "context" + "fmt" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/regutils" + "yunion.io/x/sqlchemy" + + api "yunion.io/x/onecloud/pkg/apis/notify" + "yunion.io/x/onecloud/pkg/cloudcommon/consts" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/sender" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +type SEmailQueueManager struct { + db.SLogBaseManager +} + +type SEmailQueue struct { + db.SLogBase + + RecvAt time.Time `nullable:"false" created_at:"true" index:"true" get:"user" list:"user" json:"recv_at"` + + Dest string `width:"256" charset:"ascii" nullable:"false" list:"user" create:"admin_required"` + Subject string `width:"256" charset:"utf8" nullable:"false" list:"user" create:"admin_required"` + + SessionId string `width:"256" charset:"utf8" nullable:"false" list:"user" create:"admin_optional"` + + Content jsonutils.JSONObject `length:"long" charset:"utf8" nullable:"false" list:"user" create:"admin_required"` + + ProjectId string `width:"128" charset:"ascii" list:"user" create:"admin_optional" index:"true"` + Project string `width:"128" charset:"utf8" list:"user" create:"admin_optional"` + + ProjectDomainId string `name:"project_domain_id" default:"default" width:"128" charset:"ascii" list:"user" create:"admin_optional"` + ProjectDomain string `name:"project_domain" default:"Default" width:"128" charset:"utf8" list:"user" create:"admin_optional"` + + UserId string `width:"128" charset:"ascii" list:"user" create:"admin_required"` + User string `width:"128" charset:"utf8" list:"user" create:"admin_required"` + DomainId string `width:"128" charset:"ascii" list:"user" create:"admin_optional"` + Domain string `width:"128" charset:"utf8" list:"user" create:"admin_optional"` + Roles string `width:"64" charset:"utf8" list:"user" create:"admin_optional"` +} + +var EmailQueueManager *SEmailQueueManager + +func InitEmailQueue() { + EmailQueueManager = &SEmailQueueManager{ + SLogBaseManager: db.NewLogBaseManager(SEmailQueue{}, "emailqueue_tbl", "emailqueue", "emailqueues", "recv_at", consts.OpsLogWithClickhouse), + } + EmailQueueManager.SetVirtualObject(EmailQueueManager) +} + +func (e *SEmailQueue) GetRecordTime() time.Time { + return e.RecvAt +} + +func (manager *SEmailQueueManager) ValidateCreateData( + ctx context.Context, + userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, + query jsonutils.JSONObject, + input api.EmailQueueCreateInput, +) (api.EmailQueueCreateInput, error) { + // check permission + if db.IsAdminAllowCreate(userCred, manager).Result.IsDeny() { + return input, errors.Wrap(httperrors.ErrForbidden, "only admin can send email") + } + // validate data + if len(input.To) == 0 { + return input, errors.Wrap(httperrors.ErrInputParameter, "empty receiver") + } + invalidTos := make([]string, 0) + for _, to := range input.To { + if !regutils.MatchEmail(to) { + invalidTos = append(invalidTos, to) + } + } + if len(invalidTos) > 0 { + return input, errors.Wrapf(httperrors.ErrInputParameter, "invalid email %s", strings.Join(invalidTos, ",")) + } + input.Dest = strings.Join(input.To, ",") + msg := api.SEmailMessage{ + Body: input.Body, + Attachments: input.Attachments, + } + input.Content = jsonutils.Marshal(msg) + + input.Project = userCred.GetProjectName() + input.ProjectId = userCred.GetProjectId() + input.ProjectDomain = userCred.GetProjectDomain() + input.ProjectDomainId = userCred.GetProjectDomainId() + input.User = userCred.GetUserName() + input.UserId = userCred.GetUserId() + input.Domain = userCred.GetDomainName() + input.DomainId = userCred.GetDomainId() + input.Roles = strings.Join(userCred.GetRoles(), ",") + + return input, nil +} + +func (eq *SEmailQueue) PostCreate( + ctx context.Context, + userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, + query jsonutils.JSONObject, + data jsonutils.JSONObject, +) { + eq.SLogBase.PostCreate(ctx, userCred, ownerId, query, data) + eq.setStatus(ctx, api.EmailQueued, nil) + eq.doSendAsync() +} + +func (eq *SEmailQueue) doSendAsync() { + sender.Worker.Run(eq, nil, nil) +} + +func (eq *SEmailQueue) Dump() string { + return fmt.Sprintf("send email %s", eq.Subject) +} + +func (eq *SEmailQueue) Run() { + log.Debugf("send email") + eq.doSend(context.TODO()) +} + +func (eq *SEmailQueue) doSend(ctx context.Context) { + conf, err := ConfigManager.getEmailConfig() + if err != nil { + eq.setStatus(ctx, api.EmailFail, err) + return + } + log.Debugf("conf: %s", jsonutils.Marshal(conf)) + msg, err := eq.getMessage() + if err != nil { + eq.setStatus(ctx, api.EmailFail, err) + return + } + log.Debugf("msg: %s", jsonutils.Marshal(msg)) + eq.setStatus(ctx, api.EmailSending, nil) + err = sender.SendEmail(conf, msg) + if err != nil { + eq.setStatus(ctx, api.EmailFail, err) + return + } + eq.setStatus(ctx, api.EmailSuccess, nil) + return +} + +func (eq *SEmailQueue) getMessage() (*api.SEmailMessage, error) { + msg := api.SEmailMessage{} + err := eq.Content.Unmarshal(&msg) + if err != nil { + return nil, errors.Wrap(err, "Unmarshal") + } + msg.To = strings.Split(eq.Dest, ",") + msg.Subject = eq.Subject + return &msg, nil +} + +func (eq *SEmailQueue) setStatus(ctx context.Context, status string, results error) { + eqs := SEmailQueueStatus{ + Id: eq.Id, + Status: status, + } + if results != nil { + eqs.Results = results.Error() + } + if status == api.EmailSuccess || status == api.EmailFail { + eqs.SentAt = time.Now() + } + EmailQueueStatusManager.TableSpec().InsertOrUpdate(ctx, &eqs) +} + +// 宿主机/物理机列表 +func (manager *SEmailQueueManager) ListItemFilter( + ctx context.Context, + q *sqlchemy.SQuery, + userCred mcclient.TokenCredential, + query api.EmailQueueListInput, +) (*sqlchemy.SQuery, error) { + var err error + q, err = manager.SLogBaseManager.ListItemFilter(ctx, q, userCred, query.ModelBaseListInput) + if err != nil { + return q, errors.Wrap(err, "SLogBaseManager.ListItemFilter") + } + + if len(query.Id) > 0 { + q = q.In("id", query.Id) + } + if len(query.To) > 0 { + cond := make([]sqlchemy.ICondition, 0) + for _, to := range query.To { + cond = append(cond, sqlchemy.Contains(q.Field("dest"), to)) + } + q = q.Filter(sqlchemy.OR(cond...)) + } + if len(query.Subject) > 0 { + q = q.Contains("subject", query.Subject) + } + if len(query.SessionId) > 0 { + q = q.In("session_id", query.SessionId) + } + + return q, nil +} + +func (eq *SEmailQueue) PerformSend( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + input api.EmailQueueSendInput, +) (jsonutils.JSONObject, error) { + eq.setStatus(ctx, api.EmailQueued, nil) + if input.Sync { + log.Debugf("send email synchronously") + eq.doSend(ctx) + } else { + log.Debugf("send email Asynchronously") + eq.doSendAsync() + } + return nil, nil +} + +func (manager *SEmailQueueManager) FetchCustomizeColumns( + ctx context.Context, + userCred mcclient.TokenCredential, + query jsonutils.JSONObject, + objs []interface{}, + fields stringutils2.SSortedStrings, + isList bool, +) []api.EmailQueueDetails { + rows := make([]api.EmailQueueDetails, len(objs)) + + baseRows := manager.SModelBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + + emailIds := make([]int64, len(objs)) + for i := range rows { + rows[i] = api.EmailQueueDetails{ + ModelBaseDetails: baseRows[i], + } + eq := objs[i].(*SEmailQueue) + emailIds[i] = eq.Id + } + + rets, err := EmailQueueStatusManager.fetchEmailQueueStatus(emailIds) + if err != nil { + log.Errorf("EmailQueueStatusManager.fetchEmailQueueStatus fail %s", err) + return rows + } + + for i := range rows { + eq := objs[i].(*SEmailQueue) + if eqs, ok := rets[eq.Id]; ok { + rows[i].Status = eqs.Status + rows[i].SentAt = eqs.SentAt + rows[i].Results = eqs.Results + } + } + + return rows +} diff --git a/pkg/notify/models/emailqueuestatus.go b/pkg/notify/models/emailqueuestatus.go new file mode 100644 index 0000000000..08de5a5ad9 --- /dev/null +++ b/pkg/notify/models/emailqueuestatus.go @@ -0,0 +1,63 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "time" + + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" +) + +type SEmailQueueStatusManager struct { + db.SModelBaseManager +} + +type SEmailQueueStatus struct { + db.SModelBase + + Id int64 `primary:"true" list:"user"` + + SentAt time.Time `list:"user"` + + Status string `width:"16" charset:"ascii" default:"queued" list:"user"` + + Results string `list:"user" charset:"utf8"` +} + +var EmailQueueStatusManager *SEmailQueueStatusManager + +func init() { + EmailQueueStatusManager = &SEmailQueueStatusManager{ + SModelBaseManager: db.NewModelBaseManager(SEmailQueueStatus{}, "emailqueue_status_tbl", "emailqueue_status", "emailqueue_status"), + } + EmailQueueStatusManager.SetVirtualObject(EmailQueueStatusManager) +} + +func (manager *SEmailQueueStatusManager) fetchEmailQueueStatus(ids []int64) (map[int64]SEmailQueueStatus, error) { + q := manager.Query().In("id", ids) + results := make([]SEmailQueueStatus, 0) + err := q.All(&results) + if err != nil { + return nil, errors.Wrap(err, "query.All") + } + ret := make(map[int64]SEmailQueueStatus) + for i := range results { + eqs := results[i] + ret[eqs.Id] = eqs + } + return ret, nil +} diff --git a/pkg/notify/models/event.go b/pkg/notify/models/event.go index f0b2ed2923..b7ebe23fd9 100644 --- a/pkg/notify/models/event.go +++ b/pkg/notify/models/event.go @@ -16,30 +16,30 @@ package models import ( "context" + "time" + "yunion.io/x/onecloud/pkg/cloudcommon/consts" "yunion.io/x/onecloud/pkg/cloudcommon/db" ) type SEventManager struct { - db.SStandaloneAnonResourceBaseManager + db.SLogBaseManager } var EventManager *SEventManager -func init() { +func InitEventLog() { EventManager = &SEventManager{ - SStandaloneAnonResourceBaseManager: db.NewStandaloneAnonResourceBaseManager( - SEvent{}, - "events_tbl", - "notifyevent", - "notifyevents", - ), + SLogBaseManager: db.NewLogBaseManager(SEvent{}, "events2_tbl", "notifyevent", "notifyevents", "created_at", consts.OpsLogWithClickhouse), } EventManager.SetVirtualObject(EventManager) } type SEvent struct { - db.SStandaloneAnonResourceBase + db.SLogBase + + // 资源创建时间 + CreatedAt time.Time `nullable:"false" created_at:"true" index:"true" get:"user" list:"user" json:"created_at"` Message string Event string `width:"64" nullable:"true"` @@ -68,3 +68,7 @@ func (e *SEventManager) GetEvent(id string) (*SEvent, error) { } return model.(*SEvent), nil } + +func (e *SEvent) GetRecordTime() time.Time { + return e.CreatedAt +} diff --git a/pkg/notify/models/notification.go b/pkg/notify/models/notification.go index fb607f50c1..1a398dc97f 100644 --- a/pkg/notify/models/notification.go +++ b/pkg/notify/models/notification.go @@ -303,7 +303,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred if nm.needWebconsole([]STopic{*topic}) { // webconsole - err = nm.create(ctx, userCred, api.WEBCONSOLE, receiverIds, webconsoleContacts.UnsortedList(), input.Priority, event.Id) + err = nm.create(ctx, userCred, api.WEBCONSOLE, receiverIds, webconsoleContacts.UnsortedList(), input.Priority, event.GetId()) if err != nil { output.FailedList = append(output.FailedList, api.FailedElem{ ContactType: api.WEBCONSOLE, @@ -316,7 +316,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred if ct == api.MOBILE { continue } - err := nm.create(ctx, userCred, ct, receiverIds, nil, input.Priority, event.Id) + err := nm.create(ctx, userCred, ct, receiverIds, nil, input.Priority, event.GetId()) if err != nil { output.FailedList = append(output.FailedList, api.FailedElem{ ContactType: ct, @@ -324,7 +324,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred }) } } - err = nm.createWithWebhookRobots(ctx, userCred, webhookRobots, input.Priority, event.Id) + err = nm.createWithWebhookRobots(ctx, userCred, webhookRobots, input.Priority, event.GetId()) if err != nil { output.FailedList = append(output.FailedList, api.FailedElem{ ContactType: api.WEBHOOK, @@ -332,7 +332,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred }) } // robot - err = nm.createWithRobots(ctx, userCred, robots, input.Priority, event.Id) + err = nm.createWithRobots(ctx, userCred, robots, input.Priority, event.GetId()) if err != nil { output.FailedList = append(output.FailedList, api.FailedElem{ ContactType: api.ROBOT, diff --git a/pkg/notify/sender/doc.go b/pkg/notify/sender/doc.go new file mode 100644 index 0000000000..0c10552f54 --- /dev/null +++ b/pkg/notify/sender/doc.go @@ -0,0 +1,15 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender // import "yunion.io/x/onecloud/pkg/notify/sender" diff --git a/pkg/notify/sender/email.go b/pkg/notify/sender/email.go new file mode 100644 index 0000000000..1b0be88921 --- /dev/null +++ b/pkg/notify/sender/email.go @@ -0,0 +1,110 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender + +import ( + "crypto/tls" + "encoding/base64" + "io" + "time" + + "gopkg.in/mail.v2" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + api "yunion.io/x/onecloud/pkg/apis/notify" +) + +type errorMap map[string]error + +func (em errorMap) Error() string { + msg := make(map[string]string) + for k, e := range em { + msg[k] = e.Error() + } + return jsonutils.Marshal(msg).String() +} + +func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error { + dialer := mail.NewDialer(conf.Hostname, conf.Hostport, conf.Username, conf.Password) + + if conf.SslGlobal { + dialer.SSL = true + } else { + dialer.SSL = false + dialer.TLSConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + + sender, err := dialer.Dial() + if err != nil { + return errors.Wrap(err, "dialer.Dial") + } + + retErr := errorMap{} + + for _, to := range msg.To { + gmsg := mail.NewMessage() + gmsg.SetHeader("From", conf.SenderAddress) + gmsg.SetHeader("To", to) + gmsg.SetHeader("Subject", msg.Subject) + gmsg.SetBody("text/html", msg.Body) + + for _, attach := range msg.Attachments { + gmsg.Attach(attach.Filename, + mail.SetCopyFunc(func(w io.Writer) error { + mime := attach.Mime + if len(mime) == 0 { + mime = "application/octet-stream" + } + _, err := w.Write([]byte("Content-Type: " + attach.Mime)) + return errors.Wrap(err, "WriteMime") + }), + mail.SetCopyFunc(func(w io.Writer) error { + contBytes, err := base64.StdEncoding.DecodeString(attach.Base64Content) + if err != nil { + return errors.Wrap(err, "base64.StdEncoding.DecodeString") + } + _, err = w.Write(contBytes) + return errors.Wrap(err, "WriteContent") + }), + ) + } + + errs := make([]error, 0) + for tryTime := 3; tryTime > 0; tryTime-- { + err = mail.Send(sender, gmsg) + log.Debugf("send email ...") + if err != nil { + errs = append(errs, err) + time.Sleep(time.Second * 10) + continue + } + errs = errs[0:0] + break + } + + if len(errs) > 0 { + retErr[to] = errors.NewAggregate(errs) + } + } + if len(retErr) > 0 { + return retErr + } + return nil +} diff --git a/pkg/notify/sender/worker.go b/pkg/notify/sender/worker.go new file mode 100644 index 0000000000..481da7c9fe --- /dev/null +++ b/pkg/notify/sender/worker.go @@ -0,0 +1,23 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sender + +import "yunion.io/x/onecloud/pkg/appsrv" + +var Worker *appsrv.SWorkerManager + +func init() { + Worker = appsrv.NewWorkerManager("notify_sender_worker", 1, 2048, true) +} diff --git a/pkg/notify/service/handlers.go b/pkg/notify/service/handlers.go index 0f186d122e..c13e486653 100644 --- a/pkg/notify/service/handlers.go +++ b/pkg/notify/service/handlers.go @@ -30,6 +30,9 @@ const ( func InitHandlers(app *appsrv.Application) { db.InitAllManagers() + models.InitEventLog() + models.InitEmailQueue() + db.RegistUserCredCacheUpdater() db.AddScopeResourceCountHandler(API_VERSION, app) @@ -54,6 +57,7 @@ func InitHandlers(app *appsrv.Application) { db.SharedResourceManager, models.VerificationManager, models.EventManager, + models.EmailQueueStatusManager, } { db.RegisterModelManager(manager) } @@ -68,6 +72,7 @@ func InitHandlers(app *appsrv.Application) { models.TopicManager, models.RobotManager, models.SubscriberManager, + models.EmailQueueManager, } { db.RegisterModelManager(manager) handler := db.NewModelHandler(manager) diff --git a/pkg/webconsole/models/commandlog.go b/pkg/webconsole/models/commandlog.go index e1d5907fb9..66a0b67f3b 100644 --- a/pkg/webconsole/models/commandlog.go +++ b/pkg/webconsole/models/commandlog.go @@ -45,18 +45,7 @@ func GetCommandLogManager() *SCommandLogManager { return commandLogManager } commandLogManager = &SCommandLogManager{ - SOpsLogManager: db.SOpsLogManager{ - SModelBaseManager: db.NewModelBaseManagerWithSplitable( - SCommandLog{}, - "command_log_tbl", - "commandlog", - "commandlogs", - "id", - "start_time", - consts.SplitableMaxDuration(), - consts.SplitableMaxKeepMonths(), - ), - }, + SOpsLogManager: db.NewOpsLogManager(SCommandLog{}, "command_log_tbl", "commandlog", "commandlogs", "start_time", consts.OpsLogWithClickhouse), } commandLogManager.SetVirtualObject(commandLogManager) return commandLogManager diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE new file mode 100644 index 0000000000..5f5c12af73 --- /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 0000000000..98ddf829f5 --- /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 0000000000..cfd02617cd --- /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 0000000000..24283c52f0 --- /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 0000000000..d335b4ab57 --- /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 0000000000..955edca294 --- /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 0000000000..43359d51df --- /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/gopkg.in/mail.v2/.gitignore b/vendor/gopkg.in/mail.v2/.gitignore new file mode 100644 index 0000000000..cc4721cfe5 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/.gitignore @@ -0,0 +1,17 @@ + + +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + + +# IDE's +.idea/ diff --git a/vendor/gopkg.in/mail.v2/.travis.yml b/vendor/gopkg.in/mail.v2/.travis.yml new file mode 100644 index 0000000000..397d1660c2 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/.travis.yml @@ -0,0 +1,25 @@ +language: go + +go: + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - 1.8 + - 1.9 + - master + +# safelist +branches: + only: + - master + - v2 + +notifications: + email: false + +before_install: + - mkdir -p $GOPATH/src/gopkg.in && + ln -s ../github.com/go-mail/mail $GOPATH/src/gopkg.in/mail.v2 diff --git a/vendor/gopkg.in/mail.v2/CHANGELOG.md b/vendor/gopkg.in/mail.v2/CHANGELOG.md new file mode 100644 index 0000000000..cdd898af87 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/CHANGELOG.md @@ -0,0 +1,88 @@ +# Change Log +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## *Unreleased* + +## [2.3.1] - 2018-11-12 + +### Fixed + +- #39: Reverts addition of Go modules `go.mod` manifest. + +## [2.3.0] - 2018-11-10 + +### Added + +- #12: Adds `SendError` to provide additional info about the cause and index of + a failed attempt to transmit a batch of messages. +- go-gomail#78: Adds new `Message` methods for attaching and embedding + `io.Reader`s: `AttachReader` and `EmbedReader`. + +### Fixed + +- #26: Fixes RFC 1341 compliance by properly capitalizing the + `MIME-Version` header. +- #30: Fixes IO errors being silently dropped in `Message.WriteTo`. + +## [2.2.0] - 2018-03-01 + +### Added + +- #20: Adds `Message.SetBoundary` to allow specifying a custom MIME boundary. +- #22: Adds `Message.SetBodyWriter` to make it easy to use text/template and + html/template for message bodies. Contributed by Quantcast. +- #25: Adds `Dialer.StartTLSPolicy` so that `MandatoryStartTLS` can be required, + or `NoStartTLS` can disable it. Contributed by Quantcast. + +## [2.1.0] - 2017-12-14 + +### Added + +- go-gomail#40: Adds `Dialer.LocalName` field to allow specifying the hostname + sent with SMTP's HELO command. +- go-gomail#47: `Message.SetBody`, `Message.AddAlternative`, and + `Message.AddAlternativeWriter` allow specifying the encoding of message parts. +- `Dialer.Dial`'s returned `SendCloser` automatically redials after a timeout. +- go-gomail#55, go-gomail#56: Adds `Rename` to allow specifying filename + of an attachment. +- go-gomail#100: Exports `NetDialTimeout` to allow setting a custom dialer. +- go-gomail#70: Adds `Dialer.Timeout` field to allow specifying a timeout for + dials, reads, and writes. + +### Changed + +- go-gomail#52: `Dialer.Dial` automatically uses CRAM-MD5 when available. +- `Dialer.Dial` specifies a default timeout of 10 seconds. +- Gomail is forked from to + . + +### Deprecated + +- go-gomail#52: `NewPlainDialer` is deprecated in favor of `NewDialer`. + +### Fixed + +- go-gomail#41, go-gomail#42: Fixes a panic when a `Message` contains a + nil header. +- go-gomail#44: Fixes `AddAlternativeWriter` replacing the message body instead + of adding a body part. +- go-gomail#53: Folds long header lines for RFC 2047 compliance. +- go-gomail#54: Fixes `Message.FormatAddress` when name is blank. + +## [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/gopkg.in/mail.v2/CONTRIBUTING.md b/vendor/gopkg.in/mail.v2/CONTRIBUTING.md new file mode 100644 index 0000000000..d5601c257a --- /dev/null +++ b/vendor/gopkg.in/mail.v2/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/gopkg.in/mail.v2/LICENSE b/vendor/gopkg.in/mail.v2/LICENSE new file mode 100644 index 0000000000..5f5c12af73 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/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/mail.v2/README.md b/vendor/gopkg.in/mail.v2/README.md new file mode 100644 index 0000000000..8cc31b6f59 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/README.md @@ -0,0 +1,129 @@ +# Gomail +[![Build Status](https://travis-ci.org/go-mail/mail.svg?branch=master)](https://travis-ci.org/go-mail/mail) [![Code Coverage](http://gocover.io/_badge/github.com/go-mail/mail)](http://gocover.io/github.com/go-mail/mail) [![Documentation](https://godoc.org/github.com/go-mail/mail?status.svg)](https://godoc.org/github.com/go-mail/mail) + +This is an actively maintained fork of [Gomail][1] and includes fixes and +improvements for a number of outstanding issues. The current progress is +as follows: + + - [x] Timeouts and retries can be specified outside of the 10 second default. + - [x] Proxying is supported through specifying a custom [NetDialTimeout][2]. + - [ ] Filenames are properly encoded for non-ASCII characters. + - [ ] Email addresses are properly encoded for non-ASCII characters. + - [ ] Embedded files and attachments are tested for their existence. + - [ ] An `io.Reader` can be supplied when embedding and attaching files. + +See [Transitioning Existing Codebases][3] for more information on switching. + +[1]: https://github.com/go-gomail/gomail +[2]: https://godoc.org/gopkg.in/mail.v2#NetDialTimeout +[3]: #transitioning-existing-codebases + +## 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 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/github.com/go-mail/mail + + +## Download + +If you're already using a dependency manager, like [dep][dep], use the following +import path: + +``` +github.com/go-mail/mail +``` + +If you *aren't* using vendoring, `go get` the [Gopkg.in](http://gopkg.in) +import path: + +``` +gopkg.in/mail.v2 +``` + +[dep]: https://github.com/golang/dep#readme + +## Examples + +See the [examples in the documentation](https://godoc.org/github.com/go-mail/mail#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`: + +```go +package main + +import ( + "crypto/tls" + + "gopkg.in/mail.v2" +) + +func main() { + d := mail.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. + +### Transitioning Existing Codebases + +If you're already using the original Gomail, switching is as easy as updating +the import line to: + +``` +import gomail "gopkg.in/mail.v2" +``` + +## Contribute + +Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for +more info. + + +## Change log + +See [CHANGELOG.md](CHANGELOG.md). + + +## License + +[MIT](LICENSE) + + +## Support & 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/gopkg.in/mail.v2/auth.go b/vendor/gopkg.in/mail.v2/auth.go new file mode 100644 index 0000000000..b8c0dde7f2 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/auth.go @@ -0,0 +1,49 @@ +package mail + +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/gopkg.in/mail.v2/doc.go b/vendor/gopkg.in/mail.v2/doc.go new file mode 100644 index 0000000000..d65bf35916 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/doc.go @@ -0,0 +1,6 @@ +// Package gomail provides a simple interface to compose emails and to mail them +// efficiently. +// +// More info on Github: https://github.com/go-mail/mail +// +package mail diff --git a/vendor/gopkg.in/mail.v2/errors.go b/vendor/gopkg.in/mail.v2/errors.go new file mode 100644 index 0000000000..770da8c385 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/errors.go @@ -0,0 +1,16 @@ +package mail + +import "fmt" + +// A SendError represents the failure to transmit a Message, detailing the cause +// of the failure and index of the Message within a batch. +type SendError struct { + // Index specifies the index of the Message within a batch. + Index uint + Cause error +} + +func (err *SendError) Error() string { + return fmt.Sprintf("gomail: could not send email %d: %v", + err.Index+1, err.Cause) +} diff --git a/vendor/gopkg.in/mail.v2/message.go b/vendor/gopkg.in/mail.v2/message.go new file mode 100644 index 0000000000..9f7f7bde45 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/message.go @@ -0,0 +1,359 @@ +package mail + +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 + boundary string +} + +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" +) + +// SetBoundary sets a custom multipart boundary. +func (m *Message) SetBoundary(boundary string) { + m.boundary = boundary +} + +// 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, SetBodyWriter, AddAlternative or AddAlternativeWriter. +func (m *Message) SetBody(contentType, body string, settings ...PartSetting) { + m.SetBodyWriter(contentType, newCopier(body), settings...) +} + +// SetBodyWriter sets the body of the message. It can be useful with the +// text/template or html/template packages. +func (m *Message) SetBodyWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) { + m.parts = []*part{m.newPart(contentType, f, 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.SetBodyWriter, 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 + } +} + +// AttachReader attaches a file using an io.Reader +func (m *Message) AttachReader(name string, r io.Reader, settings ...FileSetting) { + m.attachments = m.appendFile(m.attachments, fileFromReader(name, r), settings) +} + +// Attach attaches the files to the email. +func (m *Message) Attach(filename string, settings ...FileSetting) { + m.attachments = m.appendFile(m.attachments, fileFromFilename(filename), settings) +} + +// EmbedReader embeds the images to the email. +func (m *Message) EmbedReader(name string, r io.Reader, settings ...FileSetting) { + m.embedded = m.appendFile(m.embedded, fileFromReader(name, r), settings) +} + +// Embed embeds the images to the email. +func (m *Message) Embed(filename string, settings ...FileSetting) { + m.embedded = m.appendFile(m.embedded, fileFromFilename(filename), settings) +} + +func fileFromFilename(name string) *file { + return &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() + }, + } +} + +func fileFromReader(name string, r io.Reader) *file { + return &file{ + Name: filepath.Base(name), + Header: make(map[string][]string), + CopyFunc: func(w io.Writer) error { + if _, err := io.Copy(w, r); err != nil { + return err + } + return nil + }, + } +} + +func (m *Message) appendFile(list []*file, f *file, settings []FileSetting) []*file { + for _, s := range settings { + s(f) + } + + if list == nil { + return []*file{f} + } + + return append(list, f) +} diff --git a/vendor/gopkg.in/mail.v2/mime.go b/vendor/gopkg.in/mail.v2/mime.go new file mode 100644 index 0000000000..d95ea2eb24 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/mime.go @@ -0,0 +1,21 @@ +// +build go1.5 + +package mail + +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/gopkg.in/mail.v2/mime_go14.go b/vendor/gopkg.in/mail.v2/mime_go14.go new file mode 100644 index 0000000000..bdb605dcca --- /dev/null +++ b/vendor/gopkg.in/mail.v2/mime_go14.go @@ -0,0 +1,25 @@ +// +build !go1.5 + +package mail + +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/gopkg.in/mail.v2/send.go b/vendor/gopkg.in/mail.v2/send.go new file mode 100644 index 0000000000..62e67f0b81 --- /dev/null +++ b/vendor/gopkg.in/mail.v2/send.go @@ -0,0 +1,116 @@ +package mail + +import ( + "errors" + "fmt" + "io" + stdmail "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 &SendError{Cause: err, Index: uint(i)} + } + } + + 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 := stdmail.ParseAddress(field) + if err != nil { + return "", fmt.Errorf("gomail: invalid address %q: %v", field, err) + } + return addr.Address, nil +} diff --git a/vendor/gopkg.in/mail.v2/smtp.go b/vendor/gopkg.in/mail.v2/smtp.go new file mode 100644 index 0000000000..547e04d16b --- /dev/null +++ b/vendor/gopkg.in/mail.v2/smtp.go @@ -0,0 +1,292 @@ +package mail + +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 + // TLSConfig represents the TLS configuration used for the TLS (when the + // STARTTLS extension is used) or SSL connection. + TLSConfig *tls.Config + // StartTLSPolicy represents the TLS security level required to + // communicate with the SMTP server. + // + // This defaults to OpportunisticStartTLS for backwards compatibility, + // but we recommend MandatoryStartTLS for all modern SMTP servers. + // + // This option has no effect if SSL is set to true. + StartTLSPolicy StartTLSPolicy + // LocalName is the hostname sent to the SMTP server with the HELO command. + // By default, "localhost" is sent. + LocalName string + // Timeout to use for read/write operations. Defaults to 10 seconds, can + // be set to 0 to disable timeouts. + Timeout time.Duration + // Whether we should retry mailing if the connection returned an error, + // defaults to true. + RetryFailure bool +} + +// 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, + Timeout: 10 * time.Second, + RetryFailure: true, + } +} + +// 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) +} + +// NetDialTimeout specifies the DialTimeout function to establish a connection +// to the SMTP server. This can be used to override dialing in the case that a +// proxy or other special behavior is needed. +var NetDialTimeout = net.DialTimeout + +// 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), d.Timeout) + 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.Timeout > 0 { + conn.SetDeadline(time.Now().Add(d.Timeout)) + } + + if d.LocalName != "" { + if err := c.Hello(d.LocalName); err != nil { + return nil, err + } + } + + if !d.SSL && d.StartTLSPolicy != NoStartTLS { + ok, _ := c.Extension("STARTTLS") + if !ok && d.StartTLSPolicy == MandatoryStartTLS { + err := StartTLSUnsupportedError{ + Policy: d.StartTLSPolicy} + return nil, err + } + + if 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, conn, d}, nil +} + +func (d *Dialer) tlsConfig() *tls.Config { + if d.TLSConfig == nil { + return &tls.Config{ServerName: d.Host} + } + return d.TLSConfig +} + +// StartTLSPolicy constants are valid values for Dialer.StartTLSPolicy. +type StartTLSPolicy int + +const ( + // OpportunisticStartTLS means that SMTP transactions are encrypted if + // STARTTLS is supported by the SMTP server. Otherwise, messages are + // sent in the clear. This is the default setting. + OpportunisticStartTLS StartTLSPolicy = iota + // MandatoryStartTLS means that SMTP transactions must be encrypted. + // SMTP transactions are aborted unless STARTTLS is supported by the + // SMTP server. + MandatoryStartTLS + // NoStartTLS means encryption is disabled and messages are sent in the + // clear. + NoStartTLS = -1 +) + +func (policy *StartTLSPolicy) String() string { + switch *policy { + case OpportunisticStartTLS: + return "OpportunisticStartTLS" + case MandatoryStartTLS: + return "MandatoryStartTLS" + case NoStartTLS: + return "NoStartTLS" + default: + return fmt.Sprintf("StartTLSPolicy:%v", *policy) + } +} + +// StartTLSUnsupportedError is returned by Dial when connecting to an SMTP +// server that does not support STARTTLS. +type StartTLSUnsupportedError struct { + Policy StartTLSPolicy +} + +func (e StartTLSUnsupportedError) Error() string { + return "gomail: " + e.Policy.String() + " required, but " + + "SMTP server does not support STARTTLS" +} + +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 + conn net.Conn + d *Dialer +} + +func (c *smtpSender) retryError(err error) bool { + if !c.d.RetryFailure { + return false + } + + if nerr, ok := err.(net.Error); ok && nerr.Timeout() { + return true + } + + return err == io.EOF +} + +func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error { + if c.d.Timeout > 0 { + c.conn.SetDeadline(time.Now().Add(c.d.Timeout)) + } + + if err := c.Mail(from); err != nil { + if c.retryError(err) { + // 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 ( + 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/gopkg.in/mail.v2/writeto.go b/vendor/gopkg.in/mail.v2/writeto.go new file mode 100644 index 0000000000..faf6124d6b --- /dev/null +++ b/vendor/gopkg.in/mail.v2/writeto.go @@ -0,0 +1,313 @@ +package mail + +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", m.boundary) + } + + if m.hasRelatedPart() { + w.openMultipart("related", m.boundary) + } + + if m.hasAlternativePart() { + w.openMultipart("alternative", m.boundary) + } + 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, boundary string) { + mw := multipart.NewWriter(w) + if boundary != "" { + mw.SetBoundary(boundary) + } + 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) { + if w.err != nil { // do nothing when in error + return + } + var n int + n, w.err = 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/modules.txt b/vendor/modules.txt index be1e5142d7..5397689af7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1228,6 +1228,9 @@ google.golang.org/protobuf/types/known/anypb google.golang.org/protobuf/types/known/durationpb google.golang.org/protobuf/types/known/timestamppb google.golang.org/protobuf/types/pluginpb +# gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc +## explicit +gopkg.in/alexcesaro/quotedprintable.v3 # gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d ## explicit gopkg.in/asn1-ber.v1 @@ -1245,6 +1248,9 @@ gopkg.in/ini.v1 # gopkg.in/ldap.v3 v3.0.3 ## explicit gopkg.in/ldap.v3 +# gopkg.in/mail.v2 v2.3.1 +## explicit +gopkg.in/mail.v2 # gopkg.in/yaml.v2 v2.4.0 ## explicit; go 1.15 gopkg.in/yaml.v2