email.go 5.33 KB
Newer Older
Quentin DAUCHY's avatar
Quentin DAUCHY committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
// Package mails provides a simple interface with the smtp library
package mails

import (
	"bytes"
	"crypto/rand"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/smtp"
	"net/textproto"
	"time"
)

// CustomClient :
// Modelizes the constants of a connection : an actual client, and a sender
type CustomClient struct {
	sender string
	client *smtp.Client
}

// 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.
56
func (c *CustomClient) Send(receivers []string, subject, message string, extensions, filenames []string, files [][]byte) error {
Quentin DAUCHY's avatar
Quentin DAUCHY committed
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
	// 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
80
		if err := createFullMessage(&localBuffer, receiver, c.sender, header, base64Message, extensions, filenames, files, boundary); err != nil {
Quentin DAUCHY's avatar
Quentin DAUCHY committed
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
			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
121
	fmt.Fprintf(&buffer, "Date: %s%s", time.Now().Format(time.RFC1123Z), "\r\n")
Quentin DAUCHY's avatar
Quentin DAUCHY committed
122 123 124 125 126 127
	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
128
func createFullMessage(b io.Writer, receiver, sender, globalHeader, base64Message string, extensions, filenames []string, files [][]byte, boundary string) error {
Quentin DAUCHY's avatar
Quentin DAUCHY committed
129 130 131 132 133 134 135 136 137 138 139 140
	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
141 142
	for index, value := range files {
		if err := createAttachment(writer, extensions[index], filenames[index], value); err != nil {
Quentin DAUCHY's avatar
Quentin DAUCHY committed
143 144 145
			return err
		}
	}
146
	return writer.Close()
Quentin DAUCHY's avatar
Quentin DAUCHY committed
147 148 149
}

// Create an attachment with a certain extension
150
func createAttachment(writer *multipart.Writer, extension, path string, file []byte) error {
Quentin DAUCHY's avatar
Quentin DAUCHY committed
151 152 153 154 155 156 157 158 159 160 161 162
	// 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
	}

163
	fmt.Fprintf(output, base64.StdEncoding.EncodeToString(file))
Quentin DAUCHY's avatar
Quentin DAUCHY committed
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
	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[:])
}