diff --git a/TODO.md b/TODO.md
index 323b7afdba378689b6f159f38294bd30d7b7c05c..ba8d5f96d5c19e0932f539c8b04b5de28b9dbff7 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,6 +1,5 @@
 [ ] Tests : matcher, mandatement and missing FC
 [ ] OIDC : correct id token and user info
-[ ] Remove useless fmt.Print
 [ ] Business : SIRET handling (work out SIREN for mandatement)
 [ ] Business : mandate duration
 [ ] Business : allow user to see his mandates and delete them
@@ -8,3 +7,4 @@
 [ ] Admin view, with Client ID and Client Secret Management
 [ ] TODOs scattered around the code
 [ ] UI : message animations
+[ ] Remove useless fmt.Print / console.log
diff --git a/internal/clientstub/main.js b/internal/clientstub/main.js
index cb1441e5f63568e1a68c36a84d91d40d087707fa..d715211750006ad840b9eaffe471fb53f48a034c 100644
--- a/internal/clientstub/main.js
+++ b/internal/clientstub/main.js
@@ -5,18 +5,16 @@ document.addEventListener("DOMContentLoaded", async () => {
   });
   const query = new URLSearchParams(window.location.search);
   const code = query.get("code");
-  console.log(code);
   // Exchange the code for an id token (not secure, for demo purposes ONLY, since client ID is not checked)
   if (query != undefined && query != "") {
     try {
-      const response = await fetch(
-        "http://localhost:8080/api/oidc/token?code=" +
+      const response = await fetch("http://localhost:8080/api/oidc/token", {
+        method: "POST",
+        body:
+          "code=" +
           encodeURIComponent(code) +
           "&client_id=an_id&client_secret=a_secret&grant_type=authorization_code",
-        {
-          method: "GET",
-        }
-      );
+      });
       if (response.status !== 200) {
         throw new Error(`Could not get token (status ${response.status})`);
       }
diff --git a/internal/matcher/matcher.go b/internal/matcher/matcher.go
index 341f944346d24f6b750cf7bdf47edf26e3985128..8dd7b2491dfdb6194e1450390f16b819843c1312 100644
--- a/internal/matcher/matcher.go
+++ b/internal/matcher/matcher.go
@@ -6,7 +6,6 @@ import (
 	"html/template"
 	"io/ioutil"
 	"net/http"
-	"net/smtp"
 	"regexp"
 	"time"
 
@@ -15,16 +14,21 @@ import (
 	"forge.grandlyon.com/npernoud/glcpro/internal/mandate"
 	"forge.grandlyon.com/npernoud/glcpro/internal/oidcserver"
 	"forge.grandlyon.com/npernoud/glcpro/pkg/common"
+	"forge.grandlyon.com/npernoud/glcpro/pkg/email"
 	"forge.grandlyon.com/npernoud/glcpro/pkg/tokens"
 )
 
 var (
-	mailTemplate        = "mailtemplate.html"
-	emailSenderAddress  = common.StringValueFromEnv("EMAIL_SENDER_ADDRESS", "")
-	emailSenderPassword = common.StringValueFromEnv("EMAIL_SENDER_PASSWORD", "")
-	emailSMTPServer     = common.StringValueFromEnv("EMAIL_SMTP_SERVER", "")
-	emailSMTPPort       = common.StringValueFromEnv("EMAIL_SMTP_PORT", "")
-	emailRegex          = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
+	mailTemplate = "mailtemplate.html"
+	mailConfig   = email.EmailConfig{
+		Username:   common.StringValueFromEnv("EMAIL_SENDER_ADDRESS", ""),
+		Password:   common.StringValueFromEnv("EMAIL_SENDER_PASSWORD", ""),
+		ServerHost: common.StringValueFromEnv("EMAIL_SMTP_SERVER", ""),
+		ServerPort: common.StringValueFromEnv("EMAIL_SMTP_PORT", ""),
+		SenderAddr: common.StringValueFromEnv("EMAIL_SENDER_ADDRESS", ""),
+	}
+	mailSender = email.NewSender(mailConfig)
+	emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
 )
 
 type MandateDemand struct {
@@ -144,9 +148,6 @@ func sendMailToMandater(email string, mandate string, sirent string, token strin
 		email,
 	}
 
-	// Authentication.
-	auth := smtp.PlainAuth("", emailSenderAddress, emailSenderPassword, emailSMTPServer)
-
 	t, _ := template.ParseFiles(mailTemplate)
 
 	var body bytes.Buffer
@@ -167,5 +168,5 @@ func sendMailToMandater(email string, mandate string, sirent string, token strin
 	})
 
 	// Sending email.
-	return smtp.SendMail(emailSMTPServer+":"+emailSMTPPort, auth, emailSenderAddress, to, body.Bytes())
+	return mailSender.Send(to, body.Bytes())
 }
diff --git a/internal/matcher/matcher_test.go b/internal/matcher/matcher_test.go
index 44ec96a6773e7851aa4641c4b3009bc6c89bfe7f..5a9371ef0e8538fdb3b2b95b3f084117893f90da 100644
--- a/internal/matcher/matcher_test.go
+++ b/internal/matcher/matcher_test.go
@@ -1,9 +1,16 @@
 package matcher
 
-import "testing"
+import (
+	"strings"
+	"testing"
+
+	"forge.grandlyon.com/npernoud/glcpro/pkg/email"
+)
 
 func Test_sendMailToMandater(t *testing.T) {
 	mailTemplate = "../../mailtemplate.html"
+	sender, recorder := email.NewMockSender()
+	mailSender = sender
 	type args struct {
 		email   string
 		mandate string
@@ -22,6 +29,9 @@ func Test_sendMailToMandater(t *testing.T) {
 			if err := sendMailToMandater(tt.args.email, tt.args.mandate, tt.args.sirent, tt.args.token, "http://localhost"); (err != nil) != tt.wantErr {
 				t.Errorf("sendMailToMandater() error = %v, wantErr %v", err, tt.wantErr)
 			}
+			if !strings.Contains(recorder.Msg(), "Demande de mandatement") || !strings.Contains(recorder.Msg(), "http://localhost") {
+				t.Errorf("received body is not what is expected")
+			}
 		})
 	}
 }
diff --git a/internal/matcher/test.sh b/internal/matcher/test.sh
deleted file mode 100755
index 391faadfe8881a4a1795f72c49f105be1c67d418..0000000000000000000000000000000000000000
--- a/internal/matcher/test.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-
-export EMAIL_SENDER_ADDRESS=glcpro@alpha.grandlyon.com
-export EMAIL_SENDER_PASSWORD=***
-export EMAIL_SMTP_SERVER=mail.alpha.grandlyon.com
-export EMAIL_SMTP_PORT=587
-go test .
diff --git a/pkg/email/email.go b/pkg/email/email.go
new file mode 100644
index 0000000000000000000000000000000000000000..4259f72394130f631ef24da057756500c718a6be
--- /dev/null
+++ b/pkg/email/email.go
@@ -0,0 +1,57 @@
+package email
+
+import (
+	"net/smtp"
+)
+
+type EmailConfig struct {
+	Username   string
+	Password   string
+	ServerHost string
+	ServerPort string
+	SenderAddr string
+}
+
+type EmailSender interface {
+	Send(to []string, body []byte) error
+}
+
+func NewSender(conf EmailConfig) EmailSender {
+	return &emailSender{conf, smtp.SendMail}
+}
+
+type emailSender struct {
+	conf EmailConfig
+	send func(string, smtp.Auth, string, []string, []byte) error
+}
+
+func (e *emailSender) Send(to []string, body []byte) error {
+	addr := e.conf.ServerHost + ":" + e.conf.ServerPort
+	auth := smtp.PlainAuth("", e.conf.Username, e.conf.Password, e.conf.ServerHost)
+	return e.send(addr, auth, e.conf.SenderAddr, to, body)
+}
+
+func NewMockSender() (EmailSender, *EmailRecorder) {
+	f, r := mockSend(nil)
+	return &emailSender{send: f}, r
+}
+
+func mockSend(errToReturn error) (func(string, smtp.Auth, string, []string, []byte) error, *EmailRecorder) {
+	r := new(EmailRecorder)
+	return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
+		*r = EmailRecorder{addr, a, from, to, msg}
+		return errToReturn
+	}, r
+}
+
+type EmailRecorder struct {
+	addr string
+	auth smtp.Auth
+	from string
+	to   []string
+	msg  []byte
+}
+
+func (r *EmailRecorder) Msg() string {
+	return string(r.msg)
+}
diff --git a/pkg/email/email_test.go b/pkg/email/email_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..dbedcc9e8037a13645f539541a56b291f03c45c9
--- /dev/null
+++ b/pkg/email/email_test.go
@@ -0,0 +1,18 @@
+package email
+
+import (
+	"testing"
+)
+
+func TestEmail_SendSuccessful(t *testing.T) {
+	sender, recorder := NewMockSender()
+	body := "Hello World"
+	err := sender.Send([]string{"me@example.com"}, []byte(body))
+
+	if err != nil {
+		t.Errorf("unexpected error: %s", err)
+	}
+	if string(recorder.msg) != body {
+		t.Errorf("wrong message body.\n\nexpected: %v\n got: %s", body, recorder.msg)
+	}
+}