diff options
author | Runxi Yu <me@runxiyu.org> | 2025-01-12 13:40:11 +0800 |
---|---|---|
committer | Runxi Yu <me@runxiyu.org> | 2025-01-12 13:40:11 +0800 |
commit | 3fec571183461c91a52a64f008cb0dacd194443d (patch) | |
tree | 16bd9325d2e24546f138b12ccce0940cb4a935a1 | |
parent | Use bufio.ReadWriter (diff) | |
download | maild-3fec571183461c91a52a64f008cb0dacd194443d.tar.gz maild-3fec571183461c91a52a64f008cb0dacd194443d.tar.zst maild-3fec571183461c91a52a64f008cb0dacd194443d.zip |
Add incoming STARTTLS support
-rw-r--r-- | config.go | 10 | ||||
-rw-r--r-- | main.go | 2 | ||||
-rw-r--r-- | mta_recv.go | 98 |
3 files changed, 65 insertions, 45 deletions
@@ -2,6 +2,7 @@ package main import ( "bufio" + "crypto/tls" "os" "sync" @@ -15,6 +16,7 @@ var config struct { Cert string `scfg:"cert"` Key string `scfg:"key"` } `scfg:"tls"` + _tls_config *tls.Config } var config_mutex sync.RWMutex @@ -31,6 +33,14 @@ func load_config(path string) error { if err != nil { return err } + cer, err := tls.LoadX509KeyPair(config.TLS.Cert, config.TLS.Key) + if err != nil { + return err + } + config._tls_config = &tls.Config{ + Certificates: []tls.Certificate{cer}, + MinVersion: tls.VersionTLS13, + } return nil }() != nil { return err @@ -32,7 +32,7 @@ func main() { } go func() { - err := handle_incoming_server_connection(bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))) + err := handle_incoming_server_connection(bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)), &conn) if err != nil && !errors.Is(err, io.EOF) { clog.Error("connection handler returned error", "err", err) } diff --git a/mta_recv.go b/mta_recv.go index 1f680fa..3f7f473 100644 --- a/mta_recv.go +++ b/mta_recv.go @@ -3,6 +3,8 @@ package main import ( "bufio" "bytes" + "crypto/tls" + "net" "slices" "strings" @@ -18,21 +20,24 @@ const ( server_state_rcpt ) -func handle_incoming_server_connection(conn *bufio.ReadWriter) error { +func handle_incoming_server_connection(buf_conn *bufio.ReadWriter, net_conn *net.Conn) error { + var tls_conn *tls.Conn var my_server_name string var routes map[string]string + var tls_config *tls.Config config_consistent_run(func() { my_server_name = config.Server_name routes = config.Routes + tls_config = config._tls_config }) - _, _ = conn.WriteString("220 " + my_server_name + " " + VERSION + "\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("220 " + my_server_name + " " + VERSION + "\r\n") + _ = buf_conn.Flush() server_state := server_state_begin var remote_server_name string var current_mail_from string var current_rcpt_to []string for { - line, err := conn.ReadString('\n') + line, err := buf_conn.ReadString('\n') if err != nil { return err } @@ -50,79 +55,84 @@ func handle_incoming_server_connection(conn *bufio.ReadWriter) error { param := line[param_start:] switch_cmd: switch cmd { + case "STARTTLS": + _, _ = buf_conn.WriteString("220 2.0.0 Ready to start TLS\r\n") + _ = buf_conn.Flush() + tls_conn = tls.Server(*net_conn, tls_config) + buf_conn = bufio.NewReadWriter(bufio.NewReader(tls_conn), bufio.NewWriter(tls_conn)) case "HELO": if param == "" { // TODO: actually validate the hostname - _, _ = conn.WriteString("501 Syntax: HELO hostname\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("501 Syntax: HELO hostname\r\n") + _ = buf_conn.Flush() break } remote_server_name = param _ = remote_server_name // TODO server_state = server_state_helo - _, _ = conn.WriteString("250 " + my_server_name + "\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("250 " + my_server_name + "\r\n") + _ = buf_conn.Flush() case "MAIL": switch server_state { case server_state_begin: - _, _ = conn.WriteString("503 5.5.1 Error: send HELO/EHLO first\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("503 5.5.1 Error: send HELO/EHLO first\r\n") + _ = buf_conn.Flush() break switch_cmd case server_state_helo: break case server_state_mail: - _, _ = conn.WriteString("503 5.5.1 Error: nested MAIL command\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("503 5.5.1 Error: nested MAIL command\r\n") + _ = buf_conn.Flush() break switch_cmd } if len(param) <= len("FROM:") || strings.ToUpper(param[:len("FROM:")]) != "FROM:" { - _, _ = conn.WriteString("501 5.5.4 Syntax: MAIL FROM:<address>\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("501 5.5.4 Syntax: MAIL FROM:<address>\r\n") + _ = buf_conn.Flush() break } current_mail_from = param[len("FROM:"):] current_rcpt_to = []string{} server_state = server_state_mail - _, _ = conn.WriteString("250 2.1.0 Ok\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("250 2.1.0 Ok\r\n") + _ = buf_conn.Flush() // TODO: Address validation case "RCPT": if server_state != server_state_mail && server_state != server_state_rcpt { - _, _ = conn.WriteString("503 5.5.1 Error: need MAIL command\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("503 5.5.1 Error: need MAIL command\r\n") + _ = buf_conn.Flush() break } if len(param) <= len("TO:") || strings.ToUpper(param[:len("TO:")]) != "TO:" { - _, _ = conn.WriteString("501 5.5.4 Syntax: RCPT TO:<address>\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("501 5.5.4 Syntax: RCPT TO:<address>\r\n") + _ = buf_conn.Flush() break } recipient, _, _ := mailkit.Strip_angle_brackets(param[len("TO:"):]) _, ok := routes[recipient] if !ok { - _, _ = conn.WriteString("550 5.1.1 <" + recipient + ">: Recipient address rejected: User unknown in local recipient table\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("550 5.1.1 <" + recipient + ">: Recipient address rejected: User unknown in local recipient table\r\n") + _ = buf_conn.Flush() break switch_cmd } current_rcpt_to = append(current_rcpt_to, recipient) server_state = server_state_rcpt - _, _ = conn.WriteString("250 2.1.5 Ok\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("250 2.1.5 Ok\r\n") + _ = buf_conn.Flush() case "DATA": if server_state != server_state_rcpt { - _, _ = conn.WriteString("503 5.5.1 Error: need RCPT command\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("503 5.5.1 Error: need RCPT command\r\n") + _ = buf_conn.Flush() break } - _, _ = conn.WriteString("354 End data with <CR><LF>.<CR><LF>\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("354 End data with <CR><LF>.<CR><LF>\r\n") + _ = buf_conn.Flush() var current_data []byte for { - tmp, err := conn.ReadSlice('\r') + tmp, err := buf_conn.ReadSlice('\r') if err != nil { return err } - // conn.ReadSlice returns an internal buffer that gets + // buf_conn.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 @@ -130,7 +140,7 @@ func handle_incoming_server_connection(conn *bufio.ReadWriter) error { data_part := make([]byte, len(tmp)) copy(data_part, tmp) - next_four, err := conn.Peek(4) + next_four, err := buf_conn.Peek(4) if err != nil { return err } @@ -140,7 +150,7 @@ func handle_incoming_server_connection(conn *bufio.ReadWriter) error { } current_data = slices.Concat(current_data, data_part) } - _, err := conn.Discard(4) + _, err := buf_conn.Discard(4) if err != nil { return err } @@ -149,7 +159,7 @@ func handle_incoming_server_connection(conn *bufio.ReadWriter) error { for _, recipient := range current_rcpt_to { inbox, ok := routes[recipient] if !ok { - _, _ = conn.WriteString("550 5.1.1 <" + recipient + ">: Recipient address rejected: User unknown in local recipient table\r\n") + _, _ = buf_conn.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{}{} @@ -159,28 +169,28 @@ func handle_incoming_server_connection(conn *bufio.ReadWriter) error { } } if err == nil { - _, _ = conn.WriteString("250 2.0.0 Ok: Accepted\r\n") + _, _ = buf_conn.WriteString("250 2.0.0 Ok: Accepted\r\n") } else { - _, _ = conn.WriteString("500 2.0.0 Error: " + err.Error() + "\r\n") + _, _ = buf_conn.WriteString("500 2.0.0 Error: " + err.Error() + "\r\n") } - _ = conn.Flush() + _ = buf_conn.Flush() server_state = server_state_helo case "QUIT": - _, _ = conn.WriteString("221 2.0.0 Bye\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("221 2.0.0 Bye\r\n") + _ = buf_conn.Flush() return nil case "NOOP": - _, _ = conn.WriteString("250 2.0.0 Ok\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("250 2.0.0 Ok\r\n") + _ = buf_conn.Flush() case "RSET": if server_state != server_state_begin { server_state = server_state_helo } - _, _ = conn.WriteString("250 2.0.0 Ok\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("250 2.0.0 Ok\r\n") + _ = buf_conn.Flush() default: - _, _ = conn.WriteString("500 5.5.2 Error: command not recognized\r\n") - _ = conn.Flush() + _, _ = buf_conn.WriteString("500 5.5.2 Error: command not recognized\r\n") + _ = buf_conn.Flush() } } } |