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  }