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