Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(api): sendmail utf-8 #5472

Merged
merged 7 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 34 additions & 131 deletions engine/api/mail/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"net/mail"
"net/smtp"
"sync/atomic"
"text/template"
"time"

"github.com/jordan-wright/email"
"github.com/ovh/cds/sdk"
"github.com/ovh/cds/sdk/log"
)

var smtpUser, smtpPassword, smtpFrom, smtpHost, smtpPort string
var smtpTLS, smtpEnable bool
var lastError error
var counter uint64

const templateSignedup = `Welcome to CDS,

Expand Down Expand Up @@ -64,69 +64,13 @@ func Init(user, password, from, host, port string, tls, disable bool) {

// Status verification of smtp configuration, returns OK or KO
func Status(ctx context.Context) sdk.MonitoringStatusLine {
if _, err := smtpClient(ctx); err != nil {
return sdk.MonitoringStatusLine{Component: "SMTP Ping", Value: "KO: " + err.Error(), Status: sdk.MonitoringStatusAlert}
}
return sdk.MonitoringStatusLine{Component: "SMTP Ping", Value: "Connect OK", Status: sdk.MonitoringStatusOK}
}

func smtpClient(ctx context.Context) (*smtp.Client, error) {
if smtpHost == "" || smtpPort == "" || !smtpEnable {
return nil, errors.New("No SMTP configuration")
}

// Connect to the SMTP Server
servername := fmt.Sprintf("%s:%s", smtpHost, smtpPort)

// TLS config
tlsconfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: smtpHost,
}

var c *smtp.Client
var err error
if smtpTLS {
// Here is the key, you need to call tls.Dial instead of smtp.Dial
// for smtp servers running on 465 that require an ssl connection
// from the very beginning (no starttls)
conn, err := tls.Dial("tcp", servername, tlsconfig)
if err != nil {
log.Warning(ctx, "Error with c.Dial:%s\n", err.Error())
return nil, sdk.WithStack(err)
}

c, err = smtp.NewClient(conn, smtpHost)
if err != nil {
log.Warning(ctx, "Error with c.NewClient:%s\n", err.Error())
return nil, sdk.WithStack(err)
}
// TLS config
tlsconfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: smtpHost,
}
if err := c.StartTLS(tlsconfig); err != nil {
return nil, sdk.WithStack(err)
}
} else {
c, err = smtp.Dial(servername)
if err != nil {
log.Warning(ctx, "Error with c.NewClient:%s\n", err.Error())
return nil, sdk.WithStack(err)
}
if !smtpEnable {
return sdk.MonitoringStatusLine{Component: "SMTP Ping", Value: "Conf: SMTP Disabled", Status: sdk.MonitoringStatusWarn}
}

// Auth
if smtpUser != "" && smtpPassword != "" {
auth := smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost)
if err = c.Auth(auth); err != nil {
log.Warning(ctx, "Error with c.Auth:%s\n", err.Error())
c.Close()
return nil, err
}
if lastError != nil {
return sdk.MonitoringStatusLine{Component: "SMTP Ping", Value: "KO: " + lastError.Error(), Status: sdk.MonitoringStatusAlert}
}
return c, nil
return sdk.MonitoringStatusLine{Component: "SMTP Ping", Value: fmt.Sprintf("OK (%d sent)", counter), Status: sdk.MonitoringStatusOK}
}

// SendMailVerifyToken send mail to verify user account.
Expand Down Expand Up @@ -172,83 +116,42 @@ func createTemplate(templ, callbackURL, callbackAPIURL, username, token string)

//SendEmail is the core function to send an email
func SendEmail(ctx context.Context, subject string, mailContent *bytes.Buffer, userMail string, isHTML bool) error {
from := mail.Address{
Name: "",
Address: smtpFrom,
}
to := mail.Address{
Name: "",
Address: userMail,
}

// Setup headers
headers := make(map[string]string)
headers["From"] = smtpFrom
headers["To"] = to.String()
if sdk.StringIsAscii(subject) {
headers["Subject"] = subject
} else {
// https://tools.ietf.org/html/rfc2047
headers["Subject"] = "=?UTF-8?Q?" + subject + "?="
}

// https://tools.ietf.org/html/rfc4021
headers["Date"] = time.Now().Format(time.RFC1123Z)

// https://tools.ietf.org/html/rfc2392
headers["Message-ID"] = fmt.Sprintf("<%d.%s>", time.Now().UnixNano(), smtpFrom)

e := email.NewEmail()
e.From = smtpFrom
e.To = []string{userMail}
e.Subject = subject
e.Text = mailContent.Bytes()
if isHTML {
headers["Content-Type"] = `text/html; charset="utf-8"`
}

// Setup message
message := ""
for k, v := range headers {
message += fmt.Sprintf("%s: %s\r\n", k, v)
e.HTML = mailContent.Bytes()
}
message += "\r\n" + mailContent.String()

if !smtpEnable {
fmt.Println("##### NO SMTP DISPLAY MAIL IN CONSOLE ######")
fmt.Printf("Subject:%s\n", subject)
fmt.Printf("Text:%s\n", message)
fmt.Printf("Text:%s\n", string(e.Text))
fmt.Println("##### END MAIL ######")
return nil
}

c, err := smtpClient(ctx)
if err != nil {
return sdk.WrapError(err, "Cannot get smtp client")
}
defer c.Close()

// To && From
if err = c.Mail(from.Address); err != nil {
return sdk.WrapError(err, "Error with c.Mail")
}

if err = c.Rcpt(to.Address); err != nil {
return sdk.WrapError(err, "Error with c.Rcpt")
}

// Data
w, err := c.Data()
if err != nil {
return sdk.WrapError(err, "Error with c.Data")
servername := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
var auth smtp.Auth
if smtpUser != "" && smtpPassword != "" {
auth = smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost)
}

_, err = w.Write([]byte(message))
if err != nil {
return sdk.WrapError(err, "Error with c.Write")
var err error
if smtpTLS {
tlsconfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: smtpHost,
}
err = e.SendWithStartTLS(servername, auth, tlsconfig)
} else {
err = e.Send(servername, auth)
}

err = w.Close()
if err != nil {
return sdk.WrapError(err, "Error with c.Close")
lastError = err
} else {
atomic.AddUint64(&counter, 1)
lastError = nil
}

c.Quit()

return nil
return err
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ require (
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/itsjamie/gin-cors v0.0.0-20160420130702-97b4a9da7933
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/keybase/go-crypto v0.0.0-20181127160227-255a5089e85a
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5i
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible h1:CL0ooBNfbNyJTJATno+m0h+zM5bW6v7fKlboKUGP/dI=
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
Expand Down
2 changes: 1 addition & 1 deletion tests/01_signup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ testcases:
delay: 3
vars:
verify:
from: result.bodyjson.content
from: result.bodyjson.contentdecoded
regex: cdsctl signup verify --api-url (?:.*) (.*)


Expand Down
2 changes: 1 addition & 1 deletion tests/04_sc_workflow_run_notif.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ testcases:
delay: 3
vars:
verify:
from: result.bodyjson.content
from: result.bodyjson.contentdecoded
regex: logcontent:foo2

- name: run workflow 04SCWorkflowRunNotif-WORKFLOW-EMPTY
Expand Down
13 changes: 7 additions & 6 deletions tools/smtpmock/message.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package smtpmock

type Message struct {
FromAgent string `json:"from-agent"`
RemoteAddress string `json:"remote-address"`
User string `json:"user"`
From string `json:"from"`
To string `json:"to"`
Content string `json:"content"`
FromAgent string `json:"from-agent"`
RemoteAddress string `json:"remote-address"`
User string `json:"user"`
From string `json:"from"`
To string `json:"to"`
Content string `json:"content"`
ContentDecoded string `json:"content-decoded"`
}
11 changes: 10 additions & 1 deletion tools/smtpmock/server/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"fmt"
"io/ioutil"
"mime/quotedprintable"
"strings"

"github.com/fsamin/smtp"

"github.com/ovh/cds/tools/smtpmock"
)

Expand All @@ -33,6 +35,13 @@ func smtpHandler(envelope *smtp.Envelope) error {

m.Content = string(btes)

r := quotedprintable.NewReader(strings.NewReader(m.Content))
b, err := ioutil.ReadAll(r)
if err != nil {
return err
}
m.ContentDecoded = string(b)

StoreAddMessage(m)

return nil
Expand Down