Skip to content

Commit

Permalink
fix(api): sendmail utf-8 (#5472)
Browse files Browse the repository at this point in the history
* fix(api): sendmail utf-8

fix #5351

Signed-off-by: Yvonnick Esnault <[email protected]>
  • Loading branch information
yesnault authored Oct 5, 2020
1 parent ff2aedf commit b7205f5
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 140 deletions.
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 @@ -58,6 +58,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 @@ -268,6 +268,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.content-decoded
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.content-decoded
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

0 comments on commit b7205f5

Please sign in to comment.