2 // Copyright 2020 Blue Static <https://www.bluestatic.org>
3 // This program is free software licensed under the GNU General Public License,
4 // version 3.0. The full text of the license can be found in LICENSE.txt.
5 // SPDX-License-Identifier: GPL-3.0-only
23 func _fl(depth int) string {
24 _, file, line, _ := runtime.Caller(depth + 1)
25 return fmt.Sprintf("[%s:%d]", filepath.Base(file), line)
28 func ok(t testing.TB, err error) {
30 t.Errorf("%s unexpected error: %v", _fl(1), err)
34 func readCodeLine(t testing.TB, conn *textproto.Conn, code int) string {
35 _, message, err := conn.ReadCodeLine(code)
37 t.Errorf("%s ReadCodeLine error: %v", _fl(1), err)
42 // runServer creates a TCP socket, runs a listening server, and returns the connection.
43 // The server exits when the Conn is closed.
44 func runServer(t *testing.T, server Server) net.Listener {
45 l, err := net.Listen("tcp", "localhost:0")
53 conn, err := l.Accept()
57 go AcceptConnection(conn, server, zap.New(zap.NullEncoder()))
64 type testServer struct {
69 func (s *testServer) Name() string {
73 func (s *testServer) VerifyAddress(addr mail.Address) ReplyLine {
74 for _, block := range s.blockList {
75 if strings.ToLower(block) == addr.Address {
76 return ReplyBadMailbox
82 func createClient(t *testing.T, addr net.Addr) *textproto.Conn {
83 conn, err := textproto.Dial(addr.Network(), addr.String())
91 type requestResponse struct {
94 handler func(testing.TB, *textproto.Conn)
97 func runTableTest(t testing.TB, conn *textproto.Conn, seq []requestResponse) {
98 for i, rr := range seq {
99 t.Logf("%s case %d", _fl(1), i)
100 ok(t, conn.PrintfLine(rr.request))
101 if rr.handler != nil {
104 readCodeLine(t, conn, rr.responseCode)
110 func TestScenarioTypical(t *testing.T) {
112 blockList: []string{"Green@foo.com"},
114 l := runServer(t, &s)
117 conn := createClient(t, l.Addr())
119 message := readCodeLine(t, conn, 220)
120 if !strings.HasPrefix(message, s.Name()) {
121 t.Errorf("Greeting does not have server name, got %q", message)
124 greet := "greeting.TestScenarioTypical"
125 ok(t, conn.PrintfLine("EHLO "+greet))
127 _, message, err := conn.ReadResponse(250)
129 if !strings.Contains(message, greet) {
130 t.Errorf("EHLO response does not contain greeting, got %q", message)
133 ok(t, conn.PrintfLine("MAIL FROM:<Smith@bar.com>"))
134 readCodeLine(t, conn, 250)
136 ok(t, conn.PrintfLine("RCPT TO:<Jones@foo.com>"))
137 readCodeLine(t, conn, 250)
139 ok(t, conn.PrintfLine("RCPT TO:<Green@foo.com>"))
140 readCodeLine(t, conn, 550)
142 ok(t, conn.PrintfLine("RCPT TO:<Brown@foo.com>"))
143 readCodeLine(t, conn, 250)
145 ok(t, conn.PrintfLine("DATA"))
146 readCodeLine(t, conn, 354)
148 ok(t, conn.PrintfLine("Blah blah blah..."))
149 ok(t, conn.PrintfLine("...etc. etc. etc."))
150 ok(t, conn.PrintfLine("."))
151 readCodeLine(t, conn, 250)
153 ok(t, conn.PrintfLine("QUIT"))
154 readCodeLine(t, conn, 221)
157 func TestVerifyAddress(t *testing.T) {
159 blockList: []string{"banned@test.mail"},
161 l := runServer(t, &s)
164 conn := createClient(t, l.Addr())
165 readCodeLine(t, conn, 220)
167 runTableTest(t, conn, []requestResponse{
168 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
169 {"VRFY banned@test.mail", 252, nil},
170 {"VRFY allowed@test.mail", 252, nil},
171 {"MAIL FROM:<sender@example.com>", 250, nil},
172 {"RCPT TO:<banned@test.mail>", 550, nil},
177 func TestBadAddress(t *testing.T) {
178 l := runServer(t, &testServer{})
181 conn := createClient(t, l.Addr())
182 readCodeLine(t, conn, 220)
184 runTableTest(t, conn, []requestResponse{
185 {"EHLO test", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
186 {"MAIL FROM:<sender>", 501, nil},
187 {"MAIL FROM:<sender@foo.com> SIZE=2163", 250, nil},
188 {"RCPT TO:<banned.net>", 501, nil},
193 func TestCaseSensitivty(t *testing.T) {
195 s.blockList = []string{"reject@mail.com"}
199 conn := createClient(t, l.Addr())
200 readCodeLine(t, conn, 220)
202 runTableTest(t, conn, []requestResponse{
204 {"ehLO test.TEST", 0, func(t testing.TB, conn *textproto.Conn) { conn.ReadResponse(250) }},
205 {"mail FROM:<sender@example.com>", 250, nil},
206 {"RcPT tO:<receive@mail.com>", 250, nil},
207 {"RCPT TO:<reject@MAIL.com>", 550, nil},
208 {"RCPT TO:<reject@mail.com>", 550, nil},
209 {"DATa", 0, func(t testing.TB, conn *textproto.Conn) {
210 readCodeLine(t, conn, 354)
212 ok(t, conn.PrintfLine("."))
213 readCodeLine(t, conn, 250)
215 {"MAIL FR:", 501, nil},
220 func TestGetReceivedInfo(t *testing.T) {
222 server: &testServer{},
223 remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
229 const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
230 const line2 = "by Test-Server (mailpopbox) with "
231 const msgId = "abcdef.hijk"
232 lineLast := now.Format(time.RFC1123Z) + crlf
246 {params{"remote.test.", true, false, "foo@bar.com"},
248 line2 + "ESMTP id " + msgId + crlf,
249 "for <foo@bar.com>" + crlf,
250 "(using PLAINTEXT);" + crlf,
254 for _, test := range tests {
255 t.Logf("%#v", test.params)
257 conn.ehlo = test.params.ehlo
258 conn.esmtp = test.params.esmtp
259 //conn.tls = test.params.tls
261 envelope := Envelope{
262 RcptTo: []mail.Address{{"", test.params.address}},
267 actual := conn.getReceivedInfo(envelope)
268 actualLines := strings.SplitAfter(string(actual), crlf)
270 if len(actualLines) != len(test.expect) {
271 t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
275 for i, line := range actualLines {
276 expect := test.expect[i]
277 if expect != strings.TrimLeft(line, " ") {
278 t.Errorf("Expected equal string %q, got %q", expect, line)