由于我们使用的阿里云服务器不能telnet 25端口,发送ssl时候感觉很蹩脚,就自己写了一个go mail 发送


1、文档结构

image.png

2、main.go

// main.go
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/smtp"
	"os"
	"sslmail/mymail"
	"strings"
)

func SendMail(fromuser, password, subject, host, port, bodyfile, bodyhtmlfile string, tousers, attachs []string) error {
	// NewEmail返回一个email结构体的指针
	e := mymail.NewEmail()
	// 发件人
	e.From = fromuser
	// 收件人(可以有多个)
	e.To = tousers
	// 邮件主题
	e.Subject = subject
	// 解析html模板
	//body := new(bytes.Buffer)
	if strings.TrimSpace(bodyfile) != "" {
		f, err := os.OpenFile(bodyfile, os.O_RDONLY, 0600)
		if err != nil {
			fmt.Println(err)
		} else {
			contentBytes, err := ioutil.ReadAll(f)
			if err != nil {
				fmt.Println("读取文件失败")
			} else {
				e.Text = contentBytes
			}
		}
	}

	if strings.TrimSpace(bodyhtmlfile) != "" {
		ft, err := os.OpenFile(bodyhtmlfile, os.O_RDONLY, 0600)
		if err != nil {
			fmt.Println(err)
		} else {
			htmlBytes, err := ioutil.ReadAll(ft)
			if err != nil {
				fmt.Println("读取文件失败")
			} else {
				e.HTML = htmlBytes
			}
		}
	}

	if len(attachs) > 0 {
		for _, v := range attachs {
			e.AttachFile(v)
		}
	}
	addr := host + ":" + port
	//fmt.Println(addr)
	// 发送邮件(如果使用QQ邮箱发送邮件的话,passwd不是邮箱密码而是授权码)
	return e.Send(addr, smtp.PlainAuth("", fromuser, password, host))
}

type sliceValue []string

func newSliceValue(vals []string, p *[]string) *sliceValue {
	*p = vals
	return (*sliceValue)(p)
}

func (s *sliceValue) Set(val string) error {
	*s = sliceValue(strings.Split(val, ","))
	return nil
}

func (s *sliceValue) Get() interface{} { return []string(*s) }

func (s *sliceValue) String() string { return strings.Join([]string(*s), ",") }

func main() {

	//fromuser := "name@yourmail.com"
	fromuser := flag.String("fromuser", "name@yourmail.com", "sender email info")
	password := flag.String("password", "your password default", "sender email password")
	var tousers []string
	flag.Var(newSliceValue([]string{}, &tousers), "tousers", "your `tousers` email separated by ','")
	subject := flag.String("subject", "hello,world", "subject")
	host := flag.String("host", "smtp.qiye.163.com", "ssl url info")
	port := flag.String("port", "465", "ssl port")
	bodyfile := flag.String("bodyfile", "", "the body file your password default")
	bodyhtmlfile := flag.String("bodyhtmlfile", "", "the body html file your password default")
	var attachs []string
	flag.Var(newSliceValue([]string{}, &attachs), "attachs", "your `attachs` email separated by ','")
	//htmlbody := flag.String("htmlbody", "a html file ", "you can describe you mail")
	flag.Parse()
	//fmt.Println(attachs)
	err := SendMail(*fromuser, *password, *subject, *host, *port, *bodyfile, *bodyhtmlfile, tousers, attachs)
	if err != nil {
		log.Println("发送邮件失败")
		//log.Println(err)
		return
	}
	log.Println("发送邮件成功")
}

3、mymail/email.go

// 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 mymail

import (
    "bufio"
    "bytes"
    "crypto/rand"
    "crypto/tls"
    "encoding/base64"
    "errors"
    "fmt"
    "io"
    "log"
    "math"
    "math/big"
    "mime"
    "mime/multipart"
    "mime/quotedprintable"
    "net"
    "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
}

// Read trims off any unicode whitespace from the originating reader
func (tr trimReader) Read(buf []byte) (int, error) {
    n, err := tr.rd.Read(buf)
    t := bytes.TrimLeftFunc(buf[:n], unicode.IsSpace)
    n = copy(buf, t)
    return n, err
}

// 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 {
        case h == "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 h == "To":
            for _, to := range v {
                tt, err := (&mime.WordDecoder{}).DecodeHeader(to)
                if err == nil {
                    e.To = append(e.To, tt)
                } else {
                    e.To = append(e.To, to)
                }
            }
            delete(hdrs, h)
        case h == "Cc":
            for _, cc := range v {
                tcc, err := (&mime.WordDecoder{}).DecodeHeader(cc)
                if err == nil {
                    e.Cc = append(e.Cc, tcc)
                } else {
                    e.Cc = append(e.Cc, cc)
                }
            }
            delete(hdrs, h)
        case h == "Bcc":
            for _, bcc := range v {
                tbcc, err := (&mime.WordDecoder{}).DecodeHeader(bcc)
                if err == nil {
                    e.Bcc = append(e.Bcc, tbcc)
                } else {
                    e.Bcc = append(e.Bcc, bcc)
                }
            }
            delete(hdrs, h)
        case h == "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
        }
        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"
        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,
        Header:   textproto.MIMEHeader{},
        Content:  buffer.Bytes(),
    }
    // Get the Content-Type to be used in the MIMEHeader
    if c != "" {
        at.Header.Set("Content-Type", c)
    } else {
        // If the Content-Type is blank, set the Content-Type to "application/octet-stream"
        at.Header.Set("Content-Type", "application/octet-stream")
    }
    at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename))
    at.Header.Set("Content-ID", fmt.Sprintf("<%s>", filename))
    at.Header.Set("Content-Transfer-Encoding", "base64")
    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)+4)
    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()
}

// 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
    }

    var (
        isMixed       = len(e.Attachments) > 0
        isAlternative = len(e.Text) > 0 && len(e.HTML) > 0
    )

    var w *multipart.Writer
    if isMixed || isAlternative {
        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 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 {
            // Write the HTML
            if err := writeMessage(buff, e.HTML, isMixed || isAlternative, "text/html", subWriter); err != nil {
                return nil, err
            }
        }
        if isMixed && isAlternative {
            if err := subWriter.Close(); err != nil {
                return nil, err
            }
        }
    }
    // Create attachment part, if necessary
    for _, a := range e.Attachments {
        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 {
        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 SendMailUsingTLS(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
    }
}

// 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
    Header   textproto.MIMEHeader
    Content  []byte
}

// 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))
            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
}

func Dial(addr string) (*smtp.Client, error) {
    conn, err := tls.Dial("tcp", addr, nil)
    if err != nil {
        log.Println("Dialing Error:", err)
        return nil, err
    }
    //分解主机端口字符串
    host, _, _ := net.SplitHostPort(addr)
    return smtp.NewClient(conn, host)
}

//参考net/smtp的func SendMail()
//使用net.Dial连接tls(ssl)端口时,smtp.NewClient()会卡住且不提示err
//len(to)>1时,to[1]开始提示是密送
func SendMailUsingTLS(addr string, auth smtp.Auth, from string,
    to []string, msg []byte) (err error) {

    //create smtp client
    c, err := Dial(addr)
    if err != nil {
        log.Println("Create smpt client error:", err)
        return err
    }
    defer c.Close()

    if auth != nil {
        if ok, _ := c.Extension("AUTH"); ok {
            if err = c.Auth(auth); err != nil {
                log.Println("Error during AUTH", err)
                return err
            }
        }
    }

    if err = c.Mail(from); 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(msg)
    if err != nil {
        return err
    }

    err = w.Close()
    if err != nil {
        return err
    }

    return c.Quit()
}

4、build

go build -o sslmail  main.go

5、测试,前提你的默认值要有

image.png

结果:

image.png