A basic 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 EHOL.
14 stateInitial
15 stateMail
16 stateRecipient
17 stateData
18 )
19
20 type connection struct {
21 tp *textproto.Conn
22 remoteAddr net.Addr
23
24 state
25 line string
26
27 ehlo string
28 mailFrom *mail.Address
29 rcptTo []mail.Address
30 }
31
32 func AcceptConnection(netConn net.Conn, server Server) error {
33 conn := connection{
34 tp: textproto.NewConn(netConn),
35 remoteAddr: netConn.RemoteAddr(),
36 state: stateNew,
37 }
38
39 var err error
40
41 conn.writeReply(250, fmt.Sprintf("%s ESMTP [%s] mailpopbox", server.Name(), netConn.LocalAddr().String()))
42
43 for {
44 conn.line, err = conn.tp.ReadLine()
45 if err != nil {
46 conn.writeReply(500, "line too long")
47 continue
48 }
49
50 var cmd string
51 if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
52 conn.writeBadSyntax()
53 continue
54 }
55
56 switch cmd {
57 case "QUIT":
58 conn.tp.Close()
59 break
60 case "HELO":
61 fallthrough
62 case "EHLO":
63 conn.doEHLO()
64 case "MAIL":
65 conn.doMAIL()
66 case "RCPT":
67 conn.doRCPT()
68 case "DATA":
69 conn.doDATA()
70 case "RSET":
71 conn.doRSET()
72 case "VRFY":
73 conn.doVRFY()
74 case "EXPN":
75 conn.writeReply(550, "access denied")
76 case "NOOP":
77 conn.writeOK()
78 case "HELP":
79 conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
80 default:
81 conn.writeReply(500, "unrecognized command")
82 }
83 }
84
85 return err
86 }
87
88 func (conn *connection) writeReply(code int, msg string) {
89 if len(msg) > 0 {
90 conn.tp.PrintfLine("%d %s", code, msg)
91 } else {
92 conn.tp.PrintfLine("%d", code)
93 }
94 }
95
96 func (conn *connection) writeOK() {
97 conn.writeReply(250, "OK")
98 }
99
100 func (conn *connection) writeBadSyntax() {
101 conn.writeReply(501, "syntax error")
102 }
103
104 func (conn *connection) writeBadSequence() {
105 conn.writeReply(503, "bad sequence of commands")
106 }
107
108 func (conn *connection) doEHLO() {
109 conn.resetBuffers()
110
111 var cmd string
112 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
113 if err != nil {
114 conn.writeBadSyntax()
115 return
116 }
117
118 conn.writeReply(250, fmt.Sprintf("Hello %s, I am glad to meet you", conn.ehlo))
119
120 conn.state = stateInitial
121 }
122
123 func (conn *connection) doMAIL() {
124 if conn.state != stateInitial {
125 conn.writeBadSequence()
126 return
127 }
128
129 var mailFrom string
130 _, err := fmt.Sscanf(conn.line, "MAIL FROM:%s", &mailFrom)
131 if err != nil {
132 conn.writeBadSyntax()
133 return
134 }
135
136 conn.mailFrom, err = mail.ParseAddress(mailFrom)
137 if err != nil {
138 conn.writeBadSyntax()
139 return
140 }
141
142 conn.state = stateMail
143 conn.writeOK()
144 }
145
146 func (conn *connection) doRCPT() {
147 if conn.state != stateMail && conn.state != stateRecipient {
148 conn.writeBadSequence()
149 return
150 }
151
152 var rcptTo string
153 _, err := fmt.Sscanf(conn.line, "RCPT TO:%s", &rcptTo)
154 if err != nil {
155 conn.writeBadSyntax()
156 return
157 }
158
159 address, err := mail.ParseAddress(rcptTo)
160 if err != nil {
161 conn.writeBadSyntax()
162 }
163
164 conn.rcptTo = append(conn.rcptTo, *address)
165
166 conn.state = stateRecipient
167 conn.writeOK()
168 }
169
170 func (conn *connection) doDATA() {
171 if conn.state != stateRecipient {
172 conn.writeBadSequence()
173 return
174 }
175
176 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
177
178 data, err := conn.tp.ReadDotBytes()
179 if err != nil {
180 // TODO: log error
181 conn.writeReply(552, "transaction failed")
182 return
183 }
184
185 fmt.Println(string(data))
186
187 conn.state = stateInitial
188 conn.writeOK()
189 }
190
191 func (conn *connection) doVRFY() {
192 }
193
194 func (conn *connection) doRSET() {
195 conn.state = stateInitial
196 conn.resetBuffers()
197 conn.writeOK()
198 }
199
200 func (conn *connection) resetBuffers() {
201 conn.mailFrom = nil
202 conn.rcptTo = make([]mail.Address, 0)
203 }