You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
4.1 KiB

  1. // Forked from github.com/SlyMarbo/gmail
  2. package alert
  3. import (
  4. "bytes"
  5. "crypto/tls"
  6. "encoding/base64"
  7. "errors"
  8. "fmt"
  9. "io/ioutil"
  10. "net/smtp"
  11. "path/filepath"
  12. "regexp"
  13. "strings"
  14. )
  15. // Convenience function
  16. func SendEmail(subject, body string, tos []string) error {
  17. email := Compose(subject, body)
  18. email.From = config.GetString("smtp_user")
  19. email.ContentType = "text/html; charset=utf-8"
  20. email.AddRecipients(tos...)
  21. err := email.Send()
  22. return err
  23. }
  24. // Email represents a single message, which may contain
  25. // attachments.
  26. type Email struct {
  27. From string
  28. To []string
  29. Subject string
  30. ContentType string
  31. Body string
  32. Attachments map[string][]byte
  33. }
  34. // Compose begins a new email, filling the subject and body,
  35. // and allocating memory for the list of recipients and the
  36. // attachments.
  37. func Compose(Subject, Body string) *Email {
  38. out := new(Email)
  39. out.To = make([]string, 0, 1)
  40. out.Subject = Subject
  41. out.Body = Body
  42. out.Attachments = make(map[string][]byte)
  43. return out
  44. }
  45. // Attach takes a filename and adds this to the message.
  46. // Note that since only the filename is stored (and not
  47. // its path, for privacy reasons), multiple files in
  48. // different directories but with the same filename and
  49. // extension cannot be sent.
  50. func (e *Email) Attach(Filename string) error {
  51. b, err := ioutil.ReadFile(Filename)
  52. if err != nil {
  53. return err
  54. }
  55. _, fname := filepath.Split(Filename)
  56. e.Attachments[fname] = b
  57. return nil
  58. }
  59. // AddRecipient adds a single recipient.
  60. func (e *Email) AddRecipient(Recipient string) {
  61. e.To = append(e.To, Recipient)
  62. }
  63. // AddRecipients adds one or more recipients.
  64. func (e *Email) AddRecipients(Recipients ...string) {
  65. e.To = append(e.To, Recipients...)
  66. }
  67. // Send sends the email, returning any error encountered.
  68. func (e *Email) Send() error {
  69. if e.From == "" {
  70. return errors.New("Error: No sender specified. Please set the Email.From field.")
  71. }
  72. if e.To == nil || len(e.To) == 0 {
  73. return errors.New("Error: No recipient specified. Please set the Email.To field.")
  74. }
  75. auth := smtp.PlainAuth(
  76. "",
  77. config.GetString("smtp_user"),
  78. config.GetString("smtp_password"),
  79. config.GetString("smtp_host"),
  80. )
  81. conn, err := smtp.Dial(fmt.Sprintf("%v:%v", config.GetString("smtp_host"), config.GetString("smtp_port")))
  82. if err != nil {
  83. return err
  84. }
  85. err = conn.StartTLS(&tls.Config{})
  86. if err != nil {
  87. return err
  88. }
  89. err = conn.Auth(auth)
  90. if err != nil {
  91. return err
  92. }
  93. err = conn.Mail(e.From)
  94. if err != nil {
  95. if strings.Contains(err.Error(), "530 5.5.1") {
  96. return errors.New("Error: Authentication failure. Your username or password is incorrect.")
  97. }
  98. return err
  99. }
  100. for _, recipient := range e.To {
  101. err = conn.Rcpt(recipient)
  102. if err != nil {
  103. return err
  104. }
  105. }
  106. wc, err := conn.Data()
  107. if err != nil {
  108. return err
  109. }
  110. defer wc.Close()
  111. _, err = wc.Write(e.Bytes())
  112. if err != nil {
  113. return err
  114. }
  115. return nil
  116. }
  117. func (e *Email) Bytes() []byte {
  118. buf := bytes.NewBuffer(nil)
  119. var subject = e.Subject
  120. subject = regexp.MustCompile("\n+").ReplaceAllString(subject, " ")
  121. subject = regexp.MustCompile(" +").ReplaceAllString(subject, " ")
  122. buf.WriteString("Subject: " + subject + "\n")
  123. buf.WriteString("To: <" + strings.Join(e.To, ">,<") + ">\n")
  124. buf.WriteString("MIME-Version: 1.0\n")
  125. // Boundary is used by MIME to separate files.
  126. boundary := "f46d043c813270fc6b04c2d223da"
  127. if len(e.Attachments) > 0 {
  128. buf.WriteString("Content-Type: multipart/mixed; boundary=" + boundary + "\n")
  129. buf.WriteString("--" + boundary + "\n")
  130. }
  131. if e.ContentType == "" {
  132. e.ContentType = "text/plain; charset=utf-8"
  133. }
  134. buf.WriteString(fmt.Sprintf("Content-Type: %s\n\n", e.ContentType))
  135. buf.WriteString(e.Body)
  136. if len(e.Attachments) > 0 {
  137. for k, v := range e.Attachments {
  138. buf.WriteString("\n\n--" + boundary + "\n")
  139. buf.WriteString("Content-Type: application/octet-stream\n")
  140. buf.WriteString("Content-Transfer-Encoding: base64\n")
  141. buf.WriteString("Content-Disposition: attachment; filename=\"" + k + "\"\n\n")
  142. b := make([]byte, base64.StdEncoding.EncodedLen(len(v)))
  143. base64.StdEncoding.Encode(b, v)
  144. buf.Write(b)
  145. buf.WriteString("\n--" + boundary)
  146. }
  147. buf.WriteString("--")
  148. }
  149. return buf.Bytes()
  150. }