Define an smtp.Envelope that can be delivered to an smtp.Server.
[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().String()))
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 conn.writeReply(250, fmt.Sprintf("Hello %s, I am glad to meet you", conn.ehlo))
114
115 conn.state = stateInitial
116 }
117
118 func (conn *connection) doMAIL() {
119 if conn.state != stateInitial {
120 conn.reply(ReplyBadSequence)
121 return
122 }
123
124 var mailFrom string
125 _, err := fmt.Sscanf(conn.line, "MAIL FROM:%s", &mailFrom)
126 if err != nil {
127 conn.reply(ReplyBadSyntax)
128 return
129 }
130
131 conn.mailFrom, err = mail.ParseAddress(mailFrom)
132 if err != nil {
133 conn.reply(ReplyBadSyntax)
134 return
135 }
136
137 conn.state = stateMail
138 conn.reply(ReplyOK)
139 }
140
141 func (conn *connection) doRCPT() {
142 if conn.state != stateMail && conn.state != stateRecipient {
143 conn.reply(ReplyBadSequence)
144 return
145 }
146
147 var rcptTo string
148 _, err := fmt.Sscanf(conn.line, "RCPT TO:%s", &rcptTo)
149 if err != nil {
150 conn.reply(ReplyBadSyntax)
151 return
152 }
153
154 address, err := mail.ParseAddress(rcptTo)
155 if err != nil {
156 conn.reply(ReplyBadSyntax)
157 }
158
159 conn.rcptTo = append(conn.rcptTo, *address)
160
161 conn.state = stateRecipient
162 conn.reply(ReplyOK)
163 }
164
165 func (conn *connection) doDATA() {
166 if conn.state != stateRecipient {
167 conn.reply(ReplyBadSequence)
168 return
169 }
170
171 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
172
173 data, err := conn.tp.ReadDotBytes()
174 if err != nil {
175 // TODO: log error
176 conn.writeReply(552, "transaction failed")
177 return
178 }
179
180 env := Envelope{
181 RemoteAddr: conn.remoteAddr,
182 EHLO: conn.ehlo,
183 MailFrom: *conn.mailFrom,
184 RcptTo: conn.rcptTo,
185 Data: data,
186 }
187
188 if reply := conn.server.OnMessageDelivered(env); reply != nil {
189 conn.reply(*reply)
190 return
191 }
192
193 conn.state = stateInitial
194 conn.reply(ReplyOK)
195 }
196
197 func (conn *connection) doVRFY() {
198 }
199
200 func (conn *connection) doRSET() {
201 conn.state = stateInitial
202 conn.resetBuffers()
203 conn.reply(ReplyOK)
204 }
205
206 func (conn *connection) resetBuffers() {
207 conn.mailFrom = nil
208 conn.rcptTo = make([]mail.Address, 0)
209 }