package main import ( "bufio" "context" "fmt" "crypto/tls" "encoding/base64" "io" "net" "strings" "github.com/jackc/pgx/v5/pgxpool" "go.lindenii.runxiyu.org/lindenii-common/clog" ) type imap_recv_session struct { buf_conn *bufio.ReadWriter net_conn net.Conn tls_conn *tls.Conn my_server_name string db *pgxpool.Pool } const IMAP_CAPABILITIES = "CAPABILITY IMAP4rev2 AUTH=PLAIN" func (session *imap_recv_session) handle(ctx context.Context) error { session.buf_conn = bufio.NewReadWriter(bufio.NewReader(session.net_conn), bufio.NewWriter(session.net_conn)) _, _ = session.buf_conn.WriteString("* OK [" + IMAP_CAPABILITIES + "] " + VERSION + "\r\n") _ = session.buf_conn.Flush() for { line, err := session.buf_conn.ReadString('\n') if err != nil { if err == io.EOF { return err_connection_handler_eof } return err } tag, cmd, param, err := parse_imap_line(line) if err != nil { _, _ = session.buf_conn.WriteString(line + " BAD " + err.Error() + "\r\n") _ = session.buf_conn.Flush() continue } clog.Debug(fmt.Sprintf("tag=%#v, cmd=%#v, param=%#v", tag, cmd, param)) switch_cmd: switch cmd { case "CAPABILITY": _, _ = session.buf_conn.WriteString("* CAPABILITY " + IMAP_CAPABILITIES + "\r\n") _, _ = session.buf_conn.WriteString(tag + " OK CAPABILITY completed\r\n") _ = session.buf_conn.Flush() case "AUTHENTICATE": space_that_ends_the_method := strings.IndexByte(param, ' ') method := param[:space_that_ends_the_method] switch method { case "PLAIN": argument_base64 := param[space_that_ends_the_method+1:] if strings.IndexByte(argument_base64, ' ') != -1 { _, _ = session.buf_conn.WriteString(tag + " TODO too many parameters\r\n") _ = session.buf_conn.Flush() break switch_cmd } argument := make([]byte, base64.StdEncoding.DecodedLen(len(argument_base64))) _, err := base64.StdEncoding.Decode(argument, []byte(argument_base64)); if err != nil { _, _ = session.buf_conn.WriteString(tag + " TODO cannot decode base64\r\n") _ = session.buf_conn.Flush() break switch_cmd } clog.Debug(fmt.Sprintf("argumetn=%#v", string(argument))) default: _, _ = session.buf_conn.WriteString(line + " TODO i don't know this authentication method\r\n") _ = session.buf_conn.Flush() break switch_cmd } } } } func imap_new_session(ctx context.Context, net_conn net.Conn) error { session := imap_recv_session{ net_conn: net_conn, } return session.handle(ctx) } func serve_imap() { var imap_net, imap_addr string config_consistent_run(func() { imap_net = config.IMAP.Net imap_addr = config.IMAP.Addr }) listener, err := net.Listen(imap_net, imap_addr) if err != nil { clog.Fatal(1, "IMAP: Cannot listen: "+err.Error()) } defer listener.Close() clog.Info("IMAP: Listening via " + imap_net + " on " + imap_addr) for { conn, err := listener.Accept() if err != nil { clog.Error("IMAP: Cannot accept connection: " + err.Error()) } clog.Info("IMAP: Accepted connection from " + conn.RemoteAddr().String()) go func() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() err := imap_new_session(ctx, conn) if err != nil { if err == err_connection_handler_eof { clog.Info("IMAP: Connection for " + conn.RemoteAddr().String() + " closed with EOF") } else { clog.Error("IMAP: Connection handler for " + conn.RemoteAddr().String() + " returned error: " + err.Error()) } } else { clog.Info("IMAP: Connection for " + conn.RemoteAddr().String() + " closed gracefully") } }() } }