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 }