Source file
src/tawesoft.co.uk/go/legacy/email/message.go
Documentation:
src/tawesoft.co.uk/go/legacy/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 From mail.Address
21 To []mail.Address
22 Cc []mail.Address
23 Bcc []mail.Address
24 Subject string
25 Headers mail.Header
26 Html string
27 Text string
28 Attachments []*Attachment
29 }
30
31
32 type Envelope struct {
33
34
35
36 From string
37
38
39 Data *Message
40
41
42 ReceiptTo []string
43 }
44
45
46 type bound struct {
47 label string
48 }
49
50 func (b bound) start(w io.Writer, contentType string) {
51 fmt.Fprintf(w, "Content-Type: %s; boundary=\"%s\"\r\n", contentType, b.label)
52 fmt.Fprintf(w, "\r\n")
53 fmt.Fprintf(w, "--%s\r\n", b.label)
54 }
55
56 func (b bound) next(w io.Writer) {
57 fmt.Fprintf(w, "--%s\r\n", b.label)
58 }
59
60 func (b bound) end(w io.Writer) {
61 fmt.Fprintf(w, "--%s--\r\n", b.label)
62 }
63
64
65
66 func boundary(prefix string) (bound, error) {
67
68
69 const maxlen int = 70
70
71
72
73 const bchars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=?"
74
75
76
77 if (len(prefix) > 16) { return bound{}, fmt.Errorf("boundary prefix too long") }
78
79 var randlen = maxlen - len(prefix)
80 xs := make([]byte, randlen)
81
82 _, err := rand.Read(xs)
83 if err != nil { return bound{}, fmt.Errorf("boundary random source read error: %v", err) }
84
85
86 for i, x := range xs {
87 xs[i] = bchars[int(x) % len(bchars)]
88 }
89
90 return bound{prefix + string(xs)}, nil
91 }
92
93
94
95
96
97 func (e *Message) Print(dest io.Writer) error {
98
99
100 const RFC5332C = "Mon, 02 Jan 2006 15:04:05 -0700 (MST)"
101
102 var err error
103 var qp *quotedprintable.Writer
104 var bufferedDest = bufio.NewWriter(dest)
105 var dw = textproto.NewWriter(bufferedDest).DotWriter()
106 defer dw.Close()
107
108
109 var addresses = func (xs []mail.Address) string {
110 var s = make([]string, 0, len(xs))
111
112 for _, x := range xs {
113 s = append(s, x.String())
114 }
115
116 return strings.Join(s, ",")
117 }
118
119 var coreHeaders = []struct{left string; right string} {
120 {"From", e.From.String()},
121 {"To", addresses(e.To)},
122 {"Cc", addresses(e.Cc)},
123 {"Bcc", addresses(e.Bcc)},
124 {"Date", time.Now().Format(RFC5332C)},
125 {"Subject", optionalQEncode(e.Subject)},
126 {"MIME-Version", "1.0"},
127 {"Message-ID", msgid(e.From)},
128 }
129
130 for _, v := range coreHeaders {
131 fmt.Fprintf(dw, "%s: %s\r\n", textproto.CanonicalMIMEHeaderKey(v.left), v.right)
132 }
133
134 for k, vs := range e.Headers {
135 for _, v := range vs {
136 fmt.Fprintf(dw, "%s: %s\r\n", textproto.CanonicalMIMEHeaderKey(k), v)
137 }
138 }
139
140 bndMxd, err := boundary("MXD-")
141 if err != nil { return err }
142 bndMxd.start(dw, "multipart/mixed")
143
144 bndRel, err := boundary("REL-")
145 if err != nil { return err }
146 bndRel.start(dw, "multipart/related")
147
148 bndAlt, err := boundary("ALT-")
149 if err != nil { return err }
150 bndAlt.start(dw, "multipart/alternative")
151
152 fmt.Fprintf(dw, "Content-Type: text/plain; charset=utf-8\r\n")
153 fmt.Fprintf(dw, "Content-Transfer-Encoding: quoted-printable\r\n")
154 fmt.Fprintf(dw, "\r\n")
155 qp = quotedprintable.NewWriter(dw)
156 io.WriteString(qp, strings.TrimSpace(e.Text))
157 qp.Close()
158 fmt.Fprintf(dw, "\r\n")
159
160 bndAlt.next(dw)
161
162 fmt.Fprintf(dw, "Content-Type: text/html; charset=utf-8\r\n")
163 fmt.Fprintf(dw, "Content-Transfer-Encoding: quoted-printable\r\n")
164 fmt.Fprintf(dw, "\r\n")
165 qp = quotedprintable.NewWriter(dw)
166 io.WriteString(qp, e.Html)
167 qp.Close()
168 fmt.Fprintf(dw, "\r\n")
169
170 bndAlt.end(dw)
171 bndRel.end(dw)
172
173 for _, attachment := range e.Attachments {
174 bndMxd.next(dw)
175 err = attachment.write(dw)
176 if err != nil {
177 return fmt.Errorf("error writing attachment %s: %v", attachment.Filename, err)
178 }
179 }
180
181 bndMxd.end(dw)
182
183 return nil
184 }
185
View as plain text