249 lines
6.2 KiB
Go
249 lines
6.2 KiB
Go
|
package mail
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/base64"
|
||
|
"io"
|
||
|
"mime/multipart"
|
||
|
"mime/quotedprintable"
|
||
|
"net/textproto"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
type message struct {
|
||
|
headers textproto.MIMEHeader
|
||
|
body *bytes.Buffer
|
||
|
writers []*multipart.Writer
|
||
|
parts uint8
|
||
|
cids map[string]string
|
||
|
charset string
|
||
|
encoding encoding
|
||
|
}
|
||
|
|
||
|
func newMessage(email *Email) *message {
|
||
|
return &message{
|
||
|
headers: email.headers,
|
||
|
body: new(bytes.Buffer),
|
||
|
cids: make(map[string]string),
|
||
|
charset: email.Charset,
|
||
|
encoding: email.Encoding}
|
||
|
}
|
||
|
|
||
|
func encodeHeader(text string, charset string, usedChars int) string {
|
||
|
// create buffer
|
||
|
buf := new(bytes.Buffer)
|
||
|
|
||
|
// encode
|
||
|
encoder := newEncoder(buf, charset, usedChars)
|
||
|
encoder.encode([]byte(text))
|
||
|
|
||
|
return buf.String()
|
||
|
|
||
|
/*
|
||
|
switch encoding {
|
||
|
case EncodingBase64:
|
||
|
return mime.BEncoding.Encode(charset, text)
|
||
|
default:
|
||
|
return mime.QEncoding.Encode(charset, text)
|
||
|
}
|
||
|
*/
|
||
|
}
|
||
|
|
||
|
// getHeaders returns the message headers
|
||
|
func (msg *message) getHeaders() (headers string) {
|
||
|
// if the date header isn't set, set it
|
||
|
if date := msg.headers.Get("Date"); date == "" {
|
||
|
msg.headers.Set("Date", time.Now().Format(time.RFC1123Z))
|
||
|
}
|
||
|
|
||
|
// encode and combine the headers
|
||
|
for header, values := range msg.headers {
|
||
|
headers += header + ": " + encodeHeader(strings.Join(values, ", "), msg.charset, len(header)+2) + "\r\n"
|
||
|
}
|
||
|
|
||
|
headers = headers + "\r\n"
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// getCID gets the generated CID for the provided text
|
||
|
func (msg *message) getCID(text string) (cid string) {
|
||
|
// set the date format to use
|
||
|
const dateFormat = "20060102.150405"
|
||
|
|
||
|
// get the cid if we have one
|
||
|
cid, exists := msg.cids[text]
|
||
|
if !exists {
|
||
|
// generate a new cid
|
||
|
cid = time.Now().Format(dateFormat) + "." + strconv.Itoa(len(msg.cids)+1) + "@mail.0"
|
||
|
// save it
|
||
|
msg.cids[text] = cid
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// replaceCIDs replaces the CIDs found in a text string
|
||
|
// with generated ones
|
||
|
func (msg *message) replaceCIDs(text string) string {
|
||
|
// regular expression to find cids
|
||
|
re := regexp.MustCompile(`(src|href)="cid:(.*?)"`)
|
||
|
// replace all of the found cids with generated ones
|
||
|
for _, matches := range re.FindAllStringSubmatch(text, -1) {
|
||
|
cid := msg.getCID(matches[2])
|
||
|
text = strings.Replace(text, "cid:"+matches[2], "cid:"+cid, -1)
|
||
|
}
|
||
|
|
||
|
return text
|
||
|
}
|
||
|
|
||
|
// openMultipart creates a new part of a multipart message
|
||
|
func (msg *message) openMultipart(multipartType string) {
|
||
|
// create a new multipart writer
|
||
|
msg.writers = append(msg.writers, multipart.NewWriter(msg.body))
|
||
|
// create the boundary
|
||
|
contentType := "multipart/" + multipartType + ";\n \tboundary=" + msg.writers[msg.parts].Boundary()
|
||
|
|
||
|
// if no existing parts, add header to main header group
|
||
|
if msg.parts == 0 {
|
||
|
msg.headers.Set("Content-Type", contentType)
|
||
|
} else { // add header to multipart section
|
||
|
header := make(textproto.MIMEHeader)
|
||
|
header.Set("Content-Type", contentType)
|
||
|
msg.writers[msg.parts-1].CreatePart(header)
|
||
|
}
|
||
|
|
||
|
msg.parts++
|
||
|
}
|
||
|
|
||
|
// closeMultipart closes a part of a multipart message
|
||
|
func (msg *message) closeMultipart() {
|
||
|
if msg.parts > 0 {
|
||
|
msg.writers[msg.parts-1].Close()
|
||
|
msg.parts--
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// base64Encode base64 encodes the provided text with line wrapping
|
||
|
func base64Encode(text []byte) []byte {
|
||
|
// create buffer
|
||
|
buf := new(bytes.Buffer)
|
||
|
|
||
|
// create base64 encoder that linewraps
|
||
|
encoder := base64.NewEncoder(base64.StdEncoding, &base64LineWrap{writer: buf})
|
||
|
|
||
|
// write the encoded text to buf
|
||
|
encoder.Write(text)
|
||
|
encoder.Close()
|
||
|
|
||
|
return buf.Bytes()
|
||
|
}
|
||
|
|
||
|
// qpEncode uses the quoted-printable encoding to encode the provided text
|
||
|
func qpEncode(text []byte) []byte {
|
||
|
// create buffer
|
||
|
buf := new(bytes.Buffer)
|
||
|
|
||
|
encoder := quotedprintable.NewWriter(buf)
|
||
|
|
||
|
encoder.Write(text)
|
||
|
encoder.Close()
|
||
|
|
||
|
return buf.Bytes()
|
||
|
}
|
||
|
|
||
|
const maxLineChars = 76
|
||
|
|
||
|
type base64LineWrap struct {
|
||
|
writer io.Writer
|
||
|
numLineChars int
|
||
|
}
|
||
|
|
||
|
func (e *base64LineWrap) Write(p []byte) (n int, err error) {
|
||
|
n = 0
|
||
|
// while we have more chars than are allowed
|
||
|
for len(p)+e.numLineChars > maxLineChars {
|
||
|
numCharsToWrite := maxLineChars - e.numLineChars
|
||
|
// write the chars we can
|
||
|
e.writer.Write(p[:numCharsToWrite])
|
||
|
// write a line break
|
||
|
e.writer.Write([]byte("\r\n"))
|
||
|
// reset the line count
|
||
|
e.numLineChars = 0
|
||
|
// remove the chars that have been written
|
||
|
p = p[numCharsToWrite:]
|
||
|
// set the num of chars written
|
||
|
n += numCharsToWrite
|
||
|
}
|
||
|
|
||
|
// write what is left
|
||
|
e.writer.Write(p)
|
||
|
e.numLineChars += len(p)
|
||
|
n += len(p)
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (msg *message) write(header textproto.MIMEHeader, body []byte, encoding encoding) {
|
||
|
msg.writeHeader(header)
|
||
|
msg.writeBody(body, encoding)
|
||
|
}
|
||
|
|
||
|
func (msg *message) writeHeader(headers textproto.MIMEHeader) {
|
||
|
// if there are no parts add header to main headers
|
||
|
if msg.parts == 0 {
|
||
|
for header, value := range headers {
|
||
|
msg.headers[header] = value
|
||
|
}
|
||
|
} else { // add header to multipart section
|
||
|
msg.writers[msg.parts-1].CreatePart(headers)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (msg *message) writeBody(body []byte, encoding encoding) {
|
||
|
// encode and write the body
|
||
|
switch encoding {
|
||
|
case EncodingQuotedPrintable:
|
||
|
msg.body.Write(qpEncode(body))
|
||
|
case EncodingBase64:
|
||
|
msg.body.Write(base64Encode(body))
|
||
|
default:
|
||
|
msg.body.Write(body)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (msg *message) addBody(contentType string, body []byte) {
|
||
|
body = []byte(msg.replaceCIDs(string(body)))
|
||
|
|
||
|
header := make(textproto.MIMEHeader)
|
||
|
header.Set("Content-Type", contentType+"; charset="+msg.charset)
|
||
|
header.Set("Content-Transfer-Encoding", msg.encoding.string())
|
||
|
msg.write(header, body, msg.encoding)
|
||
|
}
|
||
|
|
||
|
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||
|
|
||
|
func escapeQuotes(s string) string {
|
||
|
return quoteEscaper.Replace(s)
|
||
|
}
|
||
|
|
||
|
func (msg *message) addFiles(files []*File, inline bool) {
|
||
|
encoding := EncodingBase64
|
||
|
for _, file := range files {
|
||
|
header := make(textproto.MIMEHeader)
|
||
|
header.Set("Content-Type", file.MimeType+";\n \tname=\""+encodeHeader(escapeQuotes(file.Name), msg.charset, 6)+`"`)
|
||
|
header.Set("Content-Transfer-Encoding", encoding.string())
|
||
|
if inline {
|
||
|
header.Set("Content-Disposition", "inline;\n \tfilename=\""+encodeHeader(escapeQuotes(file.Name), msg.charset, 10)+`"`)
|
||
|
header.Set("Content-ID", "<"+msg.getCID(file.Name)+">")
|
||
|
} else {
|
||
|
header.Set("Content-Disposition", "attachment;\n \tfilename=\""+encodeHeader(escapeQuotes(file.Name), msg.charset, 10)+`"`)
|
||
|
}
|
||
|
|
||
|
msg.write(header, file.Data, encoding)
|
||
|
}
|
||
|
}
|