Lower-case the addresses in smtp.conn.parsePath.
[mailpopbox.git] / smtp / conn_test.go
1 package smtp
2
3 import (
4 "fmt"
5 "net"
6 "net/mail"
7 "net/textproto"
8 "path/filepath"
9 "runtime"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/uber-go/zap"
15 )
16
17 func _fl(depth int) string {
18 _, file, line, _ := runtime.Caller(depth + 1)
19 return fmt.Sprintf("[%s:%d]", filepath.Base(file), line)
20 }
21
22 func ok(t testing.TB, err error) {
23 if err != nil {
24 t.Errorf("%s unexpected error: %v", _fl(1), err)
25 }
26 }
27
28 func readCodeLine(t testing.TB, conn *textproto.Conn, code int) string {
29 _, message, err := conn.ReadCodeLine(code)
30 if err != nil {
31 t.Errorf("%s ReadCodeLine error: %v", _fl(1), err)
32 }
33 return message
34 }
35
36 // runServer creates a TCP socket, runs a listening server, and returns the connection.
37 // The server exits when the Conn is closed.
38 func runServer(t *testing.T, server Server) net.Listener {
39 l, err := net.Listen("tcp", "localhost:0")
40 if err != nil {
41 t.Fatal(err)
42 return nil
43 }
44
45 go func() {
46 for {
47 conn, err := l.Accept()
48 if err != nil {
49 return
50 }
51 go AcceptConnection(conn, server, zap.New(zap.NullEncoder()))
52 }
53 }()
54
55 return l
56 }
57
58 type testServer struct {
59 EmptyServerCallbacks
60 blockList []string
61 }
62
63 func (s *testServer) Name() string {
64 return "Test-Server"
65 }
66
67 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
68 for _, block := range s.blockList {
69 if strings.ToLower(block) == addr.Address {
70 return ReplyBadMailbox
71 }
72 }
73 return ReplyOK
74 }
75
76 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
77 conn, err := textproto.Dial(addr.Network(), addr.String())
78 if err != nil {
79 t.Fatal(err)
80 return nil
81 }
82 return conn
83 }
84
85 type requestResponse struct {
86 request string
87 responseCode int
88 handler func(testing.TB, *textproto.Conn)
89 }
90
91 func runTableTest(t testing.TB, conn *textproto.Conn, seq []requestResponse) {
92 for i, rr := range seq {
93 t.Logf("%s case %d", _fl(1), i)
94 ok(t, conn.PrintfLine(rr.request))
95 if rr.handler != nil {
96 rr.handler(t, conn)
97 } else {
98 readCodeLine(t, conn, rr.responseCode)
99 }
100 }
101 }
102
103 // RFC 5321 ยง D.1
104 func TestScenarioTypical(t *testing.T) {
105 s := testServer{
106 blockList: []string{"Green@foo.com"},
107 }
108 l := runServer(t, &s)
109 defer l.Close()
110
111 conn := createClient(t, l.Addr())
112
113 message := readCodeLine(t, conn, 220)
114 if !strings.HasPrefix(message, s.Name()) {
115 t.Errorf("Greeting does not have server name, got %q", message)
116 }
117
118 greet := "greeting.TestScenarioTypical"
119 ok(t, conn.PrintfLine("EHLO "+greet))
120
121 _, message, err := conn.ReadResponse(250)
122 ok(t, err)
123 if !strings.Contains(message, greet) {
124 t.Errorf("EHLO response does not contain greeting, got %q", message)
125 }
126
127 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
128 readCodeLine(t, conn, 250)
129
130 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
131 readCodeLine(t, conn, 250)
132
133 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
134 readCodeLine(t, conn, 550)
135
136 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
137 readCodeLine(t, conn, 250)
138
139 ok(t, conn.PrintfLine("DATA"))
140 readCodeLine(t, conn, 354)
141
142 ok(t, conn.PrintfLine("Blah blah blah..."))
143 ok(t, conn.PrintfLine("...etc. etc. etc."))
144 ok(t, conn.PrintfLine("."))
145 readCodeLine(t, conn, 250)
146
147 ok(t, conn.PrintfLine("QUIT"))
148 readCodeLine(t, conn, 221)
149 }
150
151 func TestVerifyAddress(t *testing.T) {
152 s := testServer{
153 blockList: []string{"banned@test.mail"},
154 }
155 l := runServer(t, &s)
156 defer l.Close()
157
158 conn := createClient(t, l.Addr())
159 readCodeLine(t, conn, 220)
160
161 runTableTest(t, conn, []requestResponse{
162 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
163 {"VRFY banned@test.mail", 252, nil},
164 {"VRFY allowed@test.mail", 252, nil},
165 {"MAIL FROM:<sender@example.com>", 250, nil},
166 {"RCPT TO:<banned@test.mail>", 550, nil},
167 {"QUIT", 221, nil},
168 })
169 }
170
171 func TestBadAddress(t *testing.T) {
172 l := runServer(t, &testServer{})
173 defer l.Close()
174
175 conn := createClient(t, l.Addr())
176 readCodeLine(t, conn, 220)
177
178 runTableTest(t, conn, []requestResponse{
179 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
180 {"MAIL FROM:<sender>", 501, nil},
181 {"MAIL FROM:<sender@foo.com> SIZE=2163", 250, nil},
182 {"RCPT TO:<banned.net>", 501, nil},
183 {"QUIT", 221, nil},
184 })
185 }
186
187 func TestCaseSensitivty(t *testing.T) {
188 s := &testServer{}
189 s.blockList = []string{"reject@mail.com"}
190 l := runServer(t, s)
191 defer l.Close()
192
193 conn := createClient(t, l.Addr())
194 readCodeLine(t, conn, 220)
195
196 runTableTest(t, conn, []requestResponse{
197 {"nOoP", 250, nil},
198 {"ehLO test.TEST", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
199 {"mail FROM:<sender@example.com>", 250, nil},
200 {"RcPT tO:<receive@mail.com>", 250, nil},
201 {"RCPT TO:<reject@MAIL.com>", 550, nil},
202 {"RCPT TO:<reject@mail.com>", 550, nil},
203 {"DATa", 0, func(t testing.TB, conn *textproto.Conn) {
204 readCodeLine(t, conn, 354)
205
206 ok(t, conn.PrintfLine("."))
207 readCodeLine(t, conn, 250)
208 }},
209 {"MAIL FR:", 501, nil},
210 {"QUiT", 221, nil},
211 })
212 }
213
214 func TestGetReceivedInfo(t *testing.T) {
215 conn := connection{
216 server: &testServer{},
217 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
218 }
219
220 now := time.Now()
221
222 const crlf = "\r\n"
223 const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
224 const line2 = "by Test-Server (mailpopbox) with "
225 const msgId = "abcdef.hijk"
226 lineLast := now.Format(time.RFC1123Z) + crlf
227
228 type params struct {
229 ehlo string
230 esmtp bool
231 tls bool
232 address string
233 }
234
235 tests := []struct {
236 params params
237
238 expect []string
239 }{
240 {params{"remote.test.", true, false, "foo@bar.com"},
241 []string{line1,
242 line2 + "ESMTP id " + msgId + crlf,
243 "for <foo@bar.com>" + crlf,
244 "(using PLAINTEXT);" + crlf,
245 lineLast, ""}},
246 }
247
248 for _, test := range tests {
249 t.Logf("%#v", test.params)
250
251 conn.ehlo = test.params.ehlo
252 conn.esmtp = test.params.esmtp
253 //conn.tls = test.params.tls
254
255 envelope := Envelope{
256 RcptTo: []mail.Address{{"", test.params.address}},
257 Received: now,
258 ID: msgId,
259 }
260
261 actual := conn.getReceivedInfo(envelope)
262 actualLines := strings.SplitAfter(string(actual), crlf)
263
264 if len(actualLines) != len(test.expect) {
265 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
266 continue
267 }
268
269 for i, line := range actualLines {
270 expect := test.expect[i]
271 if expect != strings.TrimLeft(line, " ") {
272 t.Errorf("Expected equal string %q, got %q", expect, line)
273 }
274 }
275 }
276
277 }