Commit 35e6bc2d authored by Tristan Claverie's avatar Tristan Claverie
Browse files

Merge branch '115_mails_lib' into 'master'

115 mails lib



See merge request !5
parents 2c8b21cc c27359ab
Pipeline #118 passed with stage
......@@ -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"
Please register or sign in to reply
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)
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment