github.com/admpub/mail@v0.0.0-20170408110349-d63147b0317b/smtp.go (about)

     1  package mail
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"crypto/tls"
     7  	"encoding/base64"
     8  	"encoding/hex"
     9  	"errors"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"log"
    13  	"net/mail"
    14  	"net/smtp"
    15  	"path"
    16  	"path/filepath"
    17  	"regexp"
    18  	"strconv"
    19  	"strings"
    20  )
    21  
    22  var (
    23  	imageRegex  = regexp.MustCompile(`(src|background)=["'](.*?)["']`)
    24  	schemeRegxp = regexp.MustCompile(`^[A-z]+://`)
    25  )
    26  
    27  // Mail will represent a formatted email
    28  type Mail struct {
    29  	To         []string
    30  	ToName     []string
    31  	Subject    string
    32  	HTML       string
    33  	Text       string
    34  	From       string
    35  	Bcc        []string
    36  	FromName   string
    37  	ReplyTo    string
    38  	Date       string
    39  	Files      map[string]string
    40  	Headers    string
    41  	BaseDir    string //内容中图片路径
    42  	Charset    string //编码
    43  	RetReceipt string //回执地址,空白则禁用回执
    44  }
    45  
    46  // NewMail returns a new Mail
    47  func NewMail() Mail {
    48  	return Mail{}
    49  }
    50  
    51  // SMTPClient struct
    52  type SMTPClient struct {
    53  	smtpAuth smtp.Auth
    54  	host     string
    55  	port     string
    56  	user     string
    57  	secure   string
    58  }
    59  
    60  // SMTPConfig 配置结构体
    61  type SMTPConfig struct {
    62  	Username string
    63  	Password string
    64  	Host     string
    65  	Port     int
    66  	Secure   string
    67  	Identity string
    68  }
    69  
    70  func (s *SMTPConfig) Address() string {
    71  	if s.Port == 0 {
    72  		s.Port = 25
    73  	}
    74  	return s.Host + `:` + strconv.Itoa(s.Port)
    75  }
    76  
    77  func (s *SMTPConfig) Auth() smtp.Auth {
    78  	var auth smtp.Auth
    79  	s.Secure = strings.ToUpper(s.Secure)
    80  	switch s.Secure {
    81  	case "NONE":
    82  		auth = unencryptedAuth{smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)}
    83  	case "LOGIN":
    84  		auth = LoginAuth(s.Username, s.Password)
    85  	case "SSL":
    86  		fallthrough
    87  	default:
    88  		auth = smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)
    89  	}
    90  	return auth
    91  }
    92  
    93  func NewSMTPClient(conf *SMTPConfig) SMTPClient {
    94  	return SMTPClient{
    95  		smtpAuth: conf.Auth(),
    96  		host:     conf.Host,
    97  		port:     strconv.Itoa(conf.Port),
    98  		user:     conf.Username,
    99  		secure:   conf.Secure,
   100  	}
   101  }
   102  
   103  // NewMail returns a new Mail
   104  func (c *SMTPClient) NewMail() Mail {
   105  	return NewMail()
   106  }
   107  
   108  // Send - It can be used for generic SMTP stuff
   109  func (c *SMTPClient) Send(m Mail) error {
   110  	length := 0
   111  	if len(m.Charset) == 0 {
   112  		m.Charset = "utf-8"
   113  	}
   114  	boundary := "COSCMSBOUNDARYFORSMTPGOLIB"
   115  	var message bytes.Buffer
   116  	message.WriteString(fmt.Sprintf("X-SMTPAPI: %s\r\n", m.Headers))
   117  	//回执
   118  	if len(m.RetReceipt) > 0 {
   119  		message.WriteString(fmt.Sprintf("Return-Receipt-To: %s\r\n", m.RetReceipt))
   120  		message.WriteString(fmt.Sprintf("Disposition-Notification-To: %s\r\n", m.RetReceipt))
   121  	}
   122  	message.WriteString(fmt.Sprintf("From: %s <%s>\r\n", m.FromName, m.From))
   123  	if len(m.ReplyTo) > 0 {
   124  		message.WriteString(fmt.Sprintf("Return-Path: %s\r\n", m.ReplyTo))
   125  	}
   126  	length = len(m.To)
   127  	if length > 0 {
   128  		nameLength := len(m.ToName)
   129  		if nameLength > 0 {
   130  			message.WriteString(fmt.Sprintf("To: %s <%s>", m.ToName[0], m.To[0]))
   131  		} else {
   132  			message.WriteString(fmt.Sprintf("To: <%s>", m.To[0]))
   133  		}
   134  		for i := 1; i < length; i++ {
   135  			if nameLength > i {
   136  				message.WriteString(fmt.Sprintf(", %s <%s>", m.ToName[i], m.To[i]))
   137  			} else {
   138  				message.WriteString(fmt.Sprintf(", <%s>", m.To[i]))
   139  			}
   140  		}
   141  	}
   142  	length = len(m.Bcc)
   143  	if length > 0 {
   144  		message.WriteString(fmt.Sprintf("Bcc: <%s>", m.Bcc[0]))
   145  		for i := 1; i < length; i++ {
   146  			message.WriteString(fmt.Sprintf(", <%s>", m.Bcc[i]))
   147  		}
   148  	}
   149  	message.WriteString("\r\n")
   150  	message.WriteString(fmt.Sprintf("Subject: %s\r\n", m.Subject))
   151  	message.WriteString("MIME-Version: 1.0\r\n")
   152  	if m.Files != nil {
   153  		message.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\n--%s\r\n", boundary, boundary))
   154  	}
   155  	if len(m.HTML) > 0 {
   156  		//解析内容中的图片
   157  		rs := imageRegex.FindAllStringSubmatch(m.HTML, -1)
   158  		var embedImages string
   159  		for _, v := range rs {
   160  			surl := v[2]
   161  			if v2 := schemeRegxp.FindStringIndex(surl); v2 == nil {
   162  				filename := path.Base(surl)
   163  				directory := path.Dir(surl)
   164  				if directory == "." {
   165  					directory = ""
   166  				}
   167  				h := md5.New()
   168  				h.Write([]byte(surl + "@coscms.0"))
   169  				cid := hex.EncodeToString(h.Sum(nil))
   170  				if len(m.BaseDir) > 0 && !strings.HasSuffix(m.BaseDir, "/") {
   171  					m.BaseDir += "/"
   172  				}
   173  				if len(directory) > 0 && !strings.HasSuffix(directory, "/") {
   174  					directory += "/"
   175  				}
   176  				if str, err := m.ReadAttachment(m.BaseDir + directory + filename); err == nil {
   177  					re3 := regexp.MustCompile(v[1] + `=["']` + regexp.QuoteMeta(surl) + `["']`)
   178  					m.HTML = re3.ReplaceAllString(m.HTML, v[1]+`="cid:`+cid+`"`)
   179  
   180  					embedImages += fmt.Sprintf("--%s\r\n", boundary)
   181  					embedImages += fmt.Sprintf("Content-Type: application/octet-stream; name=\"%s\"; charset=\"%s\"\r\n", filename, m.Charset)
   182  					embedImages += fmt.Sprintf("Content-Description: %s\r\n", filename)
   183  					embedImages += fmt.Sprintf("Content-Disposition: inline; filename=\"%s\"; charset=\"%s\"\r\n", filename, m.Charset)
   184  					embedImages += fmt.Sprintf("Content-Transfer-Encoding: base64\r\nContent-ID: <%s>\r\n\r\n%s\r\n\n", cid, str)
   185  				}
   186  			}
   187  		}
   188  		part := fmt.Sprintf("Content-Type: text/html\r\n\n%s\r\n\n", m.HTML)
   189  		message.WriteString(part)
   190  		message.WriteString(embedImages)
   191  	} else {
   192  		part := fmt.Sprintf("Content-Type: text/plain\r\n\n%s\r\n\n", m.Text)
   193  		message.WriteString(part)
   194  	}
   195  	if m.Files != nil {
   196  		for key, value := range m.Files {
   197  			message.WriteString(fmt.Sprintf("--%s\r\n", boundary))
   198  			message.WriteString("Content-Type: application/octect-stream\r\n")
   199  			message.WriteString("Content-Transfer-Encoding:base64\r\n")
   200  			message.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=\"%s\"; charset=\"%s\"\r\n\r\n%s\r\n\n", key, m.Charset, value))
   201  		}
   202  		message.WriteString(fmt.Sprintf("--%s--", boundary))
   203  	}
   204  	if c.secure == "SSL" || c.secure == "TLS" {
   205  		return c.SendTLS(m, message)
   206  	}
   207  	return smtp.SendMail(c.host+":"+c.port, c.smtpAuth, m.From, m.To, message.Bytes())
   208  }
   209  
   210  //SendTLS 通过TLS发送
   211  func (c *SMTPClient) SendTLS(m Mail, message bytes.Buffer) error {
   212  
   213  	var ct *smtp.Client
   214  	var err error
   215  	// TLS config
   216  	tlsconfig := &tls.Config{
   217  		InsecureSkipVerify: true,
   218  		ServerName:         c.host,
   219  	}
   220  
   221  	// Here is the key, you need to call tls.Dial instead of smtp.Dial
   222  	// for smtp servers running on 465 that require an ssl connection
   223  	// from the very beginning (no starttls)
   224  	conn, err := tls.Dial("tcp", c.host+":"+c.port, tlsconfig)
   225  	if err != nil {
   226  		log.Println(err, c.host)
   227  		return err
   228  	}
   229  
   230  	ct, err = smtp.NewClient(conn, c.host)
   231  	if err != nil {
   232  		log.Println(err)
   233  		return err
   234  	}
   235  
   236  	// Auth
   237  	if err = ct.Auth(c.smtpAuth); err != nil {
   238  		log.Println("Auth Error:",
   239  			err,
   240  			c.user,
   241  		)
   242  		return err
   243  	}
   244  
   245  	// To && From
   246  	if err = ct.Mail(m.From); err != nil {
   247  		log.Println("Mail Error:", err, m.From)
   248  		return err
   249  	}
   250  
   251  	for _, v := range m.To {
   252  		if err := ct.Rcpt(v); err != nil {
   253  			log.Println("Rcpt Error:", err, v)
   254  			return err
   255  		}
   256  	}
   257  
   258  	// Data
   259  	w, err := ct.Data()
   260  	if err != nil {
   261  		log.Println("Data Object Error:", err)
   262  		return err
   263  	}
   264  
   265  	_, err = w.Write(message.Bytes())
   266  	if err != nil {
   267  		log.Println("Write Data Object Error:", err)
   268  		return err
   269  	}
   270  
   271  	err = w.Close()
   272  	if err != nil {
   273  		log.Println("Data Object Close Error:", err)
   274  		return err
   275  	}
   276  
   277  	ct.Quit()
   278  	return nil
   279  }
   280  
   281  // AddTo will take a valid email address and store it in the mail.
   282  // It will return an error if the email is invalid.
   283  func (m *Mail) AddTo(email string) error {
   284  	//Parses a single RFC 5322 address, e.g. "Barry Gibbs <bg@example.com>"
   285  	parsedAddess, e := mail.ParseAddress(email)
   286  	if e != nil {
   287  		return e
   288  	}
   289  	m.AddRecipient(parsedAddess)
   290  	return nil
   291  }
   292  
   293  // SetTos 设置收信人Email地址
   294  func (m *Mail) SetTos(emails []string) {
   295  	m.To = emails
   296  }
   297  
   298  // AddToName will add a new receipient name to mail
   299  func (m *Mail) AddToName(name string) {
   300  	m.ToName = append(m.ToName, name)
   301  }
   302  
   303  // AddRecipient will take an already parsed mail.Address
   304  func (m *Mail) AddRecipient(receipient *mail.Address) {
   305  	m.To = append(m.To, receipient.Address)
   306  	if len(receipient.Name) > 0 {
   307  		m.ToName = append(m.ToName, receipient.Name)
   308  	}
   309  }
   310  
   311  // AddSubject will set the subject of the mail
   312  func (m *Mail) AddSubject(s string) {
   313  	m.Subject = s
   314  }
   315  
   316  // AddHTML will set the body of the mail
   317  func (m *Mail) AddHTML(html string) {
   318  	m.HTML = html
   319  }
   320  
   321  // AddText will set the body of the email
   322  func (m *Mail) AddText(text string) {
   323  	m.Text = text
   324  }
   325  
   326  // AddFrom will set the senders email
   327  func (m *Mail) AddFrom(from string) error {
   328  	//Parses a single RFC 5322 address, e.g. "Barry Gibbs <bg@example.com>"
   329  	parsedAddess, e := mail.ParseAddress(from)
   330  	if e != nil {
   331  		return e
   332  	}
   333  	m.From = parsedAddess.Address
   334  	m.FromName = parsedAddess.Name
   335  	return nil
   336  }
   337  
   338  // AddBCC works like AddTo but for BCC
   339  func (m *Mail) AddBCC(email string) error {
   340  	parsedAddess, e := mail.ParseAddress(email)
   341  	if e != nil {
   342  		return e
   343  	}
   344  	m.Bcc = append(m.Bcc, parsedAddess.Address)
   345  	return nil
   346  }
   347  
   348  // AddRecipientBCC works like AddRecipient but for BCC
   349  func (m *Mail) AddRecipientBCC(email *mail.Address) {
   350  	m.Bcc = append(m.Bcc, email.Address)
   351  }
   352  
   353  // AddFromName will set the senders name
   354  func (m *Mail) AddFromName(name string) {
   355  	m.FromName = name
   356  }
   357  
   358  // AddReplyTo will set the return address
   359  func (m *Mail) AddReplyTo(reply string) {
   360  	m.ReplyTo = reply
   361  }
   362  
   363  // AddDate specifies the date
   364  func (m *Mail) AddDate(date string) {
   365  	m.Date = date
   366  }
   367  
   368  // AddAttachment will include file/s in mail
   369  func (m *Mail) AddAttachment(filePath string) error {
   370  	if m.Files == nil {
   371  		m.Files = make(map[string]string)
   372  	}
   373  	str, err := m.ReadAttachment(filePath)
   374  	if err != nil {
   375  		return err
   376  	}
   377  	_, filename := filepath.Split(filePath)
   378  	m.Files[filename] = str
   379  	return nil
   380  }
   381  
   382  // ReadAttachment reading attachment
   383  func (m *Mail) ReadAttachment(filePath string) (string, error) {
   384  	file, e := ioutil.ReadFile(filePath)
   385  	if e != nil {
   386  		return "", e
   387  	}
   388  	encoded := base64.StdEncoding.EncodeToString(file)
   389  	totalChars := len(encoded)
   390  	maxLength := 500 //每行最大长度
   391  	totalLines := totalChars / maxLength
   392  	var buf bytes.Buffer
   393  	for i := 0; i < totalLines; i++ {
   394  		buf.WriteString(encoded[i*maxLength:(i+1)*maxLength] + "\n")
   395  	}
   396  	buf.WriteString(encoded[totalLines*maxLength:])
   397  	return buf.String(), nil
   398  }
   399  
   400  // AddHeaders addding header string
   401  func (m *Mail) AddHeaders(headers string) {
   402  	m.Headers = headers
   403  }
   404  
   405  // =======================================================
   406  // unencryptedAuth
   407  // =======================================================
   408  
   409  type unencryptedAuth struct {
   410  	smtp.Auth
   411  }
   412  
   413  func (a unencryptedAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
   414  	s := *server
   415  	s.TLS = true
   416  	return a.Auth.Start(&s)
   417  }
   418  
   419  // ======================================================
   420  // loginAuth
   421  // ======================================================
   422  
   423  type loginAuth struct {
   424  	username, password string
   425  }
   426  
   427  // LoginAuth loginAuth方式认证
   428  func LoginAuth(username, password string) smtp.Auth {
   429  	return &loginAuth{username, password}
   430  }
   431  
   432  func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
   433  	if !server.TLS {
   434  		return "", nil, errors.New("unencrypted connection")
   435  	}
   436  	return "LOGIN", []byte(a.username), nil
   437  }
   438  
   439  func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
   440  	if more {
   441  		switch string(fromServer) {
   442  		case "Username:":
   443  			return []byte(a.username), nil
   444  		case "Password:":
   445  			return []byte(a.password), nil
   446  		default:
   447  			return nil, errors.New("Unkown fromServer")
   448  		}
   449  	}
   450  	return nil, nil
   451  }