all repos — mail2 @ 1cb0f7958d641b18c717e972c8db5db934ba2750

fork of github.com/joegrasse/mail with some changes

Fix initial commit
Joe Grasse hide@my.email
Thu, 01 Oct 2015 14:36:18 -0500
commit

1cb0f7958d641b18c717e972c8db5db934ba2750

4 files changed, 980 insertions(+), 0 deletions(-)

jump to
A .gitattributes

@@ -0,0 +1,17 @@

+# Auto detect text files and perform LF normalization +* -text + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain
A .gitignore

@@ -0,0 +1,28 @@

+# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# Eclipse Project files +.project +.settings
A README.md

@@ -0,0 +1,51 @@

+The best way to send emails in Go. + +**Download** + +```bash +go get github.com/joegrasse/mail +``` + +**Basic Usage** + +```go +package main + +import ( + "fmt" + "github.com/joegrasse/mail" +) + +func main() { + var err error + email := mail.New() + + email.SetFrom("From Example <from@example.com>") + email.AddTo("to@example.com>") + email.SetSubject("New Go Email") + + email.SetBody("text/plain", "Hello Gophers!") + + html_body := + `<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <title>Html Email Test!</title> + </head> + <body> + <p>Hello <b>Gophers</b>!</p> + <p><img src="cid:image.jpg" alt="image" /></p> + </body> + </html>` + email.AddAlternative("text/html", html_body) + + email.AddInline("/path/to/image.jpg") + + err = email.Send("smtp.csgi.com:25") + + if err != nil { + fmt.Println(err) + } else { + fmt.Println("Email Sent!") + } +}```
A mail.go

@@ -0,0 +1,884 @@

