-
Notifications
You must be signed in to change notification settings - Fork 8
/
transport_smtp.go
193 lines (167 loc) · 4.48 KB
/
transport_smtp.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
package passwordless
import (
"bytes"
"crypto/md5"
"crypto/tls"
"fmt"
"io"
"net"
"net/smtp"
"time"
"context"
)
// ComposerFunc is called when writing the contents of an email, including
// preamble headers.
type ComposerFunc func(ctx context.Context, token, user, recipient string, w io.Writer) error
// SMTPTransport delivers a user token via e-mail.
type SMTPTransport struct {
UseSSL bool
auth smtp.Auth
from string
addr string
composer ComposerFunc
}
// NewSMTPTransport returns a new transport capable of sending emails via
// SMTP. `addr` should be in the form "host:port" of the email server.
func NewSMTPTransport(addr, from string, auth smtp.Auth, c ComposerFunc) *SMTPTransport {
return &SMTPTransport{
UseSSL: false,
addr: addr,
auth: auth,
from: from,
composer: c,
}
}
// Send sends an email to the email address specified in `recipient`,
// containing the user token provided.
func (t *SMTPTransport) Send(ctx context.Context, token, uid, recipient string) error {
host, _, _ := net.SplitHostPort(t.addr)
// If UseSSL is true, need to ensure the connection is made over a
// TLS channel.
var c *smtp.Client
if t.UseSSL {
// Connect with SSL handshake
tlscfg := &tls.Config{
ServerName: host,
}
if conn, err := tls.Dial("tcp", t.addr, tlscfg); err != nil {
return err
} else if c, err = smtp.NewClient(conn, host); err != nil {
defer c.Close()
defer conn.Close()
return err
}
} else {
// Not using SSL handshake
if cl, err := smtp.Dial(t.addr); err != nil {
return err
} else {
c = cl
defer c.Close()
}
}
// Use STARTTLS if available
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: host}
if err := c.StartTLS(config); err != nil {
return err
}
}
// Use auth credentials if supported and provided
if ok, _ := c.Extension("AUTH"); ok && t.auth != nil {
if err := c.Auth(t.auth); err != nil {
return err
}
}
// Compose email
if err := c.Mail(t.from); err != nil {
return err
}
if err := c.Rcpt(recipient); err != nil {
return err
}
// Write body
w, err := c.Data()
if err != nil {
return err
}
// Emit message body
if err := t.composer(ctx, token, uid, recipient, w); err != nil {
return err
}
// Close writer
if err := w.Close(); err != nil {
return err
}
// Succeeded; quit nicely
return c.Quit()
}
// Email is a helper for creating multipart (text and html) emails
type Email struct {
Body []struct{ t, c string }
To string
Subject string
Date time.Time
}
// AddBody adds a content section to the email. The `contentType` should
// be a known type, such as "text/html" or "text/plain". If no `contentType`
// is provided, "text/plain" is used. Call this method for each required
// body, with the most preferable type last.
func (e *Email) AddBody(contentType, body string) {
if e.Body == nil {
e.Body = make([]struct{ t, c string }, 0)
}
if contentType == "" {
contentType = "text/plain"
}
e.Body = append(e.Body, struct{ t, c string }{contentType, body})
}
// Write emits the Email to the specified writer.
func (e Email) Write(w io.Writer) (int64, error) {
return e.Buffer().WriteTo(w)
}
// Bytes returns the contents of the email as a series of bytes.
func (e Email) Bytes() []byte {
return e.Buffer().Bytes()
}
// Buffer generates the email header and contents as a `Buffer`.
func (e Email) Buffer() *bytes.Buffer {
crlf := "\r\n"
b := bytes.NewBuffer(nil)
if e.Date.IsZero() {
b.WriteString("Date: " + time.Now().UTC().Format(time.RFC822) + crlf)
} else {
b.WriteString("Date: " + e.Date.UTC().Format(time.RFC822) + crlf)
}
if e.Subject != "" {
b.WriteString("Subject: " + e.Subject + crlf)
}
if e.To != "" {
b.WriteString("To: " + e.To + crlf)
}
boundary := ""
// Write multipart header if email contains multiple parts
if len(e.Body) > 1 {
// Generate unique boundary to separate sections
h := md5.New()
io.WriteString(h, fmt.Sprintf("%d", time.Now().UnixNano()))
boundary = fmt.Sprintf("%x", h.Sum(nil))
// Write boundary
b.WriteString("MIME-Version: 1.0" + crlf)
b.WriteString("Content-Type: multipart/alternative; boundary=" +
boundary + crlf + crlf)
}
// Write each part
for _, body := range e.Body {
if boundary != "" {
b.WriteString(crlf + "--" + boundary + crlf)
}
b.WriteString("Content-Type: " + body.t + "; charset=\"UTF-8\";")
b.WriteString(crlf + crlf + body.c)
}
if boundary != "" {
b.WriteString(crlf + "--" + boundary + "--")
}
b.WriteString(crlf)
return b
}