github.com/jordwest/imap-server@v0.0.0-20200627020849-1cf758ba359f/conn/conn.go (about) 1 package conn 2 3 import ( 4 "bufio" 5 "errors" 6 "fmt" 7 "io" 8 "strings" 9 10 "github.com/jordwest/imap-server/mailstore" 11 ) 12 13 type connState int 14 15 const ( 16 // StateNew is the initial state of a client connection; before a welcome 17 // message is sent. 18 StateNew connState = iota 19 20 // StateNotAuthenticated is when a welcome message has been sent but hasn't 21 // yet authenticated. 22 StateNotAuthenticated 23 24 // StateAuthenticated is when a client has successfully authenticated but 25 // not yet selected a mailbox. 26 StateAuthenticated 27 28 // StateSelected is when a client has successfully selected a mailbox. 29 StateSelected 30 31 // StateLoggedOut is when a client has disconnected from the server. 32 StateLoggedOut 33 ) 34 35 type writeMode bool 36 37 const ( 38 readOnly writeMode = false 39 readWrite = true 40 ) 41 42 const lineEnding string = "\r\n" 43 44 // Conn represents a client connection to the IMAP server 45 type Conn struct { 46 state connState 47 Rwc io.ReadWriteCloser 48 RwcScanner *bufio.Scanner // Provides an interface for scanning lines from the connection 49 Transcript io.Writer 50 Mailstore mailstore.Mailstore // Pointer to the IMAP server's mailstore to which this connection belongs 51 User mailstore.User 52 SelectedMailbox mailstore.Mailbox 53 mailboxWritable writeMode // True if write access is allowed to the currently selected mailbox 54 } 55 56 // NewConn creates a new client connection. It's intended to be directly used 57 // with a network connection. The transcript logs all client/server interactions 58 // and is very useful while debugging. 59 func NewConn(mailstore mailstore.Mailstore, netConn io.ReadWriteCloser, transcript io.Writer) (c *Conn) { 60 c = new(Conn) 61 c.Mailstore = mailstore 62 c.Rwc = netConn 63 c.Transcript = transcript 64 return c 65 } 66 67 // SetState sets the state that an IMAP client is in. It also resets any mailbox 68 // write access. 69 func (c *Conn) SetState(state connState) { 70 c.state = state 71 72 // As a precaution, reset any mailbox write access when changing states 73 c.SetReadOnly() 74 } 75 76 // SetReadOnly sets the client connection as read-only. It forbids any 77 // operations that may modify data. 78 func (c *Conn) SetReadOnly() { c.mailboxWritable = readOnly } 79 80 // SetReadWrite sets the connection as read-write. 81 func (c *Conn) SetReadWrite() { c.mailboxWritable = readWrite } 82 83 func (c *Conn) handleRequest(req string) { 84 for _, cmd := range commands { 85 matches := cmd.match.FindStringSubmatch(req) 86 if len(matches) > 0 { 87 cmd.handler(matches, c) 88 return 89 } 90 } 91 92 c.writeResponse("", "BAD Command not understood") 93 } 94 95 // Write a response to the client. Implements io.Writer. 96 func (c *Conn) Write(p []byte) (n int, err error) { 97 fmt.Fprintf(c.Transcript, "S: %s", p) 98 99 return c.Rwc.Write(p) 100 } 101 102 // Write a response to the client. 103 func (c *Conn) writeResponse(seq string, command string) { 104 if seq == "" { 105 seq = "*" 106 } 107 // Ensure the command is terminated with a line ending 108 if !strings.HasSuffix(command, lineEnding) { 109 command += lineEnding 110 } 111 fmt.Fprintf(c, "%s %s", seq, command) 112 } 113 114 // Send the server greeting to the client. 115 func (c *Conn) sendWelcome() error { 116 if c.state != StateNew { 117 return errors.New("Welcome already sent") 118 } 119 c.writeResponse("", "OK IMAP4rev1 Service Ready") 120 c.SetState(StateNotAuthenticated) 121 return nil 122 } 123 124 func (c *Conn) assertAuthenticated(seq string) bool { 125 if c.state != StateAuthenticated && c.state != StateSelected { 126 c.writeResponse(seq, "BAD not authenticated") 127 return false 128 } 129 130 if c.User == nil { 131 panic("In authenticated state but no user is set") 132 } 133 134 return true 135 } 136 137 func (c *Conn) assertSelected(seq string, writable writeMode) bool { 138 // Ensure we are authenticated first 139 if !c.assertAuthenticated(seq) { 140 return false 141 } 142 143 if c.state != StateSelected { 144 c.writeResponse(seq, "BAD not selected") 145 return false 146 } 147 148 if c.SelectedMailbox == nil { 149 panic("In selected state but no selected mailbox is set") 150 } 151 152 if writable == readWrite && c.mailboxWritable != readWrite { 153 c.writeResponse(seq, "NO Selected mailbox is READONLY") 154 return false 155 } 156 157 return true 158 } 159 160 // Close forces the server to close the client's connection. 161 func (c *Conn) Close() error { 162 fmt.Fprintf(c.Transcript, "Server closing connection\n") 163 return c.Rwc.Close() 164 } 165 166 // ReadLine awaits a single line from the client. 167 func (c *Conn) ReadLine() (text string, ok bool) { 168 ok = c.RwcScanner.Scan() 169 return c.RwcScanner.Text(), ok 170 } 171 172 // ReadFixedLength reads data from the connection up to the specified length. 173 func (c *Conn) ReadFixedLength(length int) (data []byte, err error) { 174 // Read the whole message into a buffer 175 data = make([]byte, length) 176 receivedLength := 0 177 for receivedLength < length { 178 bytesRead, err := c.Rwc.Read(data[receivedLength:]) 179 if err != nil { 180 return data, err 181 } 182 receivedLength += bytesRead 183 } 184 185 return data, nil 186 } 187 188 // Start tells the server to start communicating with the client (after 189 // the connection has been opened). 190 func (c *Conn) Start() error { 191 if c.Rwc == nil { 192 return errors.New("No connection exists") 193 } 194 195 c.RwcScanner = bufio.NewScanner(c.Rwc) 196 197 for c.state != StateLoggedOut { 198 // Always send welcome message if we are still in new connection state 199 if c.state == StateNew { 200 c.sendWelcome() 201 } 202 203 // Await requests from the client 204 req, ok := c.ReadLine() 205 if !ok { 206 // The client has closed the connection 207 c.state = StateLoggedOut 208 break 209 } 210 fmt.Fprintf(c.Transcript, "C: %s\n", req) 211 c.handleRequest(req) 212 213 if c.RwcScanner.Err() != nil { 214 fmt.Fprintf(c.Transcript, "Scan error: %s\n", c.RwcScanner.Err()) 215 } 216 } 217 218 return nil 219 }