message.go (view raw)
1package mail
2
3import (
4 "bytes"
5 "encoding/base64"
6 "io"
7 "mime/multipart"
8 "mime/quotedprintable"
9 "net/textproto"
10 "regexp"
11 "strconv"
12 "strings"
13 "time"
14
15 "github.com/joegrasse/mime/header"
16)
17
18type message struct {
19 headers textproto.MIMEHeader
20 body *bytes.Buffer
21 writers []*multipart.Writer
22 parts uint8
23 cids map[string]string
24 charset string
25 encoding encoding
26}
27
28func newMessage(email *Email) *message {
29 return &message{
30 headers: email.headers,
31 body: new(bytes.Buffer),
32 cids: make(map[string]string),
33 charset: email.Charset,
34 encoding: email.Encoding}
35}
36
37func encodeHeader(text string, charset string, usedChars int) string {
38 // create buffer
39 buf := new(bytes.Buffer)
40
41 // encode
42 encoder := header.NewEncoder(buf, charset, usedChars)
43 encoder.Encode([]byte(text))
44
45 return buf.String()
46
47 /*
48 switch encoding {
49 case EncodingBase64:
50 return mime.BEncoding.Encode(charset, text)
51 default:
52 return mime.QEncoding.Encode(charset, text)
53 }
54 */
55}
56
57// getHeaders returns the message headers
58func (msg *message) getHeaders() (headers string) {
59 // if the date header isn't set, set it
60 if date := msg.headers.Get("Date"); date == "" {
61 msg.headers.Set("Date", time.Now().Format(time.RFC1123Z))
62 }
63
64 // encode and combine the headers
65 for header, values := range msg.headers {
66 headers += header + ": " + encodeHeader(strings.Join(values, ", "), msg.charset, len(header)+2) + "\r\n"
67 }
68
69 headers = headers + "\r\n"
70
71 return
72}
73
74// getCID gets the generated CID for the provided text
75func (msg *message) getCID(text string) (cid string) {
76 // set the date format to use
77 const dateFormat = "20060102.150405"
78
79 // get the cid if we have one
80 cid, exists := msg.cids[text]
81 if !exists {
82 // generate a new cid
83 cid = time.Now().Format(dateFormat) + "." + strconv.Itoa(len(msg.cids)+1) + "@mail.0"
84 // save it
85 msg.cids[text] = cid
86 }
87
88 return
89}
90
91// replaceCIDs replaces the CIDs found in a text string
92// with generated ones
93func (msg *message) replaceCIDs(text string) string {
94 // regular expression to find cids
95 re := regexp.MustCompile(`(src|href)="cid:(.*?)"`)
96 // replace all of the found cids with generated ones
97 for _, matches := range re.FindAllStringSubmatch(text, -1) {
98 cid := msg.getCID(matches[2])
99 text = strings.Replace(text, "cid:"+matches[2], "cid:"+cid, -1)
100 }
101
102 return text
103}
104
105// openMultipart creates a new part of a multipart message
106func (msg *message) openMultipart(multipartType string) {
107 // create a new multipart writer
108 msg.writers = append(msg.writers, multipart.NewWriter(msg.body))
109 // create the boundary
110 contentType := "multipart/" + multipartType + ";\n \tboundary=" + msg.writers[msg.parts].Boundary()
111
112 // if no existing parts, add header to main header group
113 if msg.parts == 0 {
114 msg.headers.Set("Content-Type", contentType)
115 } else { // add header to multipart section
116 header := make(textproto.MIMEHeader)
117 header.Set("Content-Type", contentType)
118 msg.writers[msg.parts-1].CreatePart(header)
119 }
120
121 msg.parts++
122}
123
124// closeMultipart closes a part of a multipart message
125func (msg *message) closeMultipart() {
126 if msg.parts > 0 {
127 msg.writers[msg.parts-1].Close()
128 msg.parts--
129 }
130}
131
132// base64Encode base64 encodes the provided text with line wrapping
133func base64Encode(text []byte) []byte {
134 // create buffer
135 buf := new(bytes.Buffer)
136
137 // create base64 encoder that linewraps
138 encoder := base64.NewEncoder(base64.StdEncoding, &base64LineWrap{writer: buf})
139
140 // write the encoded text to buf
141 encoder.Write(text)
142 encoder.Close()
143
144 return buf.Bytes()
145}
146
147// qpEncode uses the quoted-printable encoding to encode the provided text
148func qpEncode(text []byte) []byte {
149 // create buffer
150 buf := new(bytes.Buffer)
151
152 encoder := quotedprintable.NewWriter(buf)
153
154 encoder.Write(text)
155 encoder.Close()
156
157 return buf.Bytes()
158}
159
160const maxLineChars = 76
161
162type base64LineWrap struct {
163 writer io.Writer
164 numLineChars int
165}
166
167func (e *base64LineWrap) Write(p []byte) (n int, err error) {
168 n = 0
169 // while we have more chars than are allowed
170 for len(p)+e.numLineChars > maxLineChars {
171 numCharsToWrite := maxLineChars - e.numLineChars
172 // write the chars we can
173 e.writer.Write(p[:numCharsToWrite])
174 // write a line break
175 e.writer.Write([]byte("\r\n"))
176 // reset the line count
177 e.numLineChars = 0
178 // remove the chars that have been written
179 p = p[numCharsToWrite:]
180 // set the num of chars written
181 n += numCharsToWrite
182 }
183
184 // write what is left
185 e.writer.Write(p)
186 e.numLineChars += len(p)
187 n += len(p)
188
189 return
190}
191
192func (msg *message) write(header textproto.MIMEHeader, body []byte, encoding encoding) {
193 msg.writeHeader(header)
194 msg.writeBody(body, encoding)
195}
196
197func (msg *message) writeHeader(headers textproto.MIMEHeader) {
198 // if there are no parts add header to main headers
199 if msg.parts == 0 {
200 for header, value := range headers {
201 msg.headers[header] = value
202 }
203 } else { // add header to multipart section
204 msg.writers[msg.parts-1].CreatePart(headers)
205 }
206}
207
208func (msg *message) writeBody(body []byte, encoding encoding) {
209 // encode and write the body
210 switch encoding {
211 case EncodingQuotedPrintable:
212 msg.body.Write(qpEncode(body))
213 case EncodingBase64:
214 msg.body.Write(base64Encode(body))
215 default:
216 msg.body.Write(body)
217 }
218}
219
220func (msg *message) addBody(contentType string, body []byte) {
221 body = []byte(msg.replaceCIDs(string(body)))
222
223 header := make(textproto.MIMEHeader)
224 header.Set("Content-Type", contentType+"; charset="+msg.charset)
225 header.Set("Content-Transfer-Encoding", msg.encoding.String())
226 msg.write(header, body, msg.encoding)
227}
228
229var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
230
231func escapeQuotes(s string) string {
232 return quoteEscaper.Replace(s)
233}
234
235func (msg *message) addFiles(files []*file, inline bool) {
236 encoding := EncodingBase64
237 for _, file := range files {
238 header := make(textproto.MIMEHeader)
239 header.Set("Content-Type", file.mimeType+";\n \tname=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 6)+`"`)
240 header.Set("Content-Transfer-Encoding", encoding.String())
241 if inline {
242 header.Set("Content-Disposition", "inline;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`)
243 header.Set("Content-ID", "<"+msg.getCID(file.filename)+">")
244 } else {
245 header.Set("Content-Disposition", "attachment;\n \tfilename=\""+encodeHeader(escapeQuotes(file.filename), msg.charset, 10)+`"`)
246 }
247
248 msg.write(header, file.data, encoding)
249 }
250}