Support SMTP along with ESMTP.
[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(250, 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.tp.Close()
62 break
63 case "HELO":
64 fallthrough
65 case "EHLO":
66 conn.doEHLO()
67 case "MAIL":
68 conn.doMAIL()
69 case "RCPT":
70 conn.doRCPT()
71 case "DATA":
72 conn.doDATA()
73 case "RSET":
74 conn.doRSET()
75 case "VRFY":
76 conn.doVRFY()
77 case "EXPN":
78 conn.writeReply(550, "access denied")
79 case "NOOP":
80 conn.reply(ReplyOK)
81 case "HELP":
82 conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
83 default:
84 conn.writeReply(500, "unrecognized command")
85 }
86 }
87
88 return err
89 }
90
91 func (conn *connection) reply(reply ReplyLine) {
92 conn.writeReply(reply.Code, reply.Message)
93 }
94
95 func (conn *connection) writeReply(code int, msg string) {
96 if len(msg) > 0 {
97 conn.tp.PrintfLine("%d %s", code, msg)
98 } else {
99 conn.tp.PrintfLine("%d", code)
100 }
101 }
102
103 func (conn *connection) doEHLO() {
104 conn.resetBuffers()
105
106 var cmd string
107 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
108 if err != nil {
109 conn.reply(ReplyBadSyntax)
110 return
111 }
112
113 if cmd == "HELO" {
114 conn.writeReply(250, fmt.Sprintf("Hello %s [%s]", conn.ehlo, conn.remoteAddr))
115 } else {
116 conn.tp.PrintfLine("250-Hello %s [%s]", conn.ehlo, conn.remoteAddr)
117 if conn.server.TLSConfig() != nil {
118 conn.tp.PrintfLine("250-STARTTLS")
119 }
120 conn.tp.PrintfLine("250 SIZE %d", 40960000)
121 }
122
123 conn.state = stateInitial
124 }
125
126 func (conn *connection) doMAIL() {
127 if conn.state != stateInitial {
128 conn.reply(ReplyBadSequence)
129 return
130 }
131
132 var mailFrom string
133 _, err := fmt.Sscanf(conn.line, "MAIL FROM:%s", &mailFrom)
134 if err != nil {
135 conn.reply(ReplyBadSyntax)
136 return
137 }
138
139 conn.mailFrom, err = mail.ParseAddress(mailFrom)
140 if err != nil {
141 conn.reply(ReplyBadSyntax)
142 return
143 }
144
145 conn.state = stateMail
146 conn.reply(ReplyOK)
147 }
148
149 func (conn *connection) doRCPT() {
150 if conn.state != stateMail && conn.state != stateRecipient {
151 conn.reply(ReplyBadSequence)
152 return
153 }
154
155 var rcptTo string
156 _, err := fmt.Sscanf(conn.line, "RCPT TO:%s", &rcptTo)
157 if err != nil {
158 conn.reply(ReplyBadSyntax)
159 return
160 }
161
162 address, err := mail.ParseAddress(rcptTo)
163 if err != nil {
164 conn.reply(ReplyBadSyntax)
165 }
166
167 conn.rcptTo = append(conn.rcptTo, *address)
168
169 conn.state = stateRecipient
170 conn.reply(ReplyOK)
171 }
172
173 func (conn *connection) doDATA() {
174 if conn.state != stateRecipient {
175 conn.reply(ReplyBadSequence)
176 return
177 }
178
179 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
180
181 data, err := conn.tp.ReadDotBytes()
182 if err != nil {
183 // TODO: log error
184 conn.writeReply(552, "transaction failed")
185 return
186 }
187
188 env := Envelope{
189 RemoteAddr: conn.remoteAddr,
190 EHLO: conn.ehlo,
191 MailFrom: *conn.mailFrom,
192 RcptTo: conn.rcptTo,
193 Data: data,
194 }
195
196 if reply := conn.server.OnMessageDelivered(env); reply != nil {
197 conn.reply(*reply)
198 return
199 }
200
201 conn.state = stateInitial
202 conn.reply(ReplyOK)
203 }
204
205 func (conn *connection) doVRFY() {
206 }
207
208 func (conn *connection) doRSET() {
209 conn.state = stateInitial
210 conn.resetBuffers()
211 conn.reply(ReplyOK)
212 }
213
214 func (conn *connection) resetBuffers() {
215 conn.mailFrom = nil
216 conn.rcptTo = make([]mail.Address, 0)
217 }