mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/yunionio/cloudpods.git
synced 2026-09-20 08:03:53 +08:00
feature: api for sending emails with attachments (#15201)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,3 +41,5 @@ pkg/generated
|
|||||||
|
|
||||||
# gotext extracted
|
# gotext extracted
|
||||||
out.gotext.json
|
out.gotext.json
|
||||||
|
|
||||||
|
pkg/notify/sender/email_test.go
|
||||||
|
|||||||
29
cmd/climc/shell/notifyv2/emailqueues.go
Normal file
29
cmd/climc/shell/notifyv2/emailqueues.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -93,6 +93,7 @@ require (
|
|||||||
google.golang.org/protobuf v1.27.1
|
google.golang.org/protobuf v1.27.1
|
||||||
gopkg.in/fatih/set.v0 v0.2.1
|
gopkg.in/fatih/set.v0 v0.2.1
|
||||||
gopkg.in/ldap.v3 v3.0.3
|
gopkg.in/ldap.v3 v3.0.3
|
||||||
|
gopkg.in/mail.v2 v2.3.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
k8s.io/api v0.19.3
|
k8s.io/api v0.19.3
|
||||||
k8s.io/apimachinery 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/api v0.30.0 // indirect
|
||||||
google.golang.org/appengine v1.6.6 // indirect
|
google.golang.org/appengine v1.6.6 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // 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/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
|||||||
4
go.sum
4
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 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
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/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 h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
|
||||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
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=
|
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/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 h1:YKRHW/2sIl05JsCtx/5ZuUueFuJyoj/6+DGXe3wp6ro=
|
||||||
gopkg.in/ldap.v3 v3.0.3/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw=
|
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/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/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=
|
gopkg.in/urfave/cli.v1 v1.19.1/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
|
||||||
|
|||||||
107
pkg/apis/notify/emailqueue.go
Normal file
107
pkg/apis/notify/emailqueue.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
147
pkg/cloudcommon/db/baselog.go
Normal file
147
pkg/cloudcommon/db/baselog.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ import (
|
|||||||
"yunion.io/x/pkg/util/stringutils"
|
"yunion.io/x/pkg/util/stringutils"
|
||||||
"yunion.io/x/pkg/util/timeutils"
|
"yunion.io/x/pkg/util/timeutils"
|
||||||
"yunion.io/x/sqlchemy"
|
"yunion.io/x/sqlchemy"
|
||||||
"yunion.io/x/sqlchemy/backends/clickhouse"
|
|
||||||
|
|
||||||
"yunion.io/x/onecloud/pkg/apis"
|
"yunion.io/x/onecloud/pkg/apis"
|
||||||
"yunion.io/x/onecloud/pkg/appsrv"
|
"yunion.io/x/onecloud/pkg/appsrv"
|
||||||
@@ -42,13 +40,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SOpsLogManager struct {
|
type SOpsLogManager struct {
|
||||||
SModelBaseManager
|
SLogBaseManager
|
||||||
}
|
}
|
||||||
|
|
||||||
type SOpsLog struct {
|
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"`
|
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"`
|
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"`
|
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 opslogQueryWorkerMan *appsrv.SWorkerManager
|
||||||
var opslogWriteWorkerMan *appsrv.SWorkerManager
|
var opslogWriteWorkerMan *appsrv.SWorkerManager
|
||||||
|
|
||||||
func InitOpsLog() {
|
func NewOpsLogManager(opslog interface{}, tblName string, keyword, keywordPlural string, timeField string, clickhouse bool) SOpsLogManager {
|
||||||
if consts.OpsLogWithClickhouse {
|
return SOpsLogManager{
|
||||||
OpsLog = &SOpsLogManager{NewModelBaseManagerWithDBName(
|
SLogBaseManager: NewLogBaseManager(opslog, tblName, keyword, keywordPlural, timeField, clickhouse),
|
||||||
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 InitOpsLog() {
|
||||||
|
tmp := NewOpsLogManager(SOpsLog{}, "opslog_tbl", "event", "events", "ops_time", consts.OpsLogWithClickhouse)
|
||||||
|
OpsLog = &tmp
|
||||||
OpsLog.SetVirtualObject(OpsLog)
|
OpsLog.SetVirtualObject(OpsLog)
|
||||||
|
|
||||||
opslogQueryWorkerMan = appsrv.NewWorkerManager("opslog_query_worker", 2, 512, true)
|
opslogQueryWorkerMan = appsrv.NewWorkerManager("opslog_query_worker", 2, 512, true)
|
||||||
opslogWriteWorkerMan = appsrv.NewWorkerManager("opslog_write_worker", 1, 2048, true)
|
opslogWriteWorkerMan = appsrv.NewWorkerManager("opslog_write_worker", 1, 2048, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *SOpsLogManager) CreateByInsertOrUpdate() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *SOpsLogManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) {
|
func (manager *SOpsLogManager) CustomizeHandlerInfo(info *appsrv.SHandlerInfo) {
|
||||||
manager.SModelBaseManager.CustomizeHandlerInfo(info)
|
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 {
|
func (opslog *SOpsLog) GetName() string {
|
||||||
return fmt.Sprintf("%s-%s", opslog.ObjType, opslog.Action)
|
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 {
|
func (self *SOpsLogManager) FilterByOwner(q *sqlchemy.SQuery, ownerId mcclient.IIdentityProvider, scope rbacutils.TRbacScope) *sqlchemy.SQuery {
|
||||||
if ownerId != nil {
|
if ownerId != nil {
|
||||||
switch scope {
|
switch scope {
|
||||||
@@ -487,14 +417,6 @@ func (manager *SOpsLogManager) ResourceScope() rbacutils.TRbacScope {
|
|||||||
return rbacutils.ScopeUser
|
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) {
|
func (manager *SOpsLogManager) FetchOwnerId(ctx context.Context, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
|
||||||
ownerId := SOwnerId{}
|
ownerId := SOwnerId{}
|
||||||
err := data.Unmarshal(&ownerId)
|
err := data.Unmarshal(&ownerId)
|
||||||
@@ -533,6 +455,7 @@ func (log *SOpsLog) CustomizeCreate(ctx context.Context,
|
|||||||
return log.SModelBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
|
return log.SModelBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// override
|
||||||
func (log *SOpsLog) GetRecordTime() time.Time {
|
func (log *SOpsLog) GetRecordTime() time.Time {
|
||||||
return log.OpsTime
|
return log.OpsTime
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import (
|
|||||||
"yunion.io/x/pkg/util/timeutils"
|
"yunion.io/x/pkg/util/timeutils"
|
||||||
"yunion.io/x/pkg/utils"
|
"yunion.io/x/pkg/utils"
|
||||||
"yunion.io/x/sqlchemy"
|
"yunion.io/x/sqlchemy"
|
||||||
"yunion.io/x/sqlchemy/backends/clickhouse"
|
|
||||||
|
|
||||||
"yunion.io/x/onecloud/pkg/apis"
|
"yunion.io/x/onecloud/pkg/apis"
|
||||||
api "yunion.io/x/onecloud/pkg/apis/logger"
|
api "yunion.io/x/onecloud/pkg/apis/logger"
|
||||||
@@ -85,45 +84,14 @@ var logQueue = make(chan *SActionlog, 50)
|
|||||||
|
|
||||||
func InitActionLog() {
|
func InitActionLog() {
|
||||||
InitActionWhiteList()
|
InitActionWhiteList()
|
||||||
var initTable func(tbname string) *SActionlogManager
|
initTable := func(tbname string) *SActionlogManager {
|
||||||
if consts.OpsLogWithClickhouse {
|
tbl := &SActionlogManager{
|
||||||
initTable = func(tbname string) *SActionlogManager {
|
SOpsLogManager: db.NewOpsLogManager(SActionlog{}, tbname, "action", "actions", "ops_time", consts.OpsLogWithClickhouse),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
} else {
|
if consts.OpsLogWithClickhouse {
|
||||||
initTable = func(tbname string) *SActionlogManager {
|
tbl.SRecordChecksumResourceBaseManager = *db.NewRecordChecksumResourceBaseManager()
|
||||||
tbl := &SActionlogManager{
|
|
||||||
SOpsLogManager: db.SOpsLogManager{
|
|
||||||
SModelBaseManager: db.NewModelBaseManagerWithSplitable(
|
|
||||||
SActionlog{},
|
|
||||||
tbname,
|
|
||||||
"action",
|
|
||||||
"actions",
|
|
||||||
"id",
|
|
||||||
"start_time",
|
|
||||||
consts.SplitableMaxDuration(),
|
|
||||||
consts.SplitableMaxKeepMonths(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
SRecordChecksumResourceBaseManager: *db.NewRecordChecksumResourceBaseManager(),
|
|
||||||
}
|
|
||||||
return tbl
|
|
||||||
}
|
}
|
||||||
|
return tbl
|
||||||
}
|
}
|
||||||
ActionLog = initTable("action_tbl")
|
ActionLog = initTable("action_tbl")
|
||||||
ActionLog.SetVirtualObject(ActionLog)
|
ActionLog.SetVirtualObject(ActionLog)
|
||||||
|
|||||||
32
pkg/mcclient/modules/notify/mod_emailqueues.go
Normal file
32
pkg/mcclient/modules/notify/mod_emailqueues.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
96
pkg/mcclient/options/notify/emailqueues.go
Normal file
96
pkg/mcclient/options/notify/emailqueues.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -595,6 +595,22 @@ func (self *SConfigManager) GetConfigs(contactType string) ([]notifyv2.SConfig,
|
|||||||
return ret, nil
|
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 {
|
func (self *SConfigManager) SetConfig(contactType string, config notifyv2.SConfig) error {
|
||||||
content := jsonutils.Marshal(config.Config)
|
content := jsonutils.Marshal(config.Config)
|
||||||
sConfig := &SConfig{
|
sConfig := &SConfig{
|
||||||
|
|||||||
284
pkg/notify/models/emailqueue.go
Normal file
284
pkg/notify/models/emailqueue.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
63
pkg/notify/models/emailqueuestatus.go
Normal file
63
pkg/notify/models/emailqueuestatus.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -16,30 +16,30 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
|
||||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SEventManager struct {
|
type SEventManager struct {
|
||||||
db.SStandaloneAnonResourceBaseManager
|
db.SLogBaseManager
|
||||||
}
|
}
|
||||||
|
|
||||||
var EventManager *SEventManager
|
var EventManager *SEventManager
|
||||||
|
|
||||||
func init() {
|
func InitEventLog() {
|
||||||
EventManager = &SEventManager{
|
EventManager = &SEventManager{
|
||||||
SStandaloneAnonResourceBaseManager: db.NewStandaloneAnonResourceBaseManager(
|
SLogBaseManager: db.NewLogBaseManager(SEvent{}, "events2_tbl", "notifyevent", "notifyevents", "created_at", consts.OpsLogWithClickhouse),
|
||||||
SEvent{},
|
|
||||||
"events_tbl",
|
|
||||||
"notifyevent",
|
|
||||||
"notifyevents",
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
EventManager.SetVirtualObject(EventManager)
|
EventManager.SetVirtualObject(EventManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SEvent struct {
|
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
|
Message string
|
||||||
Event string `width:"64" nullable:"true"`
|
Event string `width:"64" nullable:"true"`
|
||||||
@@ -68,3 +68,7 @@ func (e *SEventManager) GetEvent(id string) (*SEvent, error) {
|
|||||||
}
|
}
|
||||||
return model.(*SEvent), nil
|
return model.(*SEvent), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *SEvent) GetRecordTime() time.Time {
|
||||||
|
return e.CreatedAt
|
||||||
|
}
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred
|
|||||||
|
|
||||||
if nm.needWebconsole([]STopic{*topic}) {
|
if nm.needWebconsole([]STopic{*topic}) {
|
||||||
// webconsole
|
// 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 {
|
if err != nil {
|
||||||
output.FailedList = append(output.FailedList, api.FailedElem{
|
output.FailedList = append(output.FailedList, api.FailedElem{
|
||||||
ContactType: api.WEBCONSOLE,
|
ContactType: api.WEBCONSOLE,
|
||||||
@@ -316,7 +316,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred
|
|||||||
if ct == api.MOBILE {
|
if ct == api.MOBILE {
|
||||||
continue
|
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 {
|
if err != nil {
|
||||||
output.FailedList = append(output.FailedList, api.FailedElem{
|
output.FailedList = append(output.FailedList, api.FailedElem{
|
||||||
ContactType: ct,
|
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 {
|
if err != nil {
|
||||||
output.FailedList = append(output.FailedList, api.FailedElem{
|
output.FailedList = append(output.FailedList, api.FailedElem{
|
||||||
ContactType: api.WEBHOOK,
|
ContactType: api.WEBHOOK,
|
||||||
@@ -332,7 +332,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// robot
|
// robot
|
||||||
err = nm.createWithRobots(ctx, userCred, robots, input.Priority, event.Id)
|
err = nm.createWithRobots(ctx, userCred, robots, input.Priority, event.GetId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
output.FailedList = append(output.FailedList, api.FailedElem{
|
output.FailedList = append(output.FailedList, api.FailedElem{
|
||||||
ContactType: api.ROBOT,
|
ContactType: api.ROBOT,
|
||||||
|
|||||||
15
pkg/notify/sender/doc.go
Normal file
15
pkg/notify/sender/doc.go
Normal file
@@ -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"
|
||||||
110
pkg/notify/sender/email.go
Normal file
110
pkg/notify/sender/email.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
23
pkg/notify/sender/worker.go
Normal file
23
pkg/notify/sender/worker.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ const (
|
|||||||
func InitHandlers(app *appsrv.Application) {
|
func InitHandlers(app *appsrv.Application) {
|
||||||
db.InitAllManagers()
|
db.InitAllManagers()
|
||||||
|
|
||||||
|
models.InitEventLog()
|
||||||
|
models.InitEmailQueue()
|
||||||
|
|
||||||
db.RegistUserCredCacheUpdater()
|
db.RegistUserCredCacheUpdater()
|
||||||
|
|
||||||
db.AddScopeResourceCountHandler(API_VERSION, app)
|
db.AddScopeResourceCountHandler(API_VERSION, app)
|
||||||
@@ -54,6 +57,7 @@ func InitHandlers(app *appsrv.Application) {
|
|||||||
db.SharedResourceManager,
|
db.SharedResourceManager,
|
||||||
models.VerificationManager,
|
models.VerificationManager,
|
||||||
models.EventManager,
|
models.EventManager,
|
||||||
|
models.EmailQueueStatusManager,
|
||||||
} {
|
} {
|
||||||
db.RegisterModelManager(manager)
|
db.RegisterModelManager(manager)
|
||||||
}
|
}
|
||||||
@@ -68,6 +72,7 @@ func InitHandlers(app *appsrv.Application) {
|
|||||||
models.TopicManager,
|
models.TopicManager,
|
||||||
models.RobotManager,
|
models.RobotManager,
|
||||||
models.SubscriberManager,
|
models.SubscriberManager,
|
||||||
|
models.EmailQueueManager,
|
||||||
} {
|
} {
|
||||||
db.RegisterModelManager(manager)
|
db.RegisterModelManager(manager)
|
||||||
handler := db.NewModelHandler(manager)
|
handler := db.NewModelHandler(manager)
|
||||||
|
|||||||
@@ -45,18 +45,7 @@ func GetCommandLogManager() *SCommandLogManager {
|
|||||||
return commandLogManager
|
return commandLogManager
|
||||||
}
|
}
|
||||||
commandLogManager = &SCommandLogManager{
|
commandLogManager = &SCommandLogManager{
|
||||||
SOpsLogManager: db.SOpsLogManager{
|
SOpsLogManager: db.NewOpsLogManager(SCommandLog{}, "command_log_tbl", "commandlog", "commandlogs", "start_time", consts.OpsLogWithClickhouse),
|
||||||
SModelBaseManager: db.NewModelBaseManagerWithSplitable(
|
|
||||||
SCommandLog{},
|
|
||||||
"command_log_tbl",
|
|
||||||
"commandlog",
|
|
||||||
"commandlogs",
|
|
||||||
"id",
|
|
||||||
"start_time",
|
|
||||||
consts.SplitableMaxDuration(),
|
|
||||||
consts.SplitableMaxKeepMonths(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
commandLogManager.SetVirtualObject(commandLogManager)
|
commandLogManager.SetVirtualObject(commandLogManager)
|
||||||
return commandLogManager
|
return commandLogManager
|
||||||
|
|||||||
20
vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
generated
vendored
Normal file
20
vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Alexandre Cesaro
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
16
vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
generated
vendored
Normal file
16
vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# quotedprintable
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Package quotedprintable implements quoted-printable and message header encoding
|
||||||
|
as specified by RFC 2045 and RFC 2047.
|
||||||
|
|
||||||
|
It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes
|
||||||
|
the new functions of package `mime` concerning RFC 2047.
|
||||||
|
|
||||||
|
This code has minor changes with the standard library code in order to work
|
||||||
|
with Go 1.0 and newer.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3
|
||||||
279
vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
generated
vendored
Normal file
279
vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
generated
vendored
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
package quotedprintable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A WordEncoder is a RFC 2047 encoded-word encoder.
|
||||||
|
type WordEncoder byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BEncoding represents Base64 encoding scheme as defined by RFC 2045.
|
||||||
|
BEncoding = WordEncoder('b')
|
||||||
|
// QEncoding represents the Q-encoding scheme as defined by RFC 2047.
|
||||||
|
QEncoding = WordEncoder('q')
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode returns the encoded-word form of s. If s is ASCII without special
|
||||||
|
// characters, it is returned unchanged. The provided charset is the IANA
|
||||||
|
// charset name of s. It is case insensitive.
|
||||||
|
func (e WordEncoder) Encode(charset, s string) string {
|
||||||
|
if !needsEncoding(s) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return e.encodeWord(charset, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func needsEncoding(s string) bool {
|
||||||
|
for _, b := range s {
|
||||||
|
if (b < ' ' || b > '~') && b != '\t' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeWord encodes a string into an encoded-word.
|
||||||
|
func (e WordEncoder) encodeWord(charset, s string) string {
|
||||||
|
buf := getBuffer()
|
||||||
|
defer putBuffer(buf)
|
||||||
|
|
||||||
|
buf.WriteString("=?")
|
||||||
|
buf.WriteString(charset)
|
||||||
|
buf.WriteByte('?')
|
||||||
|
buf.WriteByte(byte(e))
|
||||||
|
buf.WriteByte('?')
|
||||||
|
|
||||||
|
if e == BEncoding {
|
||||||
|
w := base64.NewEncoder(base64.StdEncoding, buf)
|
||||||
|
io.WriteString(w, s)
|
||||||
|
w.Close()
|
||||||
|
} else {
|
||||||
|
enc := make([]byte, 3)
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
b := s[i]
|
||||||
|
switch {
|
||||||
|
case b == ' ':
|
||||||
|
buf.WriteByte('_')
|
||||||
|
case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_':
|
||||||
|
buf.WriteByte(b)
|
||||||
|
default:
|
||||||
|
enc[0] = '='
|
||||||
|
enc[1] = upperhex[b>>4]
|
||||||
|
enc[2] = upperhex[b&0x0f]
|
||||||
|
buf.Write(enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString("?=")
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
const upperhex = "0123456789ABCDEF"
|
||||||
|
|
||||||
|
// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
|
||||||
|
type WordDecoder struct {
|
||||||
|
// CharsetReader, if non-nil, defines a function to generate
|
||||||
|
// charset-conversion readers, converting from the provided
|
||||||
|
// charset into UTF-8.
|
||||||
|
// Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets
|
||||||
|
// are handled by default.
|
||||||
|
// One of the the CharsetReader's result values must be non-nil.
|
||||||
|
CharsetReader func(charset string, input io.Reader) (io.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word,
|
||||||
|
// word is returned unchanged.
|
||||||
|
func (d *WordDecoder) Decode(word string) (string, error) {
|
||||||
|
fields := strings.Split(word, "?") // TODO: remove allocation?
|
||||||
|
if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 {
|
||||||
|
return "", errInvalidWord
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := decode(fields[2][0], fields[3])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := getBuffer()
|
||||||
|
defer putBuffer(buf)
|
||||||
|
|
||||||
|
if err := d.convert(buf, fields[1], content); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeHeader decodes all encoded-words of the given string. It returns an
|
||||||
|
// error if and only if CharsetReader of d returns an error.
|
||||||
|
func (d *WordDecoder) DecodeHeader(header string) (string, error) {
|
||||||
|
// If there is no encoded-word, returns before creating a buffer.
|
||||||
|
i := strings.Index(header, "=?")
|
||||||
|
if i == -1 {
|
||||||
|
return header, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := getBuffer()
|
||||||
|
defer putBuffer(buf)
|
||||||
|
|
||||||
|
buf.WriteString(header[:i])
|
||||||
|
header = header[i:]
|
||||||
|
|
||||||
|
betweenWords := false
|
||||||
|
for {
|
||||||
|
start := strings.Index(header, "=?")
|
||||||
|
if start == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cur := start + len("=?")
|
||||||
|
|
||||||
|
i := strings.Index(header[cur:], "?")
|
||||||
|
if i == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
charset := header[cur : cur+i]
|
||||||
|
cur += i + len("?")
|
||||||
|
|
||||||
|
if len(header) < cur+len("Q??=") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
encoding := header[cur]
|
||||||
|
cur++
|
||||||
|
|
||||||
|
if header[cur] != '?' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cur++
|
||||||
|
|
||||||
|
j := strings.Index(header[cur:], "?=")
|
||||||
|
if j == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
text := header[cur : cur+j]
|
||||||
|
end := cur + j + len("?=")
|
||||||
|
|
||||||
|
content, err := decode(encoding, text)
|
||||||
|
if err != nil {
|
||||||
|
betweenWords = false
|
||||||
|
buf.WriteString(header[:start+2])
|
||||||
|
header = header[start+2:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write characters before the encoded-word. White-space and newline
|
||||||
|
// characters separating two encoded-words must be deleted.
|
||||||
|
if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) {
|
||||||
|
buf.WriteString(header[:start])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.convert(buf, charset, content); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
header = header[end:]
|
||||||
|
betweenWords = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(header) > 0 {
|
||||||
|
buf.WriteString(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(encoding byte, text string) ([]byte, error) {
|
||||||
|
switch encoding {
|
||||||
|
case 'B', 'b':
|
||||||
|
return base64.StdEncoding.DecodeString(text)
|
||||||
|
case 'Q', 'q':
|
||||||
|
return qDecode(text)
|
||||||
|
}
|
||||||
|
return nil, errInvalidWord
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error {
|
||||||
|
switch {
|
||||||
|
case strings.EqualFold("utf-8", charset):
|
||||||
|
buf.Write(content)
|
||||||
|
case strings.EqualFold("iso-8859-1", charset):
|
||||||
|
for _, c := range content {
|
||||||
|
buf.WriteRune(rune(c))
|
||||||
|
}
|
||||||
|
case strings.EqualFold("us-ascii", charset):
|
||||||
|
for _, c := range content {
|
||||||
|
if c >= utf8.RuneSelf {
|
||||||
|
buf.WriteRune(unicode.ReplacementChar)
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if d.CharsetReader == nil {
|
||||||
|
return fmt.Errorf("mime: unhandled charset %q", charset)
|
||||||
|
}
|
||||||
|
r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = buf.ReadFrom(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least
|
||||||
|
// one byte of non-whitespace.
|
||||||
|
func hasNonWhitespace(s string) bool {
|
||||||
|
for _, b := range s {
|
||||||
|
switch b {
|
||||||
|
// Encoded-words can only be separated by linear white spaces which does
|
||||||
|
// not include vertical tabs (\v).
|
||||||
|
case ' ', '\t', '\n', '\r':
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// qDecode decodes a Q encoded string.
|
||||||
|
func qDecode(s string) ([]byte, error) {
|
||||||
|
dec := make([]byte, len(s))
|
||||||
|
n := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch c := s[i]; {
|
||||||
|
case c == '_':
|
||||||
|
dec[n] = ' '
|
||||||
|
case c == '=':
|
||||||
|
if i+2 >= len(s) {
|
||||||
|
return nil, errInvalidWord
|
||||||
|
}
|
||||||
|
b, err := readHexByte(s[i+1], s[i+2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dec[n] = b
|
||||||
|
i += 2
|
||||||
|
case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t':
|
||||||
|
dec[n] = c
|
||||||
|
default:
|
||||||
|
return nil, errInvalidWord
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
|
||||||
|
return dec[:n], nil
|
||||||
|
}
|
||||||
26
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
generated
vendored
Normal file
26
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// +build go1.3
|
||||||
|
|
||||||
|
package quotedprintable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bufPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBuffer() *bytes.Buffer {
|
||||||
|
return bufPool.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putBuffer(buf *bytes.Buffer) {
|
||||||
|
if buf.Len() > 1024 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf.Reset()
|
||||||
|
bufPool.Put(buf)
|
||||||
|
}
|
||||||
24
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
generated
vendored
Normal file
24
vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// +build !go1.3
|
||||||
|
|
||||||
|
package quotedprintable
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
var ch = make(chan *bytes.Buffer, 32)
|
||||||
|
|
||||||
|
func getBuffer() *bytes.Buffer {
|
||||||
|
select {
|
||||||
|
case buf := <-ch:
|
||||||
|
return buf
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return new(bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putBuffer(buf *bytes.Buffer) {
|
||||||
|
buf.Reset()
|
||||||
|
select {
|
||||||
|
case ch <- buf:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
121
vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
generated
vendored
Normal file
121
vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
generated
vendored
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Package quotedprintable implements quoted-printable encoding as specified by
|
||||||
|
// RFC 2045.
|
||||||
|
package quotedprintable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reader is a quoted-printable decoder.
|
||||||
|
type Reader struct {
|
||||||
|
br *bufio.Reader
|
||||||
|
rerr error // last read error
|
||||||
|
line []byte // to be consumed before more of br
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader returns a quoted-printable reader, decoding from r.
|
||||||
|
func NewReader(r io.Reader) *Reader {
|
||||||
|
return &Reader{
|
||||||
|
br: bufio.NewReader(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromHex(b byte) (byte, error) {
|
||||||
|
switch {
|
||||||
|
case b >= '0' && b <= '9':
|
||||||
|
return b - '0', nil
|
||||||
|
case b >= 'A' && b <= 'F':
|
||||||
|
return b - 'A' + 10, nil
|
||||||
|
// Accept badly encoded bytes.
|
||||||
|
case b >= 'a' && b <= 'f':
|
||||||
|
return b - 'a' + 10, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readHexByte(a, b byte) (byte, error) {
|
||||||
|
var hb, lb byte
|
||||||
|
var err error
|
||||||
|
if hb, err = fromHex(a); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if lb, err = fromHex(b); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return hb<<4 | lb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isQPDiscardWhitespace(r rune) bool {
|
||||||
|
switch r {
|
||||||
|
case '\n', '\r', ' ', '\t':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
crlf = []byte("\r\n")
|
||||||
|
lf = []byte("\n")
|
||||||
|
softSuffix = []byte("=")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read reads and decodes quoted-printable data from the underlying reader.
|
||||||
|
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||||
|
// Deviations from RFC 2045:
|
||||||
|
// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
|
||||||
|
// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
|
||||||
|
// with other broken QP encoders & decoders.
|
||||||
|
for len(p) > 0 {
|
||||||
|
if len(r.line) == 0 {
|
||||||
|
if r.rerr != nil {
|
||||||
|
return n, r.rerr
|
||||||
|
}
|
||||||
|
r.line, r.rerr = r.br.ReadSlice('\n')
|
||||||
|
|
||||||
|
// Does the line end in CRLF instead of just LF?
|
||||||
|
hasLF := bytes.HasSuffix(r.line, lf)
|
||||||
|
hasCR := bytes.HasSuffix(r.line, crlf)
|
||||||
|
wholeLine := r.line
|
||||||
|
r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
|
||||||
|
if bytes.HasSuffix(r.line, softSuffix) {
|
||||||
|
rightStripped := wholeLine[len(r.line):]
|
||||||
|
r.line = r.line[:len(r.line)-1]
|
||||||
|
if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
|
||||||
|
r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped)
|
||||||
|
}
|
||||||
|
} else if hasLF {
|
||||||
|
if hasCR {
|
||||||
|
r.line = append(r.line, '\r', '\n')
|
||||||
|
} else {
|
||||||
|
r.line = append(r.line, '\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b := r.line[0]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case b == '=':
|
||||||
|
if len(r.line[1:]) < 2 {
|
||||||
|
return n, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b, err = readHexByte(r.line[1], r.line[2])
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
r.line = r.line[2:] // 2 of the 3; other 1 is done below
|
||||||
|
case b == '\t' || b == '\r' || b == '\n':
|
||||||
|
break
|
||||||
|
case b < ' ' || b > '~':
|
||||||
|
return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b)
|
||||||
|
}
|
||||||
|
p[0] = b
|
||||||
|
p = p[1:]
|
||||||
|
r.line = r.line[1:]
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
166
vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
generated
vendored
Normal file
166
vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
generated
vendored
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package quotedprintable
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
const lineMaxLen = 76
|
||||||
|
|
||||||
|
// A Writer is a quoted-printable writer that implements io.WriteCloser.
|
||||||
|
type Writer struct {
|
||||||
|
// Binary mode treats the writer's input as pure binary and processes end of
|
||||||
|
// line bytes as binary data.
|
||||||
|
Binary bool
|
||||||
|
|
||||||
|
w io.Writer
|
||||||
|
i int
|
||||||
|
line [78]byte
|
||||||
|
cr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWriter returns a new Writer that writes to w.
|
||||||
|
func NewWriter(w io.Writer) *Writer {
|
||||||
|
return &Writer{w: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write encodes p using quoted-printable encoding and writes it to the
|
||||||
|
// underlying io.Writer. It limits line length to 76 characters. The encoded
|
||||||
|
// bytes are not necessarily flushed until the Writer is closed.
|
||||||
|
func (w *Writer) Write(p []byte) (n int, err error) {
|
||||||
|
for i, b := range p {
|
||||||
|
switch {
|
||||||
|
// Simple writes are done in batch.
|
||||||
|
case b >= '!' && b <= '~' && b != '=':
|
||||||
|
continue
|
||||||
|
case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if i > n {
|
||||||
|
if err := w.write(p[n:i]); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
n = i
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.encode(b); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == len(p) {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.write(p[n:]); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the Writer, flushing any unwritten data to the underlying
|
||||||
|
// io.Writer, but does not close the underlying io.Writer.
|
||||||
|
func (w *Writer) Close() error {
|
||||||
|
if err := w.checkLastByte(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// write limits text encoded in quoted-printable to 76 characters per line.
|
||||||
|
func (w *Writer) write(p []byte) error {
|
||||||
|
for _, b := range p {
|
||||||
|
if b == '\n' || b == '\r' {
|
||||||
|
// If the previous byte was \r, the CRLF has already been inserted.
|
||||||
|
if w.cr && b == '\n' {
|
||||||
|
w.cr = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == '\r' {
|
||||||
|
w.cr = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.checkLastByte(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := w.insertCRLF(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.i == lineMaxLen-1 {
|
||||||
|
if err := w.insertSoftLineBreak(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.line[w.i] = b
|
||||||
|
w.i++
|
||||||
|
w.cr = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) encode(b byte) error {
|
||||||
|
if lineMaxLen-1-w.i < 3 {
|
||||||
|
if err := w.insertSoftLineBreak(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.line[w.i] = '='
|
||||||
|
w.line[w.i+1] = upperhex[b>>4]
|
||||||
|
w.line[w.i+2] = upperhex[b&0x0f]
|
||||||
|
w.i += 3
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkLastByte encodes the last buffered byte if it is a space or a tab.
|
||||||
|
func (w *Writer) checkLastByte() error {
|
||||||
|
if w.i == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := w.line[w.i-1]
|
||||||
|
if isWhitespace(b) {
|
||||||
|
w.i--
|
||||||
|
if err := w.encode(b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) insertSoftLineBreak() error {
|
||||||
|
w.line[w.i] = '='
|
||||||
|
w.i++
|
||||||
|
|
||||||
|
return w.insertCRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) insertCRLF() error {
|
||||||
|
w.line[w.i] = '\r'
|
||||||
|
w.line[w.i+1] = '\n'
|
||||||
|
w.i += 2
|
||||||
|
|
||||||
|
return w.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) flush() error {
|
||||||
|
if _, err := w.w.Write(w.line[:w.i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.i = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWhitespace(b byte) bool {
|
||||||
|
return b == ' ' || b == '\t'
|
||||||
|
}
|
||||||
17
vendor/gopkg.in/mail.v2/.gitignore
generated
vendored
Normal file
17
vendor/gopkg.in/mail.v2/.gitignore
generated
vendored
Normal file
@@ -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/
|
||||||
25
vendor/gopkg.in/mail.v2/.travis.yml
generated
vendored
Normal file
25
vendor/gopkg.in/mail.v2/.travis.yml
generated
vendored
Normal file
@@ -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
|
||||||
88
vendor/gopkg.in/mail.v2/CHANGELOG.md
generated
vendored
Normal file
88
vendor/gopkg.in/mail.v2/CHANGELOG.md
generated
vendored
Normal file
@@ -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 <https://github.com/go-gomail/gomail/> to
|
||||||
|
<https://github.com/go-mail/mail/>.
|
||||||
|
|
||||||
|
### 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.
|
||||||
20
vendor/gopkg.in/mail.v2/CONTRIBUTING.md
generated
vendored
Normal file
20
vendor/gopkg.in/mail.v2/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Thank you for contributing to Gomail! Here are a few guidelines:
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
|
||||||
|
If you think you found a bug, create an issue and supply the minimum amount
|
||||||
|
of code triggering the bug so it can be reproduced.
|
||||||
|
|
||||||
|
|
||||||
|
## Fixing a bug
|
||||||
|
|
||||||
|
If you want to fix a bug, you can send a pull request. It should contains a
|
||||||
|
new test or update an existing one to cover that bug.
|
||||||
|
|
||||||
|
|
||||||
|
## New feature proposal
|
||||||
|
|
||||||
|
If you think Gomail lacks a feature, you can open an issue or send a pull
|
||||||
|
request. I want to keep Gomail code and API as simple as possible so please
|
||||||
|
describe your needs so we can discuss whether this feature should be added to
|
||||||
|
Gomail or not.
|
||||||
20
vendor/gopkg.in/mail.v2/LICENSE
generated
vendored
Normal file
20
vendor/gopkg.in/mail.v2/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Alexandre Cesaro
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
129
vendor/gopkg.in/mail.v2/README.md
generated
vendored
Normal file
129
vendor/gopkg.in/mail.v2/README.md
generated
vendored
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Gomail
|
||||||
|
[](https://travis-ci.org/go-mail/mail) [](http://gocover.io/github.com/go-mail/mail) [](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.
|
||||||
49
vendor/gopkg.in/mail.v2/auth.go
generated
vendored
Normal file
49
vendor/gopkg.in/mail.v2/auth.go
generated
vendored
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
vendor/gopkg.in/mail.v2/doc.go
generated
vendored
Normal file
6
vendor/gopkg.in/mail.v2/doc.go
generated
vendored
Normal file
@@ -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
|
||||||
16
vendor/gopkg.in/mail.v2/errors.go
generated
vendored
Normal file
16
vendor/gopkg.in/mail.v2/errors.go
generated
vendored
Normal file
@@ -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)
|
||||||
|
}
|
||||||
359
vendor/gopkg.in/mail.v2/message.go
generated
vendored
Normal file
359
vendor/gopkg.in/mail.v2/message.go
generated
vendored
Normal file
@@ -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)
|
||||||
|
}
|
||||||
21
vendor/gopkg.in/mail.v2/mime.go
generated
vendored
Normal file
21
vendor/gopkg.in/mail.v2/mime.go
generated
vendored
Normal file
@@ -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
|
||||||
|
)
|
||||||
25
vendor/gopkg.in/mail.v2/mime_go14.go
generated
vendored
Normal file
25
vendor/gopkg.in/mail.v2/mime_go14.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
116
vendor/gopkg.in/mail.v2/send.go
generated
vendored
Normal file
116
vendor/gopkg.in/mail.v2/send.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
292
vendor/gopkg.in/mail.v2/smtp.go
generated
vendored
Normal file
292
vendor/gopkg.in/mail.v2/smtp.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
313
vendor/gopkg.in/mail.v2/writeto.go
generated
vendored
Normal file
313
vendor/gopkg.in/mail.v2/writeto.go
generated
vendored
Normal file
@@ -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
|
||||||
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
@@ -1228,6 +1228,9 @@ google.golang.org/protobuf/types/known/anypb
|
|||||||
google.golang.org/protobuf/types/known/durationpb
|
google.golang.org/protobuf/types/known/durationpb
|
||||||
google.golang.org/protobuf/types/known/timestamppb
|
google.golang.org/protobuf/types/known/timestamppb
|
||||||
google.golang.org/protobuf/types/pluginpb
|
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
|
# gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d
|
||||||
## explicit
|
## explicit
|
||||||
gopkg.in/asn1-ber.v1
|
gopkg.in/asn1-ber.v1
|
||||||
@@ -1245,6 +1248,9 @@ gopkg.in/ini.v1
|
|||||||
# gopkg.in/ldap.v3 v3.0.3
|
# gopkg.in/ldap.v3 v3.0.3
|
||||||
## explicit
|
## explicit
|
||||||
gopkg.in/ldap.v3
|
gopkg.in/ldap.v3
|
||||||
|
# gopkg.in/mail.v2 v2.3.1
|
||||||
|
## explicit
|
||||||
|
gopkg.in/mail.v2
|
||||||
# gopkg.in/yaml.v2 v2.4.0
|
# gopkg.in/yaml.v2 v2.4.0
|
||||||
## explicit; go 1.15
|
## explicit; go 1.15
|
||||||
gopkg.in/yaml.v2
|
gopkg.in/yaml.v2
|
||||||
|
|||||||
Reference in New Issue
Block a user