Skip to content

Commit df74d8d

Browse files
Evan Shawrsc
Evan Shaw
authored andcommitted
smtp: new package
R=rsc, iant, agl CC=golang-dev https://golang.org/cl/2052042
1 parent 52e3c99 commit df74d8d

File tree

5 files changed

+559
-0
lines changed

5 files changed

+559
-0
lines changed

src/pkg/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ DIRS=\
109109
runtime\
110110
runtime/pprof\
111111
scanner\
112+
smtp\
112113
sort\
113114
strconv\
114115
strings\

src/pkg/smtp/Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright 2010 The Go Authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style
3+
# license that can be found in the LICENSE file.
4+
5+
include ../../Make.inc
6+
7+
TARG=smtp
8+
GOFILES=\
9+
auth.go\
10+
smtp.go\
11+
12+
include ../../Make.pkg

src/pkg/smtp/auth.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2010 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package smtp
6+
7+
import (
8+
"os"
9+
)
10+
11+
// Auth is implemented by an SMTP authentication mechanism.
12+
type Auth interface {
13+
// Start begins an authentication with a server.
14+
// It returns the name of the authentication protocol
15+
// and optionally data to include in the initial AUTH message
16+
// sent to the server. It can return proto == "" to indicate
17+
// that the authentication should be skipped.
18+
// If it returns a non-nil os.Error, the SMTP client aborts
19+
// the authentication attempt and closes the connection.
20+
Start(server *ServerInfo) (proto string, toServer []byte, err os.Error)
21+
22+
// Next continues the authentication. The server has just sent
23+
// the fromServer data. If more is true, the server expects a
24+
// response, which Next should return as toServer; otherwise
25+
// Next should return toServer == nil.
26+
// If Next returns a non-nil os.Error, the SMTP client aborts
27+
// the authentication attempt and closes the connection.
28+
Next(fromServer []byte, more bool) (toServer []byte, err os.Error)
29+
}
30+
31+
// ServerInfo records information about an SMTP server.
32+
type ServerInfo struct {
33+
Name string // SMTP server name
34+
TLS bool // using TLS, with valid certificate for Name
35+
Auth []string // advertised authentication mechanisms
36+
}
37+
38+
type plainAuth struct {
39+
identity, username, password string
40+
host string
41+
}
42+
43+
// PlainAuth returns an Auth that implements the PLAIN authentication
44+
// mechanism as defined in RFC 4616.
45+
// The returned Auth uses the given username and password to authenticate
46+
// on TLS connections to host and act as identity. Usually identity will be
47+
// left blank to act as username.
48+
func PlainAuth(identity, username, password, host string) Auth {
49+
return &plainAuth{identity, username, password, host}
50+
}
51+
52+
func (a *plainAuth) Start(server *ServerInfo) (string, []byte, os.Error) {
53+
if !server.TLS {
54+
return "", nil, os.NewError("unencrypted connection")
55+
}
56+
if server.Name != a.host {
57+
return "", nil, os.NewError("wrong host name")
58+
}
59+
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
60+
return "PLAIN", resp, nil
61+
}
62+
63+
func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, os.Error) {
64+
if more {
65+
// We've already sent everything.
66+
return nil, os.NewError("unexpected server challenge")
67+
}
68+
return nil, nil
69+
}

