package main
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"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, imap_trans string
var tls_config *tls.Config
config_consistent_run(func() {
imap_net = config.IMAP.Net
imap_addr = config.IMAP.Addr
imap_trans = config.IMAP.Trans
tls_config = config._tls_config
})
var listener net.Listener
var err error
switch imap_trans {
case "tls", "":
listener, err = tls.Listen(imap_net, imap_addr, tls_config)
if err != nil {
clog.Fatal(1, "IMAP: Cannot listen TLS: "+err.Error())
}
case "plain":
listener, err = net.Listen(imap_net, imap_addr)
if err != nil {
clog.Fatal(1, "IMAP: Cannot listen plain: "+err.Error())
}
default:
clog.Fatal(1, "IMAP: Invalid transport for listening")
}
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")
}
}()
}
}