mirror of
https://github.com/FlourishingWorld/hk4e.git
synced 2026-03-01 00:35:36 +08:00
init commit
This commit is contained in:
809
common/utils/email/email.go
Normal file
809
common/utils/email/email.go
Normal file
@@ -0,0 +1,809 @@
|
||||
// Package email is designed to provide an "email interface for humans."
|
||||
// Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way.
|
||||
package email
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxLineLength = 76 // MaxLineLength is the maximum line length per RFC 2045
|
||||
defaultContentType = "text/plain; charset=us-ascii" // defaultContentType is the default Content-Type according to RFC 2045, section 5.2
|
||||
)
|
||||
|
||||
// ErrMissingBoundary is returned when there is no boundary given for a multipart entity
|
||||
var ErrMissingBoundary = errors.New("No boundary found for multipart entity")
|
||||
|
||||
// ErrMissingContentType is returned when there is no "Content-Type" header for a MIME entity
|
||||
var ErrMissingContentType = errors.New("No Content-Type found for MIME entity")
|
||||
|
||||
// Email is the type used for email messages
|
||||
type Email struct {
|
||||
ReplyTo []string
|
||||
From string
|
||||
To []string
|
||||
Bcc []string
|
||||
Cc []string
|
||||
Subject string
|
||||
Text []byte // Plaintext message (optional)
|
||||
HTML []byte // Html message (optional)
|
||||
Sender string // override From as SMTP envelope sender (optional)
|
||||
Headers textproto.MIMEHeader
|
||||
Attachments []*Attachment
|
||||
ReadReceipt []string
|
||||
}
|
||||
|
||||
// part is a copyable representation of a multipart.Part
|
||||
type part struct {
|
||||
header textproto.MIMEHeader
|
||||
body []byte
|
||||
}
|
||||
|
||||
// NewEmail creates an Email, and returns the pointer to it.
|
||||
func NewEmail() *Email {
|
||||
return &Email{Headers: textproto.MIMEHeader{}}
|
||||
}
|
||||
|
||||
// trimReader is a custom io.Reader that will trim any leading
|
||||
// whitespace, as this can cause email imports to fail.
|
||||
type trimReader struct {
|
||||
rd io.Reader
|
||||
trimmed bool
|
||||
}
|
||||
|
||||
// Read trims off any unicode whitespace from the originating reader
|
||||
func (tr *trimReader) Read(buf []byte) (int, error) {
|
||||
n, err := tr.rd.Read(buf)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if !tr.trimmed {
|
||||
t := bytes.TrimLeftFunc(buf[:n], unicode.IsSpace)
|
||||
tr.trimmed = true
|
||||
n = copy(buf, t)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func handleAddressList(v []string) []string {
|
||||
res := []string{}
|
||||
for _, a := range v {
|
||||
w := strings.Split(a, ",")
|
||||
for _, addr := range w {
|
||||
decodedAddr, err := (&mime.WordDecoder{}).DecodeHeader(strings.TrimSpace(addr))
|
||||
if err == nil {
|
||||
res = append(res, decodedAddr)
|
||||
} else {
|
||||
res = append(res, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// NewEmailFromReader reads a stream of bytes from an io.Reader, r,
|
||||
// and returns an email struct containing the parsed data.
|
||||
// This function expects the data in RFC 5322 format.
|
||||
func NewEmailFromReader(r io.Reader) (*Email, error) {
|
||||
e := NewEmail()
|
||||
s := &trimReader{rd: r}
|
||||
tp := textproto.NewReader(bufio.NewReader(s))
|
||||
// Parse the main headers
|
||||
hdrs, err := tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
// Set the subject, to, cc, bcc, and from
|
||||
for h, v := range hdrs {
|
||||
switch h {
|
||||
case "Subject":
|
||||
e.Subject = v[0]
|
||||
subj, err := (&mime.WordDecoder{}).DecodeHeader(e.Subject)
|
||||
if err == nil && len(subj) > 0 {
|
||||
e.Subject = subj
|
||||
}
|
||||
delete(hdrs, h)
|
||||
case "To":
|
||||
e.To = handleAddressList(v)
|
||||
delete(hdrs, h)
|
||||
case "Cc":
|
||||
e.Cc = handleAddressList(v)
|
||||
delete(hdrs, h)
|
||||
case "Bcc":
|
||||
e.Bcc = handleAddressList(v)
|
||||
delete(hdrs, h)
|
||||
case "Reply-To":
|
||||
e.ReplyTo = handleAddressList(v)
|
||||
delete(hdrs, h)
|
||||
case "From":
|
||||
e.From = v[0]
|
||||
fr, err := (&mime.WordDecoder{}).DecodeHeader(e.From)
|
||||
if err == nil && len(fr) > 0 {
|
||||
e.From = fr
|
||||
}
|
||||
delete(hdrs, h)
|
||||
}
|
||||
}
|
||||
e.Headers = hdrs
|
||||
body := tp.R
|
||||
// Recursively parse the MIME parts
|
||||
ps, err := parseMIMEParts(e.Headers, body)
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
for _, p := range ps {
|
||||
if ct := p.header.Get("Content-Type"); ct == "" {
|
||||
return e, ErrMissingContentType
|
||||
}
|
||||
ct, _, err := mime.ParseMediaType(p.header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
// Check if part is an attachment based on the existence of the Content-Disposition header with a value of "attachment".
|
||||
if cd := p.header.Get("Content-Disposition"); cd != "" {
|
||||
cd, params, err := mime.ParseMediaType(p.header.Get("Content-Disposition"))
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
filename, filenameDefined := params["filename"]
|
||||
if cd == "attachment" || (cd == "inline" && filenameDefined) {
|
||||
_, err = e.Attach(bytes.NewReader(p.body), filename, ct)
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case ct == "text/plain":
|
||||
e.Text = p.body
|
||||
case ct == "text/html":
|
||||
e.HTML = p.body
|
||||
}
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// parseMIMEParts will recursively walk a MIME entity and return a []mime.Part containing
|
||||
// each (flattened) mime.Part found.
|
||||
// It is important to note that there are no limits to the number of recursions, so be
|
||||
// careful when parsing unknown MIME structures!
|
||||
func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) {
|
||||
var ps []*part
|
||||
// If no content type is given, set it to the default
|
||||
if _, ok := hs["Content-Type"]; !ok {
|
||||
hs.Set("Content-Type", defaultContentType)
|
||||
}
|
||||
ct, params, err := mime.ParseMediaType(hs.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return ps, err
|
||||
}
|
||||
// If it's a multipart email, recursively parse the parts
|
||||
if strings.HasPrefix(ct, "multipart/") {
|
||||
if _, ok := params["boundary"]; !ok {
|
||||
return ps, ErrMissingBoundary
|
||||
}
|
||||
mr := multipart.NewReader(b, params["boundary"])
|
||||
for {
|
||||
var buf bytes.Buffer
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return ps, err
|
||||
}
|
||||
if _, ok := p.Header["Content-Type"]; !ok {
|
||||
p.Header.Set("Content-Type", defaultContentType)
|
||||
}
|
||||
subct, _, err := mime.ParseMediaType(p.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return ps, err
|
||||
}
|
||||
if strings.HasPrefix(subct, "multipart/") {
|
||||
sps, err := parseMIMEParts(p.Header, p)
|
||||
if err != nil {
|
||||
return ps, err
|
||||
}
|
||||
ps = append(ps, sps...)
|
||||
} else {
|
||||
var reader io.Reader
|
||||
reader = p
|
||||
const cte = "Content-Transfer-Encoding"
|
||||
if p.Header.Get(cte) == "base64" {
|
||||
reader = base64.NewDecoder(base64.StdEncoding, reader)
|
||||
}
|
||||
// Otherwise, just append the part to the list
|
||||
// Copy the part data into the buffer
|
||||
if _, err := io.Copy(&buf, reader); err != nil {
|
||||
return ps, err
|
||||
}
|
||||
ps = append(ps, &part{body: buf.Bytes(), header: p.Header})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it is not a multipart email, parse the body content as a single "part"
|
||||
switch hs.Get("Content-Transfer-Encoding") {
|
||||
case "quoted-printable":
|
||||
b = quotedprintable.NewReader(b)
|
||||
case "base64":
|
||||
b = base64.NewDecoder(base64.StdEncoding, b)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, b); err != nil {
|
||||
return ps, err
|
||||
}
|
||||
ps = append(ps, &part{body: buf.Bytes(), header: hs})
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
// Attach is used to attach content from an io.Reader to the email.
|
||||
// Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type
|
||||
// The function will return the created Attachment for reference, as well as nil for the error, if successful.
|
||||
func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) {
|
||||
var buffer bytes.Buffer
|
||||
if _, err = io.Copy(&buffer, r); err != nil {
|
||||
return
|
||||
}
|
||||
at := &Attachment{
|
||||
Filename: filename,
|
||||
ContentType: c,
|
||||
Header: textproto.MIMEHeader{},
|
||||
Content: buffer.Bytes(),
|
||||
}
|
||||
e.Attachments = append(e.Attachments, at)
|
||||
return at, nil
|
||||
}
|
||||
|
||||
// AttachFile is used to attach content to the email.
|
||||
// It attempts to open the file referenced by filename and, if successful, creates an Attachment.
|
||||
// This Attachment is then appended to the slice of Email.Attachments.
|
||||
// The function will then return the Attachment for reference, as well as nil for the error, if successful.
|
||||
func (e *Email) AttachFile(filename string) (a *Attachment, err error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
ct := mime.TypeByExtension(filepath.Ext(filename))
|
||||
basename := filepath.Base(filename)
|
||||
return e.Attach(f, basename, ct)
|
||||
}
|
||||
|
||||
// msgHeaders merges the Email's various fields and custom headers together in a
|
||||
// standards compliant way to create a MIMEHeader to be used in the resulting
|
||||
// message. It does not alter e.Headers.
|
||||
//
|
||||
// "e"'s fields To, Cc, From, Subject will be used unless they are present in
|
||||
// e.Headers. Unless set in e.Headers, "Date" will filled with the current time.
|
||||
func (e *Email) msgHeaders() (textproto.MIMEHeader, error) {
|
||||
res := make(textproto.MIMEHeader, len(e.Headers)+6)
|
||||
if e.Headers != nil {
|
||||
for _, h := range []string{"Reply-To", "To", "Cc", "From", "Subject", "Date", "Message-Id", "MIME-Version"} {
|
||||
if v, ok := e.Headers[h]; ok {
|
||||
res[h] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set headers if there are values.
|
||||
if _, ok := res["Reply-To"]; !ok && len(e.ReplyTo) > 0 {
|
||||
res.Set("Reply-To", strings.Join(e.ReplyTo, ", "))
|
||||
}
|
||||
if _, ok := res["To"]; !ok && len(e.To) > 0 {
|
||||
res.Set("To", strings.Join(e.To, ", "))
|
||||
}
|
||||
if _, ok := res["Cc"]; !ok && len(e.Cc) > 0 {
|
||||
res.Set("Cc", strings.Join(e.Cc, ", "))
|
||||
}
|
||||
if _, ok := res["Subject"]; !ok && e.Subject != "" {
|
||||
res.Set("Subject", e.Subject)
|
||||
}
|
||||
if _, ok := res["Message-Id"]; !ok {
|
||||
id, err := generateMessageID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Set("Message-Id", id)
|
||||
}
|
||||
// Date and From are required headers.
|
||||
if _, ok := res["From"]; !ok {
|
||||
res.Set("From", e.From)
|
||||
}
|
||||
if _, ok := res["Date"]; !ok {
|
||||
res.Set("Date", time.Now().Format(time.RFC1123Z))
|
||||
}
|
||||
if _, ok := res["MIME-Version"]; !ok {
|
||||
res.Set("MIME-Version", "1.0")
|
||||
}
|
||||
for field, vals := range e.Headers {
|
||||
if _, ok := res[field]; !ok {
|
||||
res[field] = vals
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func writeMessage(buff io.Writer, msg []byte, multipart bool, mediaType string, w *multipart.Writer) error {
|
||||
if multipart {
|
||||
header := textproto.MIMEHeader{
|
||||
"Content-Type": {mediaType + "; charset=UTF-8"},
|
||||
"Content-Transfer-Encoding": {"quoted-printable"},
|
||||
}
|
||||
if _, err := w.CreatePart(header); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
qp := quotedprintable.NewWriter(buff)
|
||||
// Write the text
|
||||
if _, err := qp.Write(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return qp.Close()
|
||||
}
|
||||
|
||||
func (e *Email) categorizeAttachments() (htmlRelated, others []*Attachment) {
|
||||
for _, a := range e.Attachments {
|
||||
if a.HTMLRelated {
|
||||
htmlRelated = append(htmlRelated, a)
|
||||
} else {
|
||||
others = append(others, a)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
|
||||
func (e *Email) Bytes() ([]byte, error) {
|
||||
// TODO: better guess buffer size
|
||||
buff := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
|
||||
headers, err := e.msgHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
htmlAttachments, otherAttachments := e.categorizeAttachments()
|
||||
if len(e.HTML) == 0 && len(htmlAttachments) > 0 {
|
||||
return nil, errors.New("there are HTML attachments, but no HTML body")
|
||||
}
|
||||
|
||||
var (
|
||||
isMixed = len(otherAttachments) > 0
|
||||
isAlternative = len(e.Text) > 0 && len(e.HTML) > 0
|
||||
isRelated = len(e.HTML) > 0 && len(htmlAttachments) > 0
|
||||
)
|
||||
|
||||
var w *multipart.Writer
|
||||
if isMixed || isAlternative || isRelated {
|
||||
w = multipart.NewWriter(buff)
|
||||
}
|
||||
switch {
|
||||
case isMixed:
|
||||
headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
|
||||
case isAlternative:
|
||||
headers.Set("Content-Type", "multipart/alternative;\r\n boundary="+w.Boundary())
|
||||
case isRelated:
|
||||
headers.Set("Content-Type", "multipart/related;\r\n boundary="+w.Boundary())
|
||||
case len(e.HTML) > 0:
|
||||
headers.Set("Content-Type", "text/html; charset=UTF-8")
|
||||
headers.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
default:
|
||||
headers.Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
headers.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
}
|
||||
headerToBytes(buff, headers)
|
||||
_, err = io.WriteString(buff, "\r\n")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check to see if there is a Text or HTML field
|
||||
if len(e.Text) > 0 || len(e.HTML) > 0 {
|
||||
var subWriter *multipart.Writer
|
||||
|
||||
if isMixed && isAlternative {
|
||||
// Create the multipart alternative part
|
||||
subWriter = multipart.NewWriter(buff)
|
||||
header := textproto.MIMEHeader{
|
||||
"Content-Type": {"multipart/alternative;\r\n boundary=" + subWriter.Boundary()},
|
||||
}
|
||||
if _, err := w.CreatePart(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
subWriter = w
|
||||
}
|
||||
// Create the body sections
|
||||
if len(e.Text) > 0 {
|
||||
// Write the text
|
||||
if err := writeMessage(buff, e.Text, isMixed || isAlternative, "text/plain", subWriter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(e.HTML) > 0 {
|
||||
messageWriter := subWriter
|
||||
var relatedWriter *multipart.Writer
|
||||
if (isMixed || isAlternative) && len(htmlAttachments) > 0 {
|
||||
relatedWriter = multipart.NewWriter(buff)
|
||||
header := textproto.MIMEHeader{
|
||||
"Content-Type": {"multipart/related;\r\n boundary=" + relatedWriter.Boundary()},
|
||||
}
|
||||
if _, err := subWriter.CreatePart(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messageWriter = relatedWriter
|
||||
} else if isRelated && len(htmlAttachments) > 0 {
|
||||
relatedWriter = w
|
||||
messageWriter = w
|
||||
}
|
||||
// Write the HTML
|
||||
if err := writeMessage(buff, e.HTML, isMixed || isAlternative || isRelated, "text/html", messageWriter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(htmlAttachments) > 0 {
|
||||
for _, a := range htmlAttachments {
|
||||
a.setDefaultHeaders()
|
||||
ap, err := relatedWriter.CreatePart(a.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Write the base64Wrapped content to the part
|
||||
base64Wrap(ap, a.Content)
|
||||
}
|
||||
|
||||
if isMixed || isAlternative {
|
||||
relatedWriter.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
if isMixed && isAlternative {
|
||||
if err := subWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create attachment part, if necessary
|
||||
for _, a := range otherAttachments {
|
||||
a.setDefaultHeaders()
|
||||
ap, err := w.CreatePart(a.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Write the base64Wrapped content to the part
|
||||
base64Wrap(ap, a.Content)
|
||||
}
|
||||
if isMixed || isAlternative || isRelated {
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buff.Bytes(), nil
|
||||
}
|
||||
|
||||
// Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail
|
||||
// This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message
|
||||
func (e *Email) Send(addr string, a smtp.Auth) error {
|
||||
// Merge the To, Cc, and Bcc fields
|
||||
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
for i := 0; i < len(to); i++ {
|
||||
addr, err := mail.ParseAddress(to[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to[i] = addr.Address
|
||||
}
|
||||
// Check to make sure there is at least one recipient and one "From" address
|
||||
if e.From == "" || len(to) == 0 {
|
||||
return errors.New("Must specify at least one From address and one To address")
|
||||
}
|
||||
sender, err := e.parseSender()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := e.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return smtp.SendMail(addr, a, sender, to, raw)
|
||||
}
|
||||
|
||||
// Select and parse an SMTP envelope sender address. Choose Email.Sender if set, or fallback to Email.From.
|
||||
func (e *Email) parseSender() (string, error) {
|
||||
if e.Sender != "" {
|
||||
sender, err := mail.ParseAddress(e.Sender)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sender.Address, nil
|
||||
} else {
|
||||
from, err := mail.ParseAddress(e.From)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return from.Address, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SendWithTLS sends an email over tls with an optional TLS config.
|
||||
//
|
||||
// The TLS Config is helpful if you need to connect to a host that is used an untrusted
|
||||
// certificate.
|
||||
func (e *Email) SendWithTLS(addr string, a smtp.Auth, t *tls.Config) error {
|
||||
// Merge the To, Cc, and Bcc fields
|
||||
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
for i := 0; i < len(to); i++ {
|
||||
addr, err := mail.ParseAddress(to[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to[i] = addr.Address
|
||||
}
|
||||
// Check to make sure there is at least one recipient and one "From" address
|
||||
if e.From == "" || len(to) == 0 {
|
||||
return errors.New("Must specify at least one From address and one To address")
|
||||
}
|
||||
sender, err := e.parseSender()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := e.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := smtp.NewClient(conn, t.ServerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.Hello("localhost"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
if ok, _ := c.Extension("AUTH"); ok {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = c.Mail(sender); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// SendWithStartTLS sends an email over TLS using STARTTLS with an optional TLS config.
|
||||
//
|
||||
// The TLS Config is helpful if you need to connect to a host that is used an untrusted
|
||||
// certificate.
|
||||
func (e *Email) SendWithStartTLS(addr string, a smtp.Auth, t *tls.Config) error {
|
||||
// Merge the To, Cc, and Bcc fields
|
||||
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
for i := 0; i < len(to); i++ {
|
||||
addr, err := mail.ParseAddress(to[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to[i] = addr.Address
|
||||
}
|
||||
// Check to make sure there is at least one recipient and one "From" address
|
||||
if e.From == "" || len(to) == 0 {
|
||||
return errors.New("Must specify at least one From address and one To address")
|
||||
}
|
||||
sender, err := e.parseSender()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := e.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Taken from the standard library
|
||||
// https://github.com/golang/go/blob/master/src/net/smtp/smtp.go#L328
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.Hello("localhost"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Use TLS if available
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
if err = c.StartTLS(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
if ok, _ := c.Extension("AUTH"); ok {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = c.Mail(sender); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Attachment is a struct representing an email attachment.
|
||||
// Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
|
||||
type Attachment struct {
|
||||
Filename string
|
||||
ContentType string
|
||||
Header textproto.MIMEHeader
|
||||
Content []byte
|
||||
HTMLRelated bool
|
||||
}
|
||||
|
||||
func (at *Attachment) setDefaultHeaders() {
|
||||
contentType := "application/octet-stream"
|
||||
if len(at.ContentType) > 0 {
|
||||
contentType = at.ContentType
|
||||
}
|
||||
at.Header.Set("Content-Type", contentType)
|
||||
|
||||
if len(at.Header.Get("Content-Disposition")) == 0 {
|
||||
disposition := "attachment"
|
||||
if at.HTMLRelated {
|
||||
disposition = "inline"
|
||||
}
|
||||
at.Header.Set("Content-Disposition", fmt.Sprintf("%s;\r\n filename=\"%s\"", disposition, at.Filename))
|
||||
}
|
||||
if len(at.Header.Get("Content-ID")) == 0 {
|
||||
at.Header.Set("Content-ID", fmt.Sprintf("<%s>", at.Filename))
|
||||
}
|
||||
if len(at.Header.Get("Content-Transfer-Encoding")) == 0 {
|
||||
at.Header.Set("Content-Transfer-Encoding", "base64")
|
||||
}
|
||||
}
|
||||
|
||||
// base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
|
||||
// The output is then written to the specified io.Writer
|
||||
func base64Wrap(w io.Writer, b []byte) {
|
||||
// 57 raw bytes per 76-byte base64 line.
|
||||
const maxRaw = 57
|
||||
// Buffer for each line, including trailing CRLF.
|
||||
buffer := make([]byte, MaxLineLength+len("\r\n"))
|
||||
copy(buffer[MaxLineLength:], "\r\n")
|
||||
// Process raw chunks until there's no longer enough to fill a line.
|
||||
for len(b) >= maxRaw {
|
||||
base64.StdEncoding.Encode(buffer, b[:maxRaw])
|
||||
w.Write(buffer)
|
||||
b = b[maxRaw:]
|
||||
}
|
||||
// Handle the last chunk of bytes.
|
||||
if len(b) > 0 {
|
||||
out := buffer[:base64.StdEncoding.EncodedLen(len(b))]
|
||||
base64.StdEncoding.Encode(out, b)
|
||||
out = append(out, "\r\n"...)
|
||||
w.Write(out)
|
||||
}
|
||||
}
|
||||
|
||||
// headerToBytes renders "header" to "buff". If there are multiple values for a
|
||||
// field, multiple "Field: value\r\n" lines will be emitted.
|
||||
func headerToBytes(buff io.Writer, header textproto.MIMEHeader) {
|
||||
for field, vals := range header {
|
||||
for _, subval := range vals {
|
||||
// bytes.Buffer.Write() never returns an error.
|
||||
io.WriteString(buff, field)
|
||||
io.WriteString(buff, ": ")
|
||||
// Write the encoded header if needed
|
||||
switch {
|
||||
case field == "Content-Type" || field == "Content-Disposition":
|
||||
buff.Write([]byte(subval))
|
||||
case field == "From" || field == "To" || field == "Cc" || field == "Bcc":
|
||||
participants := strings.Split(subval, ",")
|
||||
for i, v := range participants {
|
||||
addr, err := mail.ParseAddress(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
participants[i] = addr.String()
|
||||
}
|
||||
buff.Write([]byte(strings.Join(participants, ", ")))
|
||||
default:
|
||||
buff.Write([]byte(mime.QEncoding.Encode("UTF-8", subval)))
|
||||
}
|
||||
io.WriteString(buff, "\r\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var maxBigInt = big.NewInt(math.MaxInt64)
|
||||
|
||||
// generateMessageID generates and returns a string suitable for an RFC 2822
|
||||
// compliant Message-ID, e.g.:
|
||||
// <1444789264909237300.3464.1819418242800517193@DESKTOP01>
|
||||
//
|
||||
// The following parameters are used to generate a Message-ID:
|
||||
// - The nanoseconds since Epoch
|
||||
// - The calling PID
|
||||
// - A cryptographically random int64
|
||||
// - The sending hostname
|
||||
func generateMessageID() (string, error) {
|
||||
t := time.Now().UnixNano()
|
||||
pid := os.Getpid()
|
||||
rint, err := rand.Int(rand.Reader, maxBigInt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := os.Hostname()
|
||||
// If we can't get the hostname, we'll use localhost
|
||||
if err != nil {
|
||||
h = "localhost.localdomain"
|
||||
}
|
||||
msgid := fmt.Sprintf("<%d.%d.%d@%s>", t, pid, rint, h)
|
||||
return msgid, nil
|
||||
}
|
||||
Reference in New Issue
Block a user