/
message.go
285 lines (269 loc) · 8.11 KB
/
message.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package gmail
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"mime/quotedprintable"
"net/http"
"net/mail"
"net/textproto"
"path/filepath"
"sort"
"strings"
"google.golang.org/api/gmail/v1"
)
// Predefined error returned when trying to send a message.
var (
// is returned if you try to send a blank message with no attached files and
// no text messages
ErrNoBody = errors.New("contents are undefined")
// error not initialized the GMail
ErrServiceNotInitialized = errors.New("gmail service not initialized")
)
// The Message describes an email message.
type Message struct {
header textproto.MIMEHeader // headers
parts map[string]*part // the list of file by names
}
// NewMessage creates a new email message to send.
//
// The message is always sent on behalf of an authorized user, so that the from
// field can be empty or contain the string "me". If you set this field to a
// different address, it will appear in the Reply-To and when you reply to this
// message will use the sender's address.
//
// You can specify email address in the following formats (supported by parsing
// the name and email address):
//
// test@example.com
// <test@example.com>
// TestUser <test@example.com>
//
// Parsing checks the validity of the format of the email address. You must
// specify at least one address to send (to or cc), or when trying to send
// messages will return an error.
//
// You can use text or HTML message format is determined automatically. To
// guarantee that the format will be specified as HTML, consider wrapping the
// text with <html> tag. When adding the HTML content of the message, text
// version, to support legacy mail program will be added automatically. When
// you try to add as text message binary data will return an error. You can
// set nil as a parameter to empty message body.
func NewMessage(subject, from string, to, cc []string, body []byte) (*Message, error) {
var h = make(textproto.MIMEHeader)
if from != "" && from != "me" {
if mfrom, err := mail.ParseAddress(from); err == nil {
from := mfrom.String()
h.Set("From", from)
h.Set("Reply-To", from)
} else if err.Error() != "mail: no address" {
return nil, fmt.Errorf("from %v", err)
}
}
if len(to) > 0 {
if addr, err := addrsList(to); err == nil {
h.Set("To", addr)
} else if err.Error() != "mail: no address" {
return nil, fmt.Errorf("to %v", err)
}
}
if len(cc) > 0 {
if addr, err := addrsList(cc); err == nil {
h.Set("Сс", addr)
} else if err.Error() != "mail: no address" {
return nil, fmt.Errorf("cc %v", err)
}
}
if h.Get("To") == "" && h.Get("Cc") == "" {
return nil, errors.New("no recipient specified")
}
if subject != "" {
h.Set("Subject", mime.QEncoding.Encode("utf-8", subject))
}
var msg = &Message{header: h}
if len(body) > 0 {
if err := msg.SetBody(body); err != nil {
return msg, err
}
}
return msg, nil
}
const _body = "\000body" // the file name with the contents of the message
// Attach attaches to the message an attachment as a file. Passing an empty
// content deletes the file with the same name if it was previously added.
func (m *Message) Attach(name string, data []byte) error {
if len(data) == 0 {
if m.parts != nil {
delete(m.parts, name)
}
return nil
}
name = filepath.Base(name)
switch name {
case ".", "..", string(filepath.Separator):
return fmt.Errorf("bad file name: %v", name)
}
var h = make(textproto.MIMEHeader)
var contentType = mime.TypeByExtension(filepath.Ext(name))
if contentType == "" {
contentType = http.DetectContentType(data)
}
if contentType != "" {
h.Set("Content-Type", contentType)
}
var coding = "quoted-printable"
if !strings.HasPrefix(contentType, "text") {
if name == _body {
return fmt.Errorf("unsupported body content type: %v", contentType)
}
coding = "base64"
}
h.Set("Content-Transfer-Encoding", coding)
if name != _body {
disposition := fmt.Sprintf("attachment; filename=%s", name)
h.Set("Content-Disposition", disposition)
}
if m.parts == nil {
m.parts = make(map[string]*part)
}
m.parts[name] = &part{
header: h,
data: data,
}
return nil
}
// AddFile reads the contents of specified in the parameter file and attaches
// it as an attachment to the message.
func (m *Message) AddFile(filename string) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
return m.Attach(filename, data)
}
// SetBody sets the contents of the text of the letter.
//
// You can use text or HTML message format (is determined automatically). To
// guarantee that the format will be specified as HTML, consider wrapping the
// text with <html> tag. When adding the HTML content, text version, to support
// legacy mail program will be added automatically. When you try to add as
// message binary data will return an error. You can pass as a parameter the nil,
// then the message will be without a text submission.
func (m *Message) SetBody(data []byte) error {
return m.Attach(_body, data)
}
// Has returns true if a file with that name was in the message as an attachment.
func (m *Message) Has(name string) bool {
_, ok := m.parts[name]
return ok
}
// writeTo generates and writes the text representation of mail messages.
func (m *Message) writeTo(w io.Writer) error {
if len(m.parts) == 0 {
return ErrNoBody
}
var h = make(textproto.MIMEHeader)
h.Set("MIME-Version", "1.0")
h.Set("X-Mailer", "REST GMailer (github.com/mdigger/gmail)")
// copy the primary header of the message
for k, v := range m.header {
h[k] = v
}
// check that only defined the basic message, no file
if len(m.parts) == 1 && m.Has(_body) {
body := m.parts[_body]
for k, v := range body.header {
h[k] = v
}
writeHeader(w, h)
if err := body.writeData(w); err != nil {
return err
}
return nil
}
// there are attached files
var mw = multipart.NewWriter(w)
defer mw.Close()
h.Set("Content-Type",
fmt.Sprintf("multipart/mixed; boundary=%s", mw.Boundary()))
writeHeader(w, h)
for _, p := range m.parts {
pw, err := mw.CreatePart(p.header)
if err != nil {
return err
}
if err = p.writeData(pw); err != nil {
return err
}
}
return nil
}
// Send sends the message through GMail.
//
// Before sending, you must initialize the service by calling the Init function.
func (m *Message) Send() error {
if gmailService == nil || gmailService.Users == nil {
return ErrServiceNotInitialized
}
var buf bytes.Buffer
m.writeTo(&buf)
body := base64.RawURLEncoding.EncodeToString(buf.Bytes())
var gmailMessage = &gmail.Message{Raw: body}
_, err := gmailService.Users.Messages.Send("me", gmailMessage).Do()
return err
}
// part describes part email message: the file or message.
type part struct {
header textproto.MIMEHeader // headers
data []byte // content
}
// writeData writes the contents of the message file with maintain the coding
// system. At the moment only implemented quoted-printable and base64 encoding.
// For all others, an error is returned.
func (p *part) writeData(w io.Writer) (err error) {
switch name := p.header.Get("Content-Transfer-Encoding"); name {
case "quoted-printable":
enc := quotedprintable.NewWriter(w)
_, err = enc.Write(p.data)
enc.Close()
case "base64":
enc := base64.NewEncoder(base64.StdEncoding, w)
_, err = enc.Write(p.data)
enc.Close()
default:
err = fmt.Errorf("unsupported transform encoding: %v", name)
}
return err
}
// writeHeader writes the header of the message or file. The keys of the header
// are sorted alphabetically.
func writeHeader(w io.Writer, h textproto.MIMEHeader) {
var keys = make([]string, 0, len(h))
for k := range h {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
for _, v := range h[k] {
fmt.Fprintf(w, "%s: %s\r\n", k, v)
}
}
fmt.Fprintf(w, "\r\n") // add the offset from the header
}
// addrsList returns a string with the addresses generated from the address list.
func addrsList(addrs []string) (string, error) {
mails, err := mail.ParseAddressList(strings.Join(addrs, ", "))
if err != nil {
return "", err
}
var list = make([]string, len(mails))
for i, addr := range mails {
list[i] = addr.String()
}
return strings.Join(list, ", "), nil
}