package main import ( "bufio" "bytes" "slices" "strings" "go.lindenii.runxiyu.org/lindenii-common/mailkit" ) type server_state_t uint const ( server_state_begin server_state_t = iota server_state_helo server_state_mail server_state_rcpt ) func handle_incoming_server_connection(reader *bufio.Reader, writer *bufio.Writer) error { var my_server_name string var routes map[string]string config_consistent_run(func() { my_server_name = config.Server_name routes = config.Routes }) _, _ = writer.WriteString("220 " + my_server_name + " " + VERSION + "\r\n") _ = writer.Flush() server_state := server_state_begin var remote_server_name string var current_mail_from string var current_rcpt_to []string for { line, err := reader.ReadString('\n') if err != nil { return err } line = strings.TrimSuffix(line, "\n") line = strings.TrimSuffix(line, "\r") cmd_end := strings.IndexByte(line, ' ') var param_start int if cmd_end == -1 { cmd_end = len(line) param_start = len(line) } else { param_start = cmd_end + 1 } cmd := strings.ToUpper(line[:cmd_end]) param := line[param_start:] switch_cmd: switch cmd { case "HELO": if param == "" { // TODO: actually validate the hostname _, _ = writer.WriteString("501 Syntax: HELO hostname\r\n") _ = writer.Flush() break } remote_server_name = param _ = remote_server_name // TODO server_state = server_state_helo _, _ = writer.WriteString("250 " + my_server_name + "\r\n") _ = writer.Flush() case "MAIL": switch server_state { case server_state_begin: _, _ = writer.WriteString("503 5.5.1 Error: send HELO/EHLO first\r\n") _ = writer.Flush() break switch_cmd case server_state_helo: break case server_state_mail: _, _ = writer.WriteString("503 5.5.1 Error: nested MAIL command\r\n") _ = writer.Flush() break switch_cmd } if len(param) <= len("FROM:") || strings.ToUpper(param[:len("FROM:")]) != "FROM:" { _, _ = writer.WriteString("501 5.5.4 Syntax: MAIL FROM:
\r\n") _ = writer.Flush() break } current_mail_from = param[len("FROM:"):] current_rcpt_to = []string{} server_state = server_state_mail _, _ = writer.WriteString("250 2.1.0 Ok\r\n") _ = writer.Flush() // TODO: Address validation case "RCPT": if server_state != server_state_mail && server_state != server_state_rcpt { _, _ = writer.WriteString("503 5.5.1 Error: need MAIL command\r\n") _ = writer.Flush() break } if len(param) <= len("TO:") || strings.ToUpper(param[:len("TO:")]) != "TO:" { _, _ = writer.WriteString("501 5.5.4 Syntax: RCPT TO:
\r\n") _ = writer.Flush() break } recipient, _, _ := mailkit.Strip_angle_brackets(param[len("TO:"):]) _, ok := routes[recipient] if !ok { _, _ = writer.WriteString("550 5.1.1 <" + recipient + ">: Recipient address rejected: User unknown in local recipient table\r\n") _ = writer.Flush() break switch_cmd } current_rcpt_to = append(current_rcpt_to, recipient) server_state = server_state_rcpt _, _ = writer.WriteString("250 2.1.5 Ok\r\n") _ = writer.Flush() case "DATA": if server_state != server_state_rcpt { _, _ = writer.WriteString("503 5.5.1 Error: need RCPT command\r\n") _ = writer.Flush() break } _, _ = writer.WriteString("354 End data with .\r\n") _ = writer.Flush() var current_data []byte for { tmp, err := reader.ReadSlice('\r') if err != nil { return err } // reader.ReadSlice returns an internal buffer that gets // overwritten on the next reader operation. So we must // make a copy; also we have to allocate data_part to // the correct length because [[builtin.copy]] copies // min(len(dst), len(src)) items. data_part := make([]byte, len(tmp)) copy(data_part, tmp) next_four, err := reader.Peek(4) if err != nil { return err } if bytes.Equal(next_four, []byte{'\n', '.', '\r', '\n'}) { current_data = slices.Concat(current_data, data_part[:len(data_part)-1]) break } current_data = slices.Concat(current_data, data_part) } _, err := reader.Discard(4) if err != nil { return err } { inboxes_to_deliver_to := make(map[string]struct{}) for _, recipient := range current_rcpt_to { inbox, ok := routes[recipient] if !ok { _, _ = writer.WriteString("550 5.1.1 <" + recipient + ">: Recipient address rejected: User unknown in local recipient table\r\n") break switch_cmd } inboxes_to_deliver_to[inbox] = struct{}{} } for inbox := range inboxes_to_deliver_to { err = deliver_incoming(current_mail_from, current_rcpt_to, current_data, inbox) } } if err == nil { _, _ = writer.WriteString("250 2.0.0 Ok: Accepted\r\n") } else { _, _ = writer.WriteString("500 2.0.0 Error: " + err.Error() + "\r\n") } _ = writer.Flush() server_state = server_state_helo case "QUIT": _, _ = writer.WriteString("221 2.0.0 Bye\r\n") _ = writer.Flush() return nil case "NOOP": _, _ = writer.WriteString("250 2.0.0 Ok\r\n") _ = writer.Flush() case "RSET": if server_state != server_state_begin { server_state = server_state_helo } _, _ = writer.WriteString("250 2.0.0 Ok\r\n") _ = writer.Flush() default: _, _ = writer.WriteString("500 5.5.2 Error: command not recognized\r\n") _ = writer.Flush() } } }