+package mail + +import ( + "bytes" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "github.com/joegrasse/mime/header" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "mime/quotedprintable" + "net" + "net/mail" + "net/smtp" + "net/textproto" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" +) + +// email represents an email message. +type email struct { + from string + sender string + replyTo string + returnPath string + recipients []string + headers textproto.MIMEHeader + parts []part + attachments []*file + inlines []*file + Charset string + Encoding encoding + Encryption encryption + Username string + Password string + TLSConfig *tls.Config + ConnectTimeout int +} + +// part represents the different content parts of an email body. +type part struct { + contentType string + body *bytes.Buffer +} + +// file represents the files that can be added to the email message. +type file struct { + filename string + mimeType string + data []byte +} + +type encryption int + +const ( + EncryptionTLS encryption = iota + EncryptionSSL + EncryptionNone +) + +var encryptionTypes = [...]string{"TLS", "SSL", "None"} + +func (encryption encryption) String() string { + return encryptionTypes[encryption] +} + +type encoding int + +const ( + EncodingQuotedPrintable encoding = iota + EncodingBase64 + EncodingNone +) + +var encodingTypes = [...]string{"quoted-printable", "base64", "binary"} + +func (encoding encoding) String() string { + return encodingTypes[encoding] +} + +// New creates a new email. It uses UTF-8 by default. +func New() *email { + email := &email{ + headers: make(textproto.MIMEHeader), + Charset: "UTF-8", + Encoding: EncodingQuotedPrintable, + Encryption: EncryptionNone, + TLSConfig: new(tls.Config), + } + + email.AddHeader("MIME-Version", "1.0") + + return email +} + +// SetFrom sets the From address. +func (email *email) SetFrom(address string) error { + return email.AddAddresses("From", address) +} + +// SetSender sets the Sender address. +func (email *email) SetSender(address string) error { + return email.AddAddresses("Sender", address) +} + +// SetReplyTo sets the Reply-To address. +func (email *email) SetReplyTo(address string) error { + return email.AddAddresses("Reply-To", address) +} + +// SetReturnPath sets the Return-Path address. This is most often used +// to send bounced emails to a different email address. +func (email *email) SetReturnPath(address string) error { + return email.AddAddresses("Return-Path", address) +} + +// AddTo adds a To address. You can provide multiple +// addresses at the same time. +func (email *email) AddTo(addresses ...string) error { + return email.AddAddresses("To", addresses...) +} + +// AddCc adds a Cc address. You can provide multiple +// addresses at the same time. +func (email *email) AddCc(addresses ...string) error { + return email.AddAddresses("Cc", addresses...) +} + +// AddBcc adds a Bcc address. You can provide multiple +// addresses at the same time. +func (email *email) AddBcc(addresses ...string) error { + return email.AddAddresses("Bcc", addresses...) +} + +// AddAddresses allows you to add addresses to the specified address header. +func (email *email) AddAddresses(header string, addresses ...string) error { + found := false + + // check for a valid address header + for _, h := range []string{"To", "Cc", "Bcc", "From", "Sender", "Reply-To", "Return-Path"} { + if header == h { + found = true + } + } + + if !found { + return errors.New("Mail Error: Invalid address header; Header: [" + header + "]") + } + + // check to see if the addresses are valid + for i := range addresses { + address, err := mail.ParseAddress(addresses[i]) + if err != nil { + return errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]") + } + + // check for more than one address + switch { + case header == "From" && len(email.from) > 0: + fallthrough + case header == "Sender" && len(email.sender) > 0: + fallthrough + case header == "Reply-To" && len(email.replyTo) > 0: + fallthrough + case header == "Return-Path" && len(email.returnPath) > 0: + return errors.New("Mail Error: There can only be one \"" + header + "\" address; Header: [" + header + "] Address: [" + addresses[i] + "]") + default: + // other address types can have more than one address + } + + // save the address + switch header { + case "From": + email.from = address.Address + case "Sender": + email.sender = address.Address + case "Reply-To": + email.replyTo = address.Address + case "Return-Path": + email.returnPath = address.Address + default: + // check that the address was added to the recipients list + email.recipients, err = addAddress(email.recipients, address.Address) + if err != nil { + return errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]") + } + } + + // make sure the from and sender addresses are different + if email.from != "" && email.sender != "" && email.from == email.sender { + email.sender = "" + email.headers.Del("Sender") + return errors.New("Mail Error: From and Sender should not be set to the same address.") + } + + // add all addresses to the headers except for Bcc and Return-Path + if header != "Bcc" && header != "Return-Path" { + // add the address to the headers + email.headers.Add(header, address.String()) + } + } + + return nil +} + +// addAddress adds an address to the address list if it hasn't already been added +func addAddress(addressList []string, address string) ([]string, error) { + // loop through the address list to check for dups + for _, a := range addressList { + if address == a { + return addressList, errors.New("Mail Error: Address: [" + address + "] has already been added") + } + } + + return append(addressList, address), nil +} + +type priority int + +const ( + PriorityHigh priority = iota + PriorityLow +) + +var priorities = [...]string{"High", "Low"} + +func (priority priority) String() string { + return priorities[priority] +} + +// SetPriority sets the email message priority. Use with +// either "High" or "Low". +func (email *email) SetPriority(priority priority) error { + switch priority { + case PriorityHigh: + return email.AddHeaders(textproto.MIMEHeader{ + "X-Priority": {"1 (Highest)"}, + "X-MSMail-Priority": {"High"}, + "Importance": {"High"}, + }) + case PriorityLow: + return email.AddHeaders(textproto.MIMEHeader{ + "X-Priority": {"5 (Lowest)"}, + "X-MSMail-Priority": {"Low"}, + "Importance": {"Low"}, + }) + default: + } + + return nil +} + +// SetDate sets the date header to the provided date/time. +// The format of the string should be YYYY-MM-DD HH:MM:SS Time Zone. +// +// Example: SetDate("2015-04-28 10:32:00 CDT") +func (email *email) SetDate(dateTime string) error { + const dateFormat = "2006-01-02 15:04:05 MST" + + // Try to parse the provided date/time + dt, err := time.Parse(dateFormat, dateTime) + if err != nil { + return errors.New("Mail Error: Setting date failed with: " + err.Error()) + } + + email.headers.Set("Date", dt.Format(time.RFC1123Z)) + + return nil +} + +// SetSubject sets the subject of the email message. +func (email *email) SetSubject(subject string) error { + return email.AddHeader("Subject", subject) +} + +// SetBody sets the body of the email message. +func (email *email) SetBody(contentType, body string) { + email.parts = []part{ + part{ + contentType: contentType, + body: bytes.NewBufferString(body), + }, + } +} + +// Header adds the given "header" with the passed "value". +func (email *email) AddHeader(header string, values ...string) error { + // check that there is actually a value + if len(values) < 1 { + return errors.New("Mail Error: no value provided; Header: [" + header + "]") + } + + switch header { + case "Sender": + fallthrough + case "From": + fallthrough + case "To": + fallthrough + case "Bcc": + fallthrough + case "Cc": + fallthrough + case "Reply-To": + fallthrough + case "Return-Path": + return email.AddAddresses(header, values...) + case "Date": + if len(values) > 1 { + return errors.New("Mail Error: To many dates provided") + } + return email.SetDate(values[0]) + default: + email.headers[header] = values + } + + return nil +} + +// Headers is used to add mulitple headers at once +func (email *email) AddHeaders(headers textproto.MIMEHeader) error { + for header, values := range headers { + if err := email.AddHeader(header, values...); err != nil { + return err + } + } + + return nil +} + +// Alternative allows you to add alternative parts to the body +// of the email message. This is most commonly used to add an +// html version in addition to a plain text version that was +// already added with SetBody. +func (email *email) AddAlternative(contentType, body string) { + email.parts = append(email.parts, + part{ + contentType: contentType, + body: bytes.NewBufferString(body), + }, + ) +} + +// Attach allows you to add an attachment to the email message. +// You can optionally provide a different name for the file. +func (email *email) AddAttachment(file string, name ...string) error { + if len(name) > 1 { + return errors.New("Mail Error: Attach can only have a file and an optional name") + } + + return email.attach(file, false, name...) +} + +// Inline allows you to add an inline attachment to the email message. +// You can optionally provide a different name for the file. +func (email *email) AddInline(file string, name ...string) error { + if len(name) > 1 { + return errors.New("Mail Error: Inline can only have a file and an optional name") + } + + return email.attach(file, true, name...) +} + +// attach does the low level attaching of the files +func (email *email) attach(f string, inline bool, name ...string) error { + // Get the file data + data, err := ioutil.ReadFile(f) + if err != nil { + return errors.New("Mail Error: Failed to add file with following error: " + err.Error()) + } + + // get the file mime type + mimeType := mime.TypeByExtension(filepath.Ext(f)) + if mimeType == "" { + mimeType = "application/octet-stream" + } + + // get the filename + _, filename := filepath.Split(f) + + // if an alternative filename was provided, use that instead + if len(name) == 1 { + filename = name[0] + } + + if inline { + email.inlines = append(email.inlines, &file{ + filename: filename, + mimeType: mimeType, + data: data, + }) + } else { + email.attachments = append(email.attachments, &file{ + filename: filename, + mimeType: mimeType, + data: data, + }) + } + + return nil +} + +// getFrom returns the sender of the email, if any +func (email *email) getFrom() string { + from := email.returnPath + if from == "" { + from = email.sender + if from == "" { + from = email.from + if from == "" { + from = email.replyTo + } + } + } + + return from +} + +func (email *email) hasMixedPart() bool { + return (len(email.parts) > 0 && len(email.attachments) > 0) || len(email.attachments) > 1 +} + +func (email *email) hasRelatedPart() bool { + return (len(email.parts) > 0 && len(email.inlines) > 0) || len(email.inlines) > 1 +} + +func (email *email) hasAlternativePart() bool { + return len(email.parts) > 1 +} + +// GetMessage builds and returns the email message +func (email *email) GetMessage() string { + msg := newMessage(email) + + if email.hasMixedPart() { + msg.openMultipart("mixed") + } + + if email.hasRelatedPart() { + msg.openMultipart("related") + } + + if email.hasAlternativePart() { + msg.openMultipart("alternative") + } + + for _, part := range email.parts { + msg.addBody(part.contentType, part.body.Bytes()) + } + + if email.hasAlternativePart() { + msg.closeMultipart() + } + + msg.addFiles(email.inlines, true) + if email.hasRelatedPart() { + msg.closeMultipart() + } + + msg.addFiles(email.attachments, false) + if email.hasMixedPart() { + msg.closeMultipart() + } + + return msg.getHeaders() + msg.body.String() +} + +// Send sends the composed email +func (email *email) Send(address string) error { + var auth smtp.Auth + + from := email.getFrom() + if from == "" { + return errors.New(`Mail Error: No "From" address specified.`) + } + + if len(email.recipients) < 1 { + return errors.New("Mail Error: No recipient specified.") + } + + msg := email.GetMessage() + + host, port, err := net.SplitHostPort(address) + if err != nil { + return errors.New("Mail Error: " + err.Error()) + } + + if email.Username != "" || email.Password != "" { + auth = smtp.PlainAuth("", email.Username, email.Password, host) + } + + return send(host, port, from, email.recipients, msg, auth, email.Encryption, email.TLSConfig, email.ConnectTimeout) +} + +// dial connects to the smtp server with the request encryption type +func dial(host string, port string, encryption encryption, config *tls.Config) (*smtp.Client, error) { + var conn net.Conn + var err error + + address := host + ":" + port + + // do the actual dial + switch encryption { + case EncryptionSSL: + conn, err = tls.Dial("tcp", address, config) + default: + conn, err = net.Dial("tcp", address) + } + + if err != nil { + return nil, errors.New("Mail Error on dailing with encryption type " + encryption.String() + ": " + err.Error()) + } + + c, err := smtp.NewClient(conn, host) + + if err != nil { + return nil, errors.New("Mail Error on smtp dial: " + err.Error()) + } + + return c, err +} + +// smtpConnect connects to the smtp server and starts TLS and passes auth +// if necessary +func smtpConnect(host string, port string, from string, to []string, msg string, auth smtp.Auth, encryption encryption, config *tls.Config) (*smtp.Client, error) { + // connect to the mail server + c, err := dial(host, port, encryption, config) + + if err != nil { + return nil, err + } + + // send Hello + if err = c.Hello("localhost"); err != nil { + c.Close() + return nil, errors.New("Mail Error on Hello: " + err.Error()) + } + + // start TLS if necessary + if encryption == EncryptionTLS { + if ok, _ := c.Extension("STARTTLS"); ok { + if config.ServerName == "" { + config = &tls.Config{ServerName: host} + } + + if err = c.StartTLS(config); err != nil { + c.Close() + return nil, errors.New("Mail Error on Start TLS: " + err.Error()) + } + } + } + + // pass the authentication if necessary + if auth != nil { + if ok, _ := c.Extension("AUTH"); ok { + if err = c.Auth(auth); err != nil { + c.Close() + return nil, errors.New("Mail Error on Auth: " + err.Error()) + } + } + } + + return c, nil +} + +type smtpConnectErrorChannel struct { + client *smtp.Client + err error +} + +// send does the low level sending of the email +func send(host string, port string, from string, to []string, msg string, auth smtp.Auth, encryption encryption, config *tls.Config, connectTimeout int) error { + var smtpConnectChannel chan smtpConnectErrorChannel + var c *smtp.Client = nil + var err error + + // set the timeout value + timeout := time.Duration(connectTimeout) * time.Second + + // if there is a timeout, setup the channel and do the connect under a goroutine + if timeout != 0 { + smtpConnectChannel = make(chan smtpConnectErrorChannel, 2) + go func() { + c, err = smtpConnect(host, port, from, to, msg, auth, encryption, config) + // send the result + smtpConnectChannel <- smtpConnectErrorChannel{ + client: c, + err: err, + } + }() + } + + if timeout == 0 { + // no timeout, just fire the connect + c, err = smtpConnect(host, port, from, to, msg, auth, encryption, config) + } else { + // get the connect result or timeout result, which ever happens first + select { + case result := <-smtpConnectChannel: + c = result.client + err = result.err + case <-time.After(timeout): + return errors.New("Mail Error: SMTP Connection timed out") + } + } + + // check for connect error + if err != nil { + return err + } + + defer c.Close() + + // Set the sender + if err := c.Mail(from); err != nil { + return err + } + + // Set the recipients + for _, address := range to { + if err := c.Rcpt(address); err != nil { + return err + } + } + + // Send the data command + w, err := c.Data() + if err != nil { + return err + } + + // write the message + _, err = fmt.Fprint(w, msg) + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + + return c.Quit() +} + +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 := header.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.filename), msg.charset, 6)+`"`) + header.Set("Content-Transfer-Encoding", encoding.String()) + if inline { + header.Set("Content-Disposition", "inline;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`) + header.Set("Content-ID", "<"+msg.getCID(file.filename)+">") + } else { + header.Set("Content-Disposition", "attachment;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`) + } + + msg.write(header, file.data, encoding) + } +}