Source file
src/tawesoft.co.uk/go/email/message.go
Documentation:
src/tawesoft.co.uk/go/email/message.go
1 package email
2
3 import (
4 "bufio"
5 "crypto/rand"
6 "fmt"
7 "io"
8 "mime/quotedprintable"
9 "net/mail"
10 "net/textproto"
11 "strings"
12 "time"
13 )
14
15
16
17
18
19 type Message struct {
20 ID string
21 From mail.Address
22 To []mail.Address
23 Cc []mail.Address
24 Bcc []mail.Address
25 Subject string
26
27
28
29
30
31
32 Headers mail.Header
33
34
35
36 Html string
37
38
39
40 Text string
41
42
43 Attachments []*Attachment
44 }
45
46
47 type Envelope struct {
48
49
50
51
52
53
54
55 ReturnPath string
56
57
58
59 ReceiptTo []string
60 }
61
62
63 type bound struct {
64 label string
65 }
66
67 func (b bound) start(w io.Writer, contentType string) {
68 fmt.Fprintf(w, "Content-Type: %s; boundary=\"%s\"\r\n", contentType, b.label)
69 fmt.Fprintf(w, "\r\n")
70 fmt.Fprintf(w, "--%s\r\n", b.label)
71 }
72
73 func (b bound) next(w io.Writer) {
74 fmt.Fprintf(w, "--%s\r\n", b.label)
75 }
76
77 func (b bound) end(w io.Writer) {
78 fmt.Fprintf(w, "--%s--\r\n", b.label)
79 }
80
81
82
83 func boundary(prefix string) (bound, error) {
84
85
86 const maxlen int = 70
87
88
89
90 const bchars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=?"
91
92
93
94 if (len(prefix) > 16) { return bound{}, fmt.Errorf("boundary prefix too long") }
95
96 var randlen = maxlen - len(prefix)
97 xs := make([]byte, randlen)
98
99 _, err := rand.Read(xs)
100 if err != nil { return bound{}, fmt.Errorf("boundary random source read error: %v", err) }
101
102
103 for i, x := range xs {
104 xs[i] = bchars[int(x) % len(bchars)]
105 }
106
107 return bound{prefix + string(xs)}, nil
108 }
109
110
111 func (e *Message) Write(dest io.Writer) error {
112 return e.write(dest, false)
113 }
114
115
116
117
118 func (e *Message) WriteCompat(dest io.Writer) error {
119 return e.write(dest, true)
120 }
121
122 func (e *Message) write(dest io.Writer, compat bool) (ret error) {
123 defer func() {
124 if r := recover(); r != nil {
125 if err, ok := r.(error); ok {
126 ret = err
127 } else {
128 ret = fmt.Errorf("panic: %v", r)
129 }
130 }
131 }()
132
133
134 var fws func(value string, keylen int) string
135 if compat {
136 fws = func(value string, keylen int) string {
137 result, err := fwsNone(value, keylen, 998)
138 if err != nil { panic(err) }
139 return result
140 }
141 } else {
142 fws = func(value string, keylen int) string {
143 result, err := fwsWrap(value, keylen, 998)
144 if err != nil { panic(err) }
145 return result
146 }
147 }
148
149
150 const RFC5332C = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
151
152 var qp *quotedprintable.Writer
153 var bufferedDest = bufio.NewWriter(dest)
154 var dw = textproto.NewWriter(bufferedDest).DotWriter()
155 defer dw.Close()
156
157
158 var addresses = func (xs []mail.Address) string {
159 var s = make([]string, 0, len(xs))
160
161 for _, x := range xs {
162 s = append(s, x.String())
163 }
164
165 return strings.Join(s, ",")
166 }
167
168 var coreHeaders = []struct{left string; right string} {
169 {"From", fws(e.From.String(), len("From"))},
170 {"To", fws(addresses(e.To), len("To"))},
171 {"Cc", fws(addresses(e.Cc), len("Cc"))},
172 {"Bcc", fws(addresses(e.Bcc), len("Bcc"))},
173 {"Date", time.Now().Format(RFC5332C)},
174 {"Subject", fws(optionalQEncode(e.Subject), len("Subject"))},
175 {"MIME-Version", "1.0"},
176 }
177
178 if e.ID != "" {
179 coreHeaders = append(coreHeaders,
180 struct{left string; right string}{"Message-ID", "<"+e.ID+">"})
181 }
182
183 for _, v := range coreHeaders {
184 fmt.Fprintf(dw, "%s: %s\r\n", textproto.CanonicalMIMEHeaderKey(v.left), v.right)
185 }
186
187 for k, vs := range e.Headers {
188 for _, v := range vs {
189 ck := textproto.CanonicalMIMEHeaderKey(k)
190 fwsValue, err := fwsNone(v, len(ck), 998)
191 if err != nil { return err }
192 fmt.Fprintf(dw, "%s: %s\r\n", ck, fwsValue)
193 }
194 }
195
196 bndMxd, err := boundary("MXD-")
197 if err != nil { return err }
198 bndMxd.start(dw, "multipart/mixed")
199
200 bndRel, err := boundary("REL-")
201 if err != nil { return err }
202 bndRel.start(dw, "multipart/related")
203
204 bndAlt, err := boundary("ALT-")
205 if err != nil { return err }
206 bndAlt.start(dw, "multipart/alternative")
207
208 fmt.Fprintf(dw, "Content-Type: text/plain; charset=utf-8\r\n")
209 fmt.Fprintf(dw, "Content-Transfer-Encoding: quoted-printable\r\n")
210 fmt.Fprintf(dw, "\r\n")
211 qp = quotedprintable.NewWriter(dw)
212 io.WriteString(qp, strings.TrimSpace(e.Text))
213 qp.Close()
214 fmt.Fprintf(dw, "\r\n")
215
216 bndAlt.next(dw)
217
218 fmt.Fprintf(dw, "Content-Type: text/html; charset=utf-8\r\n")
219 fmt.Fprintf(dw, "Content-Transfer-Encoding: quoted-printable\r\n")
220 fmt.Fprintf(dw, "\r\n")
221 qp = quotedprintable.NewWriter(dw)
222 io.WriteString(qp, e.Html)
223 qp.Close()
224 fmt.Fprintf(dw, "\r\n")
225
226 bndAlt.end(dw)
227 bndRel.end(dw)
228
229 for _, attachment := range e.Attachments {
230 bndMxd.next(dw)
231 err = attachment.write(dw)
232 if err != nil {
233 return fmt.Errorf("error writing attachment %q: %v", attachment.Filename, err)
234 }
235 }
236
237 bndMxd.end(dw)
238
239 return nil
240 }
241
View as plain text