1package server
2
3import (
4	"crypto/tls"
5	"errors"
6	"fmt"
7	"io"
8	"net"
9	"runtime/debug"
10	"time"
11
12	"github.com/emersion/go-imap"
13	"github.com/emersion/go-imap/backend"
14)
15
16// Conn is a connection to a client.
17type Conn interface {
18	io.Reader
19
20	// Server returns this connection's server.
21	Server() *Server
22	// Context returns this connection's context.
23	Context() *Context
24	// Capabilities returns a list of capabilities enabled for this connection.
25	Capabilities() []string
26	// WriteResp writes a response to this connection.
27	WriteResp(res imap.WriterTo) error
28	// IsTLS returns true if TLS is enabled.
29	IsTLS() bool
30	// TLSState returns the TLS connection state if TLS is enabled, nil otherwise.
31	TLSState() *tls.ConnectionState
32	// Upgrade upgrades a connection, e.g. wrap an unencrypted connection with an
33	// encrypted tunnel.
34	Upgrade(upgrader imap.ConnUpgrader) error
35	// Close closes this connection.
36	Close() error
37	WaitReady()
38
39	Info() *imap.ConnInfo
40
41	setTLSConn(*tls.Conn)
42	silent() *bool // TODO: remove this
43	serve(Conn) error
44	commandHandler(cmd *imap.Command) (hdlr Handler, err error)
45}
46
47// Context stores a connection's metadata.
48type Context struct {
49	// This connection's current state.
50	State imap.ConnState
51	// If the client is logged in, the user.
52	User backend.User
53	// If the client has selected a mailbox, the mailbox.
54	Mailbox backend.Mailbox
55	// True if the currently selected mailbox has been opened in read-only mode.
56	MailboxReadOnly bool
57	// Responses to send to the client.
58	Responses chan<- imap.WriterTo
59	// Closed when the client is logged out.
60	LoggedOut <-chan struct{}
61}
62
63type conn struct {
64	*imap.Conn
65
66	conn      Conn // With extensions overrides
67	s         *Server
68	ctx       *Context
69	tlsConn   *tls.Conn
70	continues chan bool
71	upgrade   chan bool
72	responses chan imap.WriterTo
73	loggedOut chan struct{}
74	silentVal bool
75}
76
77func newConn(s *Server, c net.Conn) *conn {
78	// Create an imap.Reader and an imap.Writer
79	continues := make(chan bool)
80	r := imap.NewServerReader(nil, continues)
81	w := imap.NewWriter(nil)
82
83	responses := make(chan imap.WriterTo)
84	loggedOut := make(chan struct{})
85
86	tlsConn, _ := c.(*tls.Conn)
87
88	conn := &conn{
89		Conn: imap.NewConn(c, r, w),
90
91		s: s,
92		ctx: &Context{
93			State:     imap.ConnectingState,
94			Responses: responses,
95			LoggedOut: loggedOut,
96		},
97		tlsConn:   tlsConn,
98		continues: continues,
99		upgrade:   make(chan bool),
100		responses: responses,
101		loggedOut: loggedOut,
102	}
103
104	if s.Debug != nil {
105		conn.Conn.SetDebug(s.Debug)
106	}
107	if s.MaxLiteralSize > 0 {
108		conn.Conn.MaxLiteralSize = s.MaxLiteralSize
109	}
110
111	go conn.send()
112
113	return conn
114}
115
116func (c *conn) Server() *Server {
117	return c.s
118}
119
120func (c *conn) Context() *Context {
121	return c.ctx
122}
123
124type response struct {
125	response imap.WriterTo
126	done     chan struct{}
127}
128
129func (r *response) WriteTo(w *imap.Writer) error {
130	err := r.response.WriteTo(w)
131	close(r.done)
132	return err
133}
134
135func (c *conn) setDeadline() {
136	if c.s.AutoLogout == 0 {
137		return
138	}
139
140	dur := c.s.AutoLogout
141	if dur < MinAutoLogout {
142		dur = MinAutoLogout
143	}
144	t := time.Now().Add(dur)
145
146	c.Conn.SetDeadline(t)
147}
148
149func (c *conn) WriteResp(r imap.WriterTo) error {
150	done := make(chan struct{})
151	c.responses <- &response{r, done}
152	<-done
153	c.setDeadline()
154	return nil
155}
156
157func (c *conn) Close() error {
158	if c.ctx.User != nil {
159		c.ctx.User.Logout()
160	}
161
162	return c.Conn.Close()
163}
164
165func (c *conn) Capabilities() []string {
166	caps := []string{"IMAP4rev1", "LITERAL+", "SASL-IR"}
167
168	if c.ctx.State == imap.NotAuthenticatedState {
169		if !c.IsTLS() && c.s.TLSConfig != nil {
170			caps = append(caps, "STARTTLS")
171		}
172
173		if !c.canAuth() {
174			caps = append(caps, "LOGINDISABLED")
175		} else {
176			for name := range c.s.auths {
177				caps = append(caps, "AUTH="+name)
178			}
179		}
180	}
181
182	for _, ext := range c.s.extensions {
183		caps = append(caps, ext.Capabilities(c)...)
184	}
185
186	return caps
187}
188
189func (c *conn) writeAndFlush(w imap.WriterTo) error {
190	if err := w.WriteTo(c.Writer); err != nil {
191		return err
192	}
193	return c.Writer.Flush()
194}
195
196func (c *conn) send() {
197	// Send responses
198	for {
199		select {
200		case <-c.upgrade:
201			// Wait until upgrade is finished.
202			c.Wait()
203		case needCont := <-c.continues:
204			// Send continuation requests
205			if needCont {
206				resp := &imap.ContinuationReq{Info: "send literal"}
207				if err := c.writeAndFlush(resp); err != nil {
208					c.Server().ErrorLog.Println("cannot send continuation request: ", err)
209				}
210			}
211		case res := <-c.responses:
212			// Got a response that needs to be sent
213			// Request to send the response
214			if err := c.writeAndFlush(res); err != nil {
215				c.Server().ErrorLog.Println("cannot send response: ", err)
216			}
217		case <-c.loggedOut:
218			return
219		}
220	}
221}
222
223func (c *conn) greet() error {
224	c.ctx.State = imap.NotAuthenticatedState
225
226	caps := c.Capabilities()
227	args := make([]interface{}, len(caps))
228	for i, cap := range caps {
229		args[i] = imap.RawString(cap)
230	}
231
232	greeting := &imap.StatusResp{
233		Type:      imap.StatusRespOk,
234		Code:      imap.CodeCapability,
235		Arguments: args,
236		Info:      "IMAP4rev1 Service Ready",
237	}
238
239	return c.WriteResp(greeting)
240}
241
242func (c *conn) setTLSConn(tlsConn *tls.Conn) {
243	c.tlsConn = tlsConn
244}
245
246func (c *conn) IsTLS() bool {
247	return c.tlsConn != nil
248}
249
250func (c *conn) TLSState() *tls.ConnectionState {
251	if c.tlsConn != nil {
252		state := c.tlsConn.ConnectionState()
253		return &state
254	}
255	return nil
256}
257
258// canAuth checks if the client can use plain text authentication.
259func (c *conn) canAuth() bool {
260	return c.IsTLS() || c.s.AllowInsecureAuth
261}
262
263func (c *conn) silent() *bool {
264	return &c.silentVal
265}
266
267func (c *conn) serve(conn Conn) (err error) {
268	c.conn = conn
269
270	defer func() {
271		c.ctx.State = imap.LogoutState
272		close(c.loggedOut)
273	}()
274
275	defer func() {
276		if r := recover(); r != nil {
277			c.WriteResp(&imap.StatusResp{
278				Type: imap.StatusRespBye,
279				Info: "Internal server error, closing connection.",
280			})
281
282			stack := debug.Stack()
283			c.s.ErrorLog.Printf("panic serving %v: %v\n%s", c.Info().RemoteAddr, r, stack)
284
285			err = fmt.Errorf("%v", r)
286		}
287	}()
288
289	// Send greeting
290	if err := c.greet(); err != nil {
291		return err
292	}
293
294	for {
295		if c.ctx.State == imap.LogoutState {
296			return nil
297		}
298
299		var res *imap.StatusResp
300		var up Upgrader
301
302		fields, err := c.ReadLine()
303		if err == io.EOF || c.ctx.State == imap.LogoutState {
304			return nil
305		}
306		c.setDeadline()
307
308		if err != nil {
309			if imap.IsParseError(err) {
310				res = &imap.StatusResp{
311					Type: imap.StatusRespBad,
312					Info: err.Error(),
313				}
314			} else {
315				c.s.ErrorLog.Println("cannot read command:", err)
316				return err
317			}
318		} else {
319			cmd := &imap.Command{}
320			if err := cmd.Parse(fields); err != nil {
321				res = &imap.StatusResp{
322					Tag:  cmd.Tag,
323					Type: imap.StatusRespBad,
324					Info: err.Error(),
325				}
326			} else {
327				var err error
328				res, up, err = c.handleCommand(cmd)
329				if err != nil {
330					res = &imap.StatusResp{
331						Tag:  cmd.Tag,
332						Type: imap.StatusRespBad,
333						Info: err.Error(),
334					}
335				}
336			}
337		}
338
339		if res != nil {
340
341			if err := c.WriteResp(res); err != nil {
342				c.s.ErrorLog.Println("cannot write response:", err)
343				continue
344			}
345
346			if up != nil && res.Type == imap.StatusRespOk {
347				if err := up.Upgrade(c.conn); err != nil {
348					c.s.ErrorLog.Println("cannot upgrade connection:", err)
349					return err
350				}
351			}
352		}
353	}
354}
355
356func (c *conn) WaitReady() {
357	c.upgrade <- true
358	c.Conn.WaitReady()
359}
360
361func (c *conn) commandHandler(cmd *imap.Command) (hdlr Handler, err error) {
362	newHandler := c.s.Command(cmd.Name)
363	if newHandler == nil {
364		err = errors.New("Unknown command")
365		return
366	}
367
368	hdlr = newHandler()
369	err = hdlr.Parse(cmd.Arguments)
370	return
371}
372
373func (c *conn) handleCommand(cmd *imap.Command) (res *imap.StatusResp, up Upgrader, err error) {
374	hdlr, err := c.commandHandler(cmd)
375	if err != nil {
376		return
377	}
378
379	hdlrErr := hdlr.Handle(c.conn)
380	if statusErr, ok := hdlrErr.(*errStatusResp); ok {
381		res = statusErr.resp
382	} else if hdlrErr != nil {
383		res = &imap.StatusResp{
384			Type: imap.StatusRespNo,
385			Info: hdlrErr.Error(),
386		}
387	} else {
388		res = &imap.StatusResp{
389			Type: imap.StatusRespOk,
390		}
391	}
392
393	if res != nil {
394		res.Tag = cmd.Tag
395
396		if res.Type == imap.StatusRespOk && res.Info == "" {
397			res.Info = cmd.Name + " completed"
398		}
399	}
400
401	up, _ = hdlr.(Upgrader)
402	return
403}
404