Do not implement SMTP VRFY.
[mailpopbox.git] / smtp / conn.go
1 package smtp
2
3 import (
4 "fmt"
5 "net"
6 "net/mail"
7 "net/textproto"
8 )
9
10 type state int
11
12 const (
13 stateNew state = iota // Before EHLO.
14 stateInitial
15 stateMail
16 stateRecipient
17 stateData
18 )
19
20 type connection struct {
21 server Server
22
23 tp *textproto.Conn
24 remoteAddr net.Addr
25
26 state
27 line string
28
29 ehlo string
30 mailFrom *mail.Address
31 rcptTo []mail.Address
32 }
33
34 func AcceptConnection(netConn net.Conn, server Server) error {
35 conn := connection{
36 server: server,
37 tp: textproto.NewConn(netConn),
38 remoteAddr: netConn.RemoteAddr(),
39 state: stateNew,
40 }
41
42 var err error
43
44 conn.writeReply(220, fmt.Sprintf("%s ESMTP [%s] (mailpopbox)", server.Name(), netConn.LocalAddr()))
45
46 for {
47 conn.line, err = conn.tp.ReadLine()
48 if err != nil {
49 conn.writeReply(500, "line too long")
50 continue
51 }
52
53 var cmd string
54 if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
55 conn.reply(ReplyBadSyntax)
56 continue
57 }
58
59 switch cmd {
60 case "QUIT":
61 conn.writeReply(221, "Goodbye")
62 conn.tp.Close()
63 break
64 case "HELO":
65 fallthrough
66 case "EHLO":
67 conn.doEHLO()
68 case "MAIL":
69 conn.doMAIL()
70 case "RCPT":
71 conn.doRCPT()
72 case "DATA":
73 conn.doDATA()
74 case "RSET":
75 conn.doRSET()
76 case "VRFY":
77 conn.writeReply(252, "I'll do my best")
78 case "EXPN":
79 conn.writeReply(550, "access denied")
80 case "NOOP":
81 conn.reply(ReplyOK)
82 case "HELP":
83 conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
84 default:
85 conn.writeReply(500, "unrecognized command")
86 }
87 }
88
89 return err
90 }
91
92 func (conn *connection) reply(reply ReplyLine) {
93 conn.writeReply(reply.Code, reply.Message)
94 }
95
96 func (conn *connection) writeReply(code int, msg string) {
97 if len(msg) > 0 {
98 conn.tp.PrintfLine("%d %s", code, msg)
99 } else {
100 conn.tp.PrintfLine("%d", code)
101 }
102 }
103
104 func (conn *connection) doEHLO() {
105 conn.resetBuffers()
106
107 var cmd string
108 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
109 if err != nil {
110 conn.reply(ReplyBadSyntax)
111 return
112 }
113
114 if cmd == "HELO" {
115 conn.writeReply(250, fmt.Sprintf("Hello %s [%s]", conn.ehlo, conn.remoteAddr))
116 } else {
117 conn.tp.PrintfLine("250-Hello %s [%s]", conn.ehlo, conn.remoteAddr)
118 if conn.server.TLSConfig() != nil {
119 conn.tp.PrintfLine("250-STARTTLS")
120 }
121 conn.tp.PrintfLine("250 SIZE %d", 40960000)
122 }
123
124 conn.state = stateInitial
125 }
126
127 func (conn *connection) doMAIL() {
128 if conn.state != stateInitial {
129 conn.reply(ReplyBadSequence)
130 return
131 }
132
133 var mailFrom string
134 _, err := fmt.Sscanf(conn.line, "MAIL FROM:%s", &mailFrom)
135 if err != nil {
136 conn.reply(ReplyBadSyntax)
137 return
138 }
139
140 conn.mailFrom, err = mail.ParseAddress(mailFrom)
141 if err != nil {
142 conn.reply(ReplyBadSyntax)
143 return
144 }
145
146 conn.state = stateMail
147 conn.reply(ReplyOK)
148 }
149
150 func (conn *connection) doRCPT() {
151 if conn.state != stateMail && conn.state != stateRecipient {
152 conn.reply(ReplyBadSequence)
153 return
154 }
155
156 var rcptTo string
157 _, err := fmt.Sscanf(conn.line, "RCPT TO:%s", &rcptTo)
158 if err != nil {
159 conn.reply(ReplyBadSyntax)
160 return
161 }
162
163 address, err := mail.ParseAddress(rcptTo)
164 if err != nil {
165 conn.reply(ReplyBadSyntax)
166 }
167
168 if reply := conn.server.VerifyAddress(*address); reply != ReplyOK {
169 conn.reply(reply)
170 return
171 }
172
173 conn.rcptTo = append(conn.rcptTo, *address)
174
175 conn.state = stateRecipient
176 conn.reply(ReplyOK)
177 }
178
179 func (conn *connection) doDATA() {
180 if conn.state != stateRecipient {
181 conn.reply(ReplyBadSequence)
182 return
183 }
184
185 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
186
187 data, err := conn.tp.ReadDotBytes()
188 if err != nil {
189 // TODO: log error
190 conn.writeReply(552, "transaction failed")
191 return
192 }
193
194 env := Envelope{
195 RemoteAddr: conn.remoteAddr,
196 EHLO: conn.ehlo,
197 MailFrom: *conn.mailFrom,
198 RcptTo: conn.rcptTo,
199 Data: data,
200 }
201
202 if reply := conn.server.OnMessageDelivered(env); reply != nil {
203 conn.reply(*reply)
204 return
205 }
206
207 conn.state = stateInitial
208 conn.reply(ReplyOK)
209 }
210
211 func (conn *connection) doRSET() {
212 conn.state = stateInitial
213 conn.resetBuffers()
214 conn.reply(ReplyOK)
215 }
216
217 func (conn *connection) resetBuffers() {
218 conn.mailFrom = nil
219 conn.rcptTo = make([]mail.Address, 0)
220 }