Repository: ovh/cds (go)

Issue:
Mail notification and UTF-8 "Title"
If an email notification is created with a Title containing UTF-8 chars, like:
```
[release 🚀] {{.git.message}}
```
the encoding is not declared in the Subject header, so the mail reader can not render it correctly:
```
[release ðŸš€] …
```
To avoid doing it by ourselves:
```
=?UTF-8?Q?[release =f0=9f=9a=80]?= {{.git.message}}
```
Shouldn't the mail subject be automatically encoded using RFC 2047 if an UTF-8 character is detected?

Pull request:
fix(api): support utf8 in mail subject
close #5351

Signed-off-by: francois  samin <francois.samin@corp.ovh.com>

Patch:
diff --git a/engine/api/mail/mail.go b/engine/api/mail/mail.go
index 69650c59d..26a774e24 100644
--- a/engine/api/mail/mail.go
+++ b/engine/api/mail/mail.go
@@ -90,28 +90,30 @@ func smtpClient(ctx context.Context) (*smtp.Client, error) {
 		// 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, errc := tls.Dial("tcp", servername, tlsconfig)
-		if errc != nil {
-			log.Warning(ctx, "Error with c.Dial:%s\n", errc.Error())
-			return nil, errc
+		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, err
+			return nil, sdk.WithStack(err)
 		}
 		// TLS config
 		tlsconfig := &tls.Config{
 			InsecureSkipVerify: false,
 			ServerName:         smtpHost,
 		}
-		c.StartTLS(tlsconfig)
+		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, err
+			return nil, sdk.WithStack(err)
 		}
 	}
 
@@ -183,7 +185,12 @@ func SendEmail(ctx context.Context, subject string, mailContent *bytes.Buffer, u
 	headers := make(map[string]string)
 	headers["From"] = smtpFrom
 	headers["To"] = to.String()
-	headers["Subject"] = subject
+	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)
diff --git a/engine/api/router_middleware_auth.go b/engine/api/router_middleware_auth.go
index c2e838917..2ce6fc8b3 100644
--- a/engine/api/router_middleware_auth.go
+++ b/engine/api/router_middleware_auth.go
@@ -169,21 +169,18 @@ func (api *API) xsrfMiddleware(ctx context.Context, w http.ResponseWriter, req *
 	ctx, end := telemetry.Span(ctx, "router.xsrfMiddleware")
 	defer end()
 
+	// If no consumer in the context, means that the route is not authentified, we don't need to check the xsrf token.
+	if getAPIConsumer(ctx) == nil {
+		return ctx, nil
+	}
+
 	jwtFromCookieVal := ctx.Value(service.ContextJWTFromCookie)
 	jwtFromCookie, _ := jwtFromCookieVal.(bool)
 	if !jwtFromCookie {
 		return ctx, nil
 	}
 
-	sessionValue := ctx.Value(contextSession)
-	if sessionValue == nil {
-		return ctx, sdk.WithStack(sdk.ErrUnauthorized)
-	}
-
-	session, ok := sessionValue.(*sdk.AuthSession)
-	if !ok {
-		return ctx, sdk.WithStack(sdk.ErrUnauthorized)
-	}
+	session := getAuthSession(ctx)
 
 	xsrfToken := req.Header.Get(xsrfHeaderName)
 	existingXSRFToken, existXSRFTokenInCache := authentication.GetSessionXSRFToken(api.Cache, session.ID)
diff --git a/sdk/common.go b/sdk/common.go
index 266d636d2..59b914ad3 100644
--- a/sdk/common.go
+++ b/sdk/common.go
@@ -267,6 +267,15 @@ func (s *Int64Slice) Remove(v int64) {
 	*s = filtered
 }
 
+func StringIsAscii(s string) bool {
+	for _, r := range s {
+		if r > unicode.MaxASCII {
+			return false
+		}
+	}
+	return true
+}
+
 func RemoveNotPrintableChar(in string) string {
 	m := func(r rune) rune {
 		switch {
diff --git a/ui/src/app/store/feature.state.ts b/ui/src/app/store/feature.state.ts
index f2929e2bb..cff2e1176 100644
--- a/ui/src/app/store/feature.state.ts
+++ b/ui/src/app/store/feature.state.ts
@@ -35,11 +35,13 @@ export class FeatureState {
     addFeatureResult(ctx: StateContext<FeatureStateModel>, action: actionFeature.AddFeatureResult) {
         const state = ctx.getState();
 
-        let feature = state.features.find(f => f.key === action.payload.key);
-        if (!feature) {
-            feature = <FeatureResults>{ key: action.payload.key, results: [] };
+        let feature = <FeatureResults>{ key: action.payload.key, results: [] };
+
+        let existingFeature = state.features.find(f => f.key === action.payload.key);
+        if (existingFeature) {
+            feature.results = existingFeature.results.filter(r => r.paramString !== action.payload.result.paramString);
         }
-        feature.results = feature.results.filter(r => r.paramString !== action.payload.result.paramString).concat(action.payload.result);
+        feature.results.push(action.payload.result)
 
         ctx.setState({
             ...state,


Test patch:
diff --git a/sdk/common_test.go b/sdk/common_test.go
index acd62275e..55d672c74 100644
--- a/sdk/common_test.go
+++ b/sdk/common_test.go
@@ -7,6 +7,11 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func TestStringIsAscii(t *testing.T) {
+	assert.True(t, StringIsAscii("aaa"))
+	assert.False(t, StringIsAscii("aaa 🚀"))
+}
+
 func TestRemoveNotPrintableChar(t *testing.T) {
 	var tests = []struct {
 		in  string