d06c0427 by astaxie

support send mail

1 parent aa2fef0d
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 }
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 }
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!