src/pkg/smtp/smtp.go

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
// Copyright 2010 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
6+
// It also implements the following extensions:
7+
// 8BITMIME RFC 1652
8+
// AUTH RFC 2554
9+
// STARTTLS RFC 3207
10+
// Additional extensions may be handled by clients.
11+
package smtp
12+
13+
import (
14+
"crypto/tls"
15+
"encoding/base64"
16+
"io"
17+
"os"
18+
"net"
19+
"net/textproto"
20+
"strings"
21+
)
22+
23+
// A Client represents a client connection to an SMTP server.
24+
type Client struct {
25+
// Text is the textproto.Conn used by the Client. It is exported to allow for
26+
// clients to add extensions.
27+
Text *textproto.Conn
28+
// keep a reference to the connection so it can be used to create a TLS
29+
// connection later
30+
conn net.Conn
31+
// whether the Client is using TLS
32+
tls bool
33+
serverName string
34+
// map of supported extensions
35+
ext map[string]string
36+
// supported auth mechanisms
37+
auth []string
38+
}
39+
40+
// Dial returns a new Client connected to an SMTP server at addr.
41+
func Dial(addr string) (*Client, os.Error) {
42+
conn, err := net.Dial("tcp", "", addr)
43+
if err != nil {
44+
return nil, err
45+
}
46+
host := addr[:strings.Index(addr, ":")]
47+
return NewClient(conn, host)
48+
}
49+
50+
// NewClient returns a new Client using an existing connection and host as a
51+
// server name to be used when authenticating.
52+
func NewClient(conn net.Conn, host string) (*Client, os.Error) {
53+
text := textproto.NewConn(conn)
54+
_, msg, err := text.ReadResponse(220)
55+
if err != nil {
56+
text.Close()
57+
return nil, err
58+
}
59+
c := &Client{Text: text, conn: conn, serverName: host}
60+
if strings.Index(msg, "ESMTP") >= 0 {
61+
err = c.ehlo()
62+
} else {
63+
err = c.helo()
64+
}
65+
return c, err
66+
}
67+
68+
// cmd is a convenience function that sends a command and returns the response
69+
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, os.Error) {
70+
id, err := c.Text.Cmd(format, args...)
71+
if err != nil {
72+
return 0, "", err
73+
}
74+
c.Text.StartResponse(id)
75+
defer c.Text.EndResponse(id)
76+
code, msg, err := c.Text.ReadResponse(expectCode)
77+
return code, msg, err
78+
}
79+
80+
// helo sends the HELO greeting to the server. It should be used only when the
81+
// server does not support ehlo.
82+
func (c *Client) helo() os.Error {
83+
c.ext = nil
84+
_, _, err := c.cmd(250, "HELO localhost")
85+
return err
86+
}
87+
88+
// ehlo sends the EHLO (extended hello) greeting to the server. It
89+
// should be the preferred greeting for servers that support it.
90+
func (c *Client) ehlo() os.Error {
91+
_, msg, err := c.cmd(250, "EHLO localhost")
92+
if err != nil {
93+
return err
94+
}
95+
ext := make(map[string]string)
96+
extList := strings.Split(msg, "\n", -1)
97+
if len(extList) > 1 {
98+
extList = extList[1:]
99+
for _, line := range extList {
100+
args := strings.Split(line, " ", 2)
101+
if len(args) > 1 {
102+
ext[args[0]] = args[1]
103+
} else {
104+
ext[args[0]] = ""
105+
}
106+
}
107+
}
108+
if mechs, ok := ext["AUTH"]; ok {
109+
c.auth = strings.Split(mechs, " ", -1)
110+
}
111+
c.ext = ext
112+
return err
113+
}
114+
115+
// StartTLS sends the STARTTLS command and encrypts all further communication.
116+
// Only servers that advertise the STARTTLS extension support this function.
117+
func (c *Client) StartTLS() os.Error {
118+
_, _, err := c.cmd(220, "STARTTLS")
119+
if err != nil {
120+
return err
121+
}
122+
c.conn = tls.Client(c.conn, nil)
123+
c.Text = textproto.NewConn(c.conn)
124+
c.tls = true
125+
return c.ehlo()
126+
}
127+
128+
// Verify checks the validity of an email address on the server.
129+
// If Verify returns nil, the address is valid. A non-nil return
130+
// does not necessarily indicate an invalid address. Many servers
131+
// will not verify addresses for security reasons.
132+
func (c *Client) Verify(addr string) os.Error {
133+
_, _, err := c.cmd(250, "VRFY %s", addr)
134+
return err
135+
}
136+
137+
// Auth authenticates a client using the provided authentication mechanism.
138+
// A failed authentication closes the connection.
139+
// Only servers that advertise the AUTH extension support this function.
140+
func (c *Client) Auth(a Auth) os.Error {
141+
encoding := base64.StdEncoding
142+
mech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})
143+
if err != nil {
144+
c.Quit()
145+
return err
146+
}
147+
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
148+
encoding.Encode(resp64, resp)
149+
code, msg64, err := c.cmd(0, "AUTH %s %s", mech, resp64)
150+
for err == nil {
151+
var msg []byte
152+
switch code {
153+
case 334:
154+
msg = make([]byte, encoding.DecodedLen(len(msg64)))
155+
_, err = encoding.Decode(msg, []byte(msg64))
156+
case 235:
157+
// the last message isn't base64 because it isn't a challenge
158+
msg = []byte(msg64)
159+
default:
160+
err = &textproto.Error{code, msg64}
161+
}
162+
resp, err = a.Next(msg, code == 334)
163+
if err != nil {
164+
// abort the AUTH
165+
c.cmd(501, "*")
166+
c.Quit()
167+
break
168+
}
169+
if resp == nil {
170+
break
171+
}
172+
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
173+
encoding.Encode(resp64, resp)
174+
code, msg64, err = c.cmd(0, string(resp64))
175+
}
176+
return err
177+
}
178+
179+
// Mail issues a MAIL command to the server using the provided email address.
180+
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
181+
// parameter.
182+
// This initiates a mail transaction and is followed by one or more Rcpt calls.
183+
func (c *Client) Mail(from string) os.Error {
184+
cmdStr := "MAIL FROM:<%s>"
185+
if c.ext != nil {
186+
if _, ok := c.ext["8BITMIME"]; ok {
187+
cmdStr += " BODY=8BITMIME"
188+
}
189+
}
190+
_, _, err := c.cmd(250, cmdStr, from)
191+
return err
192+
}
193+
194+
// Rcpt issues a RCPT command to the server using the provided email address.
195+
// A call to Rcpt must be preceded by a call to Mail and may be followed by
196+
// a Data call or another Rcpt call.
197+
func (c *Client) Rcpt(to string) os.Error {
198+
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
199+
return err
200+
}
201+
202+
type dataCloser struct {
203+
c *Client
204+
io.WriteCloser
205+
}
206+
207+
func (d *dataCloser) Close() os.Error {
208+
d.WriteCloser.Close()
209+
_, _, err := d.c.Text.ReadResponse(250)
210+
return err
211+
}
212+
213+
// Data issues a DATA command to the server and returns a writer that
214+
// can be used to write the data. The caller should close the writer
215+
// before calling any more methods on c.
216+
// A call to Data must be preceded by one or more calls to Rcpt.
217+
func (c *Client) Data() (io.WriteCloser, os.Error) {
218+
_, _, err := c.cmd(354, "DATA")
219+
if err != nil {
220+
return nil, err
221+
}
222+
return &dataCloser{c, c.Text.DotWriter()}, nil
223+
}
224+
225+
// SendMail connects to the server at addr, switches to TLS if possible,
226+
// authenticates with mechanism a if possible, and then sends an email from
227+
// address from, to addresses to, with message msg.
228+
func SendMail(addr string, a Auth, from string, to []string, msg []byte) os.Error {
229+
c, err := Dial(addr)
230+
if err != nil {
231+
return err
232+
}
233+
if ok, _ := c.Extension("STARTTLS"); ok {
234+
if err = c.StartTLS(); err != nil {
235+
return err
236+
}
237+
}
238+
if a != nil && c.ext != nil {
239+
if _, ok := c.ext["AUTH"]; ok {
240+
if err = c.Auth(a); err != nil {
241+
return err
242+
}
243+
}
244+
}
245+
if err = c.Mail(from); err != nil {
246+
return err
247+
}
248+
for _, addr := range to {
249+
if err = c.Rcpt(addr); err != nil {
250+
return err
251+
}
252+
}
253+
w, err := c.Data()
254+
if err != nil {
255+
return err
256+
}
257+
_, err = w.Write(msg)
258+
if err != nil {
259+
return err
260+
}
261+
err = w.Close()
262+
if err != nil {
263+
return err
264+
}
265+
return c.Quit()
266+
}
267+
268+
// Extension reports whether an extension is support by the server.
269+
// The extension name is case-insensitive. If the extension is supported,
270+
// Extension also returns a string that contains any parameters the
271+
// server specifies for the extension.
272+
func (c *Client) Extension(ext string) (bool, string) {
273+
if c.ext == nil {
274+
return false, ""
275+
}
276+
ext = strings.ToUpper(ext)
277+
param, ok := c.ext[ext]
278+
return ok, param
279+
}
280+
281+
// Reset sends the RSET command to the server, aborting the current mail
282+
// transaction.
283+
func (c *Client) Reset() os.Error {
284+
_, _, err := c.cmd(250, "RSET")
285+
return err
286+
}
287+
288+
// Quit sends the QUIT command and closes the connection to the server.
289+
func (c *Client) Quit() os.Error {
290+
_, _, err := c.cmd(221, "QUIT")
291+
if err != nil {
292+
return err
293+
}
294+
return c.Text.Close()
295+
}

0 commit comments

Comments
 (0)