Skip to content
Snippets Groups Projects
Commit c27359ab authored by Quentin DAUCHY's avatar Quentin DAUCHY Committed by Quentin DAUCHY
Browse files

Add mails library

parent b22dadec
No related branches found
No related tags found
1 merge request!5115 mails lib
Pipeline #
......@@ -9,6 +9,7 @@ Unit tests:
- strong # Disable this build on small runners
services:
- "lesterpig/mongo:latest" # Use this light version of mongo
- "lesterpig/postfix:latest"
#artifacts: # Waiting GitLab 8.2.1...
# paths:
# - "coverage.html"
......@@ -16,6 +17,7 @@ Unit tests:
- "go get gopkg.in/mgo.v2"
- "go test -coverprofile auth.part -v ./auth"
- "go test -coverprofile mgdb.part -v ./mgdb"
- "go test -coverprofile mails.part -v ./mails"
- "echo 'mode: set' *part > c.out"
- "grep -h -v 'mode: set' *part >> c.out"
- "go tool cover -html=c.out -o coverage.html"
......@@ -26,7 +28,8 @@ ARM tests:
- arm
script:
- "go get gopkg.in/mgo.v2"
- "go test -cover -short -run 'Test[^M][^o][^n][^g][^o]' -v ./..."
- "go test -cover -short -v ./auth"
- "go test -cover -short -v ./mgdb"
Code lint:
stage: test
......@@ -38,4 +41,4 @@ Code lint:
- "go get github.com/alecthomas/gometalinter"
- "go get gopkg.in/mgo.v2"
- "gometalinter --install"
- "gometalinter -t --deadline=60s ./..."
- "gometalinter -t --deadline=100s -j1 ./..."
### DFSS - Mails lib ###
This library is designed to wrap the smtp library of go.
## Initiating a connection to a server ##
To start a connection to a server, create a CustomClient via NewCustomClient
This takes :
- A sender (ex : qdauchy@insa-rennes.fr)
- A host (ex : mailhost.insa-rennes.fr)
- A port (ex : 587)
- A user (ex : qdauchy)
- A password
This requires the server to have TLS
## Using the connection ##
The connection that has been created can then be used to send one or several mails
Using Send requires :
- A slice of receivers
- A subject
- A message
- A (possibly empty) slice of extensions
- A (possibly empty) slice of filenames. This slice must be of the same length as the extensions one.
## Closing the connection ##
Finally, close the connection using Close.
## Example ###
Refer to the doc's to see the library in practice
## Testing the library ##
The testing file uses the following variables to set up the tests :
DFSS_TEST_MAIL_SENDER
DFSS_TEST_MAIL_HOST
DFSS_TEST_MAIL_PORT
DFSS_TEST_MAIL_USER
DFSS_TEST_MAIL_PASSWORD
DFSS_TEST_MAIL_RCPT1
DFSS_TEST_MAIL_RCPT2
Modify the header so that the spam score is lower (9.986 is cutting it close)
Document
// Package mails provides a simple interface with the smtp library
package mails
import (
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/smtp"
"net/textproto"
"strings"
"time"
)
// CustomClient :
// Modelizes the constants of a connection : an actual client, and a sender
type CustomClient struct {
sender string
client *smtp.Client
}
const rfc2822 = "Fri 18 Dec 2015 10:01:17 -0606" // used to format time to rfc2822. Not accurate but fmt can't see a ,
// NewCustomClient starts up a custom client.
func NewCustomClient(sender, host, port, username, password string) (*CustomClient, error) {
// Connect to the server. Type of connection is smtp.Client
connection, err := smtp.Dial(host + ":" + port)
if err != nil {
return nil, err
}
// Check that the server does implement TLS
if ok, _ := connection.Extension("StartTLS"); !ok {
return nil, errors.New("Connection failed : mail server doesn't support TLS")
}
// Start tls
if err := connection.StartTLS(&tls.Config{InsecureSkipVerify: true, ServerName: host}); err != nil {
return nil, err
}
// Authenticate to the server if it is supported
if ok, _ := connection.Extension("AUTH"); ok {
auth := smtp.PlainAuth(username, username, password, host)
if err := connection.Auth(auth); err != nil {
return nil, err
}
}
return &CustomClient{sender, connection}, nil
}
// Send a mail with the custom client. Returns nil on success.
func (c *CustomClient) Send(receivers []string, subject, message string, extensions, filenames []string) error {
// Keep the connection in a local variable for ease of access
connection := c.client
boundary := randomBoundary()
header := createHeader(c.sender, subject, boundary)
// Encode the message in base64 ONCE
base64Message := base64.StdEncoding.EncodeToString([]byte(message))
for _, receiver := range receivers {
// Set the sender
if err := connection.Mail(c.sender); err != nil {
return err
}
// Set the receiver. This modifies the header in the current instance
if err := connection.Rcpt(receiver); err != nil {
return err
}
// Set the message : header, then message encoded in base64, then attachments
var localBuffer bytes.Buffer
if err := createFullMessage(&localBuffer, receiver, c.sender, header, base64Message, extensions, filenames, boundary); err != nil {
return err
}
// Send it. Data returns a writer to which one can write to write the message itself
emailWriter, err := connection.Data()
if err != nil {
return err
}
_, err = fmt.Fprintf(emailWriter, localBuffer.String())
if err != nil {
return err
}
err = emailWriter.Close()
if err != nil {
return err
}
// Reset the envellope
err = connection.Reset()
if err != nil {
return err
}
}
return nil
}
// Close the connection of CustomClient
func (c *CustomClient) Close() error {
return c.client.Close()
}
// Creates the header for all messages
func createHeader(sender, subject, boundary string) string {
var buffer bytes.Buffer
fmt.Fprintf(&buffer, "From: %s\r\n", sender)
fmt.Fprintf(&buffer, "MIME-Version: 1.0\r\n")
fmt.Fprintf(&buffer, "Subject: %s\r\n", subject)
// Replace the first space with a comma and a space to conform to rfc2822
fmt.Fprintf(&buffer, "Date: %s%s", strings.Replace(time.Now().UTC().Format(rfc2822), " ", ", ", 1), "\r\n")
fmt.Fprintf(&buffer, "Content-Type: multipart/mixed; boundary=\"%s\"; charset=\"UTF-8\"\r\n", boundary)
fmt.Fprintf(&buffer, "To: ")
return buffer.String()
}
// Create the full message for a single receiver
func createFullMessage(b *bytes.Buffer, receiver, sender, globalHeader, base64Message string, extensions, filenames []string, boundary string) error {
fmt.Fprintf(b, "%s%s\r\n", globalHeader, receiver)
writer := multipart.NewWriter(b)
if err := writer.SetBoundary(boundary); err != nil {
return err
}
// Set the message
if err := createText(writer, base64Message); err != nil {
return err
}
// Set attachments. Here for now because the boundaries are wanted unique
for index, value := range filenames {
if err := createAttachment(writer, extensions[index], value); err != nil {
return err
}
}
if err := writer.Close(); err != nil {
return err
}
return nil
}
// Create an attachment with a certain extension
func createAttachment(writer *multipart.Writer, extension, path string) error {
// Create a header
newHeader := make(textproto.MIMEHeader)
newHeader.Add("Content-Type", extension)
newHeader.Add("Content-Transfer-Encoding", "base64")
newHeader.Add("Content-Disposition", "attachment; filename="+path+";")
// Create a writer for the file
output, err := writer.CreatePart(newHeader)
if err != nil {
return err
}
// Write the file to the message
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
fmt.Fprintf(output, base64.StdEncoding.EncodeToString([]byte(data)))
return nil
}
// Creates the equivalent of the message wrapped in a boundary. The message is expected to have been encoded via base64
func createText(writer *multipart.Writer, message string) error {
// Create the mime header for the message
mimeHeaderMessage := make(textproto.MIMEHeader)
mimeHeaderMessage.Add("Content-Transfer-Encoding", "base64")
mimeHeaderMessage.Add("Content-Type", "text/plain; charset=\"UTF-8\"")
// Set the message
output, err := writer.CreatePart(mimeHeaderMessage)
if err != nil {
return err
}
fmt.Fprintf(output, "%s", message)
return nil
}
// Totally copied from go stl
func randomBoundary() string {
var buf [30]byte
_, err := io.ReadFull(rand.Reader, buf[:])
if err != nil {
panic(err)
}
return fmt.Sprintf("%x", buf[:])
}
package mails
import (
"fmt"
"os"
"testing"
)
var client *CustomClient
var err error
var rcpt1 string
var rcpt2 string
func TestMain(m *testing.M) {
// Setup is based on environment variables
sender := os.Getenv("DFSS_TEST_MAIL_SENDER")
host := os.Getenv("DFSS_TEST_MAIL_HOST")
port := os.Getenv("DFSS_TEST_MAIL_PORT")
username := os.Getenv("DFSS_TEST_MAIL_USER")
password := os.Getenv("DFSS_TEST_MAIL_PASSWORD")
rcpt1 = os.Getenv("DFSS_TEST_MAIL_RCPT1")
rcpt2 = os.Getenv("DFSS_TEST_MAIL_RCPT2")
client, err = NewCustomClient(sender, host, port, username, password)
if err != nil {
fmt.Println(err)
}
code := m.Run()
err = client.Close()
if err != nil {
fmt.Println(err)
}
os.Exit(code)
}
func TestSingleMail(t *testing.T) {
err = client.Send([]string{rcpt1}, "TestSingleMail", "Gros espoirs!", []string{}, []string{})
if err != nil {
t.Fatal(err)
}
}
func TestDoubleMail(t *testing.T) {
err = client.Send([]string{rcpt1, rcpt2}, "TestDoubleMail", "Gros espoirs!", []string{}, []string{})
if err != nil {
t.Fatal(err)
}
}
func TestRuneMail(t *testing.T) {
err = client.Send([]string{rcpt1}, "TestRuneMail", "测试", []string{}, []string{})
if err != nil {
t.Fatal(err)
}
}
func TestAttachmentMailText(t *testing.T) {
err = client.Send([]string{rcpt1}, "TestAttachmentMailText", "What would make a good attachment?", []string{"text/plain"}, []string{"mail_test.go"})
if err != nil {
t.Fatal(err)
}
}
func TestAttachmentMailImage(t *testing.T) {
err = client.Send([]string{rcpt1}, "TestAttachmentMailImage", "What would make a good attachment?", []string{"image/gif"}, []string{"testImg.gif"})
if err != nil {
t.Fatal(err)
}
}
func ExampleCustomClient() {
// Create a connection
client, err := NewCustomClient("qdauchy@insa-rennes.fr", "mailhost.insa-rennes.fr", "587", "qdauchy", "notreallymypass")
if err != nil {
fmt.Println(err)
}
// Some reused variables
receiver1 := "lbonniot@insa-rennes.fr"
receiver2 := "qdauchy@insa-rennes.fr"
receiver3 := "tclaverie@insa-rennes.fr"
subject := "Mail example"
message := `Hello, this is a mail example. It's not like the cactus is going
to be jealous or anything...`
// Send a first mail, without attachments
err = client.Send([]string{receiver1, receiver2}, subject, message, []string{}, []string{})
if err != nil {
fmt.Println(err)
}
// Send a second mail, with some attachments
err = client.Send([]string{receiver1, receiver3}, subject, message, []string{"text/plain", "image/gif"}, []string{"email.go", "testImg.gif"})
if err != nil {
fmt.Println(err)
}
// Close the connection
err = client.Close()
if err != nil {
fmt.Println(err)
}
}
mails/testImg.gif

13.6 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment