support send mail
Showing
2 changed files
with
323 additions
and
0 deletions
utils/mail.go
0 → 100644
| 1 | package utils | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bytes" | ||
| 5 | "encoding/base64" | ||
| 6 | "encoding/json" | ||
| 7 | "errors" | ||
| 8 | "fmt" | ||
| 9 | "io" | ||
| 10 | "mime" | ||
| 11 | "mime/multipart" | ||
| 12 | "net/mail" | ||
| 13 | "net/smtp" | ||
| 14 | "net/textproto" | ||
| 15 | "os" | ||
| 16 | "path" | ||
| 17 | "path/filepath" | ||
| 18 | "strconv" | ||
| 19 | "strings" | ||
| 20 | ) | ||
| 21 | |||
| 22 | const ( | ||
| 23 | maxLineLength = 76 | ||
| 24 | ) | ||
| 25 | |||
| 26 | // Email is the type used for email messages | ||
| 27 | type Email struct { | ||
| 28 | Auth smtp.Auth | ||
| 29 | Identity string `json:"identity"` | ||
| 30 | Username string `json:"username"` | ||
| 31 | Password string `json:"password"` | ||
| 32 | Host string `json:"host"` | ||
| 33 | Port int `json:"port"` | ||
| 34 | From string `json:"from"` | ||
| 35 | To []string | ||
| 36 | Bcc []string | ||
| 37 | Cc []string | ||
| 38 | Subject string | ||
| 39 | Text string // Plaintext message (optional) | ||
| 40 | HTML string // Html message (optional) | ||
| 41 | Headers textproto.MIMEHeader | ||
| 42 | Attachments []*Attachment | ||
| 43 | ReadReceipt []string | ||
| 44 | } | ||
| 45 | |||
| 46 | // Attachment is a struct representing an email attachment. | ||
| 47 | // Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question | ||
| 48 | type Attachment struct { | ||
| 49 | Filename string | ||
| 50 | Header textproto.MIMEHeader | ||
| 51 | Content []byte | ||
| 52 | } | ||
| 53 | |||
| 54 | func NewEMail(config string) *Email { | ||
| 55 | e := new(Email) | ||
| 56 | e.Headers = textproto.MIMEHeader{} | ||
| 57 | err := json.Unmarshal([]byte(config), e) | ||
| 58 | if err != nil { | ||
| 59 | return nil | ||
| 60 | } | ||
| 61 | if e.From == "" { | ||
| 62 | e.From = e.Username | ||
| 63 | } | ||
| 64 | return e | ||
| 65 | } | ||
| 66 | |||
| 67 | // make all send information to byte | ||
| 68 | func (e *Email) Bytes() ([]byte, error) { | ||
| 69 | buff := &bytes.Buffer{} | ||
| 70 | w := multipart.NewWriter(buff) | ||
| 71 | // Set the appropriate headers (overwriting any conflicts) | ||
| 72 | // Leave out Bcc (only included in envelope headers) | ||
| 73 | e.Headers.Set("To", strings.Join(e.To, ",")) | ||
| 74 | if e.Cc != nil { | ||
| 75 | e.Headers.Set("Cc", strings.Join(e.Cc, ",")) | ||
| 76 | } | ||
| 77 | e.Headers.Set("From", e.From) | ||
| 78 | e.Headers.Set("Subject", e.Subject) | ||
| 79 | if len(e.ReadReceipt) != 0 { | ||
| 80 | e.Headers.Set("Disposition-Notification-To", strings.Join(e.ReadReceipt, ",")) | ||
| 81 | } | ||
| 82 | e.Headers.Set("MIME-Version", "1.0") | ||
| 83 | e.Headers.Set("Content-Type", fmt.Sprintf("multipart/mixed;\r\n boundary=%s\r\n", w.Boundary())) | ||
| 84 | |||
| 85 | // Write the envelope headers (including any custom headers) | ||
| 86 | if err := headerToBytes(buff, e.Headers); err != nil { | ||
| 87 | return nil, fmt.Errorf("Failed to render message headers: %s", err) | ||
| 88 | } | ||
| 89 | // Start the multipart/mixed part | ||
| 90 | fmt.Fprintf(buff, "--%s\r\n", w.Boundary()) | ||
| 91 | header := textproto.MIMEHeader{} | ||
| 92 | // Check to see if there is a Text or HTML field | ||
| 93 | if e.Text != "" || e.HTML != "" { | ||
| 94 | subWriter := multipart.NewWriter(buff) | ||
| 95 | // Create the multipart alternative part | ||
| 96 | header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary())) | ||
| 97 | // Write the header | ||
| 98 | if err := headerToBytes(buff, header); err != nil { | ||
| 99 | return nil, fmt.Errorf("Failed to render multipart message headers: %s", err) | ||
| 100 | } | ||
| 101 | // Create the body sections | ||
| 102 | if e.Text != "" { | ||
| 103 | header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8")) | ||
| 104 | header.Set("Content-Transfer-Encoding", "quoted-printable") | ||
| 105 | if _, err := subWriter.CreatePart(header); err != nil { | ||
| 106 | return nil, err | ||
| 107 | } | ||
| 108 | // Write the text | ||
| 109 | if err := quotePrintEncode(buff, e.Text); err != nil { | ||
| 110 | return nil, err | ||
| 111 | } | ||
| 112 | } | ||
| 113 | if e.HTML != "" { | ||
| 114 | header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8")) | ||
| 115 | header.Set("Content-Transfer-Encoding", "quoted-printable") | ||
| 116 | if _, err := subWriter.CreatePart(header); err != nil { | ||
| 117 | return nil, err | ||
| 118 | } | ||
| 119 | // Write the text | ||
| 120 | if err := quotePrintEncode(buff, e.HTML); err != nil { | ||
| 121 | return nil, err | ||
| 122 | } | ||
| 123 | } | ||
| 124 | if err := subWriter.Close(); err != nil { | ||
| 125 | return nil, err | ||
| 126 | } | ||
| 127 | } | ||
| 128 | // Create attachment part, if necessary | ||
| 129 | for _, a := range e.Attachments { | ||
| 130 | ap, err := w.CreatePart(a.Header) | ||
| 131 | if err != nil { | ||
| 132 | return nil, err | ||
| 133 | } | ||
| 134 | // Write the base64Wrapped content to the part | ||
| 135 | base64Wrap(ap, a.Content) | ||
| 136 | } | ||
| 137 | if err := w.Close(); err != nil { | ||
| 138 | return nil, err | ||
| 139 | } | ||
| 140 | return buff.Bytes(), nil | ||
| 141 | } | ||
| 142 | |||
| 143 | // Attach file to the send mail | ||
| 144 | func (e *Email) AttachFile(filename string) (a *Attachment, err error) { | ||
| 145 | f, err := os.Open(filename) | ||
| 146 | if err != nil { | ||
| 147 | return | ||
| 148 | } | ||
| 149 | ct := mime.TypeByExtension(filepath.Ext(filename)) | ||
| 150 | basename := path.Base(filename) | ||
| 151 | return e.Attach(f, basename, ct) | ||
| 152 | } | ||
| 153 | |||
| 154 | // Attach is used to attach content from an io.Reader to the email. | ||
| 155 | // Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type | ||
| 156 | func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) { | ||
| 157 | var buffer bytes.Buffer | ||
| 158 | if _, err = io.Copy(&buffer, r); err != nil { | ||
| 159 | return | ||
| 160 | } | ||
| 161 | at := &Attachment{ | ||
| 162 | Filename: filename, | ||
| 163 | Header: textproto.MIMEHeader{}, | ||
| 164 | Content: buffer.Bytes(), | ||
| 165 | } | ||
| 166 | // Get the Content-Type to be used in the MIMEHeader | ||
| 167 | if c != "" { | ||
| 168 | at.Header.Set("Content-Type", c) | ||
| 169 | } else { | ||
| 170 | // If the Content-Type is blank, set the Content-Type to "application/octet-stream" | ||
| 171 | at.Header.Set("Content-Type", "application/octet-stream") | ||
| 172 | } | ||
| 173 | at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename)) | ||
| 174 | at.Header.Set("Content-Transfer-Encoding", "base64") | ||
| 175 | e.Attachments = append(e.Attachments, at) | ||
| 176 | return at, nil | ||
| 177 | } | ||
| 178 | |||
| 179 | func (e *Email) Send() error { | ||
| 180 | if e.Auth == nil { | ||
| 181 | e.Auth = smtp.PlainAuth(e.Identity, e.Username, e.Password, e.Host) | ||
| 182 | } | ||
| 183 | // Merge the To, Cc, and Bcc fields | ||
| 184 | to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc)) | ||
| 185 | to = append(append(append(to, e.To...), e.Cc...), e.Bcc...) | ||
| 186 | // Check to make sure there is at least one recipient and one "From" address | ||
| 187 | if e.From == "" || len(to) == 0 { | ||
| 188 | return errors.New("Must specify at least one From address and one To address") | ||
| 189 | } | ||
| 190 | from, err := mail.ParseAddress(e.From) | ||
| 191 | if err != nil { | ||
| 192 | return err | ||
| 193 | } | ||
| 194 | raw, err := e.Bytes() | ||
| 195 | if err != nil { | ||
| 196 | return err | ||
| 197 | } | ||
| 198 | return smtp.SendMail(e.Host+":"+strconv.Itoa(e.Port), e.Auth, from.Address, to, raw) | ||
| 199 | } | ||
| 200 | |||
| 201 | // quotePrintEncode writes the quoted-printable text to the IO Writer (according to RFC 2045) | ||
| 202 | func quotePrintEncode(w io.Writer, s string) error { | ||
| 203 | var buf [3]byte | ||
| 204 | mc := 0 | ||
| 205 | for i := 0; i < len(s); i++ { | ||
| 206 | c := s[i] | ||
| 207 | // We're assuming Unix style text formats as input (LF line break), and | ||
| 208 | // quoted-printble uses CRLF line breaks. (Literal CRs will become | ||
| 209 | // "=0D", but probably shouldn't be there to begin with!) | ||
| 210 | if c == '\n' { | ||
| 211 | io.WriteString(w, "\r\n") | ||
| 212 | mc = 0 | ||
| 213 | continue | ||
| 214 | } | ||
| 215 | |||
| 216 | var nextOut []byte | ||
| 217 | if isPrintable(c) { | ||
| 218 | nextOut = append(buf[:0], c) | ||
| 219 | } else { | ||
| 220 | nextOut = buf[:] | ||
| 221 | qpEscape(nextOut, c) | ||
| 222 | } | ||
| 223 | |||
| 224 | // Add a soft line break if the next (encoded) byte would push this line | ||
| 225 | // to or past the limit. | ||
| 226 | if mc+len(nextOut) >= maxLineLength { | ||
| 227 | if _, err := io.WriteString(w, "=\r\n"); err != nil { | ||
| 228 | return err | ||
| 229 | } | ||
| 230 | mc = 0 | ||
| 231 | } | ||
| 232 | |||
| 233 | if _, err := w.Write(nextOut); err != nil { | ||
| 234 | return err | ||
| 235 | } | ||
| 236 | mc += len(nextOut) | ||
| 237 | } | ||
| 238 | // No trailing end-of-line?? Soft line break, then. TODO: is this sane? | ||
| 239 | if mc > 0 { | ||
| 240 | io.WriteString(w, "=\r\n") | ||
| 241 | } | ||
| 242 | return nil | ||
| 243 | } | ||
| 244 | |||
| 245 | // isPrintable returns true if the rune given is "printable" according to RFC 2045, false otherwise | ||
| 246 | func isPrintable(c byte) bool { | ||
| 247 | return (c >= '!' && c <= '<') || (c >= '>' && c <= '~') || (c == ' ' || c == '\n' || c == '\t') | ||
| 248 | } | ||
| 249 | |||
| 250 | // qpEscape is a helper function for quotePrintEncode which escapes a | ||
| 251 | // non-printable byte. Expects len(dest) == 3. | ||
| 252 | func qpEscape(dest []byte, c byte) { | ||
| 253 | const nums = "0123456789ABCDEF" | ||
| 254 | dest[0] = '=' | ||
| 255 | dest[1] = nums[(c&0xf0)>>4] | ||
| 256 | dest[2] = nums[(c & 0xf)] | ||
| 257 | } | ||
| 258 | |||
| 259 | // headerToBytes enumerates the key and values in the header, and writes the results to the IO Writer | ||
| 260 | func headerToBytes(w io.Writer, t textproto.MIMEHeader) error { | ||
| 261 | for k, v := range t { | ||
| 262 | // Write the header key | ||
| 263 | _, err := fmt.Fprintf(w, "%s:", k) | ||
| 264 | if err != nil { | ||
| 265 | return err | ||
| 266 | } | ||
| 267 | // Write each value in the header | ||
| 268 | for _, c := range v { | ||
| 269 | _, err := fmt.Fprintf(w, " %s\r\n", c) | ||
| 270 | if err != nil { | ||
| 271 | return err | ||
| 272 | } | ||
| 273 | } | ||
| 274 | } | ||
| 275 | return nil | ||
| 276 | } | ||
| 277 | |||
| 278 | // base64Wrap encodeds the attachment content, and wraps it according to RFC 2045 standards (every 76 chars) | ||
| 279 | // The output is then written to the specified io.Writer | ||
| 280 | func base64Wrap(w io.Writer, b []byte) { | ||
| 281 | // 57 raw bytes per 76-byte base64 line. | ||
| 282 | const maxRaw = 57 | ||
| 283 | // Buffer for each line, including trailing CRLF. | ||
| 284 | var buffer [maxLineLength + len("\r\n")]byte | ||
| 285 | copy(buffer[maxLineLength:], "\r\n") | ||
| 286 | // Process raw chunks until there's no longer enough to fill a line. | ||
| 287 | for len(b) >= maxRaw { | ||
| 288 | base64.StdEncoding.Encode(buffer[:], b[:maxRaw]) | ||
| 289 | w.Write(buffer[:]) | ||
| 290 | b = b[maxRaw:] | ||
| 291 | } | ||
| 292 | // Handle the last chunk of bytes. | ||
| 293 | if len(b) > 0 { | ||
| 294 | out := buffer[:base64.StdEncoding.EncodedLen(len(b))] | ||
| 295 | base64.StdEncoding.Encode(out, b) | ||
| 296 | out = append(out, "\r\n"...) | ||
| 297 | w.Write(out) | ||
| 298 | } | ||
| 299 | } |
utils/mail_test.go
0 → 100644
| 1 | package utils | ||
| 2 | |||
| 3 | import "testing" | ||
| 4 | |||
| 5 | func TestMail(t *testing.T) { | ||
| 6 | config := `{"username":"astaxie@gmail.com","password":"astaxie","host":"smtp.gmail.com","port":587}` | ||
| 7 | mail := NewEMail(config) | ||
| 8 | if mail.Username != "astaxie@gmail.com" { | ||
| 9 | t.Fatal("email parse get username error") | ||
| 10 | } | ||
| 11 | if mail.Password != "astaxie" { | ||
| 12 | t.Fatal("email parse get password error") | ||
| 13 | } | ||
| 14 | if mail.Host != "smtp.gmail.com" { | ||
| 15 | t.Fatal("email parse get host error") | ||
| 16 | } | ||
| 17 | if mail.Port != 587 { | ||
| 18 | t.Fatal("email parse get port error") | ||
| 19 | } | ||
| 20 | mail.To = []string{"xiemengjun@gmail.com"} | ||
| 21 | mail.From = "astaxie@gmail.com" | ||
| 22 | mail.Subject = "hi, just from beego!" | ||
| 23 | mail.Send() | ||
| 24 | } |
-
Please register or sign in to post a comment