104 lines
4.8 KiB
Go
104 lines
4.8 KiB
Go
package services
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/base64"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func NewSMTPHandler(log LoggerFunc) Handler {
|
|
return func(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
_, _ = conn.Write([]byte("220 mail.example.com ESMTP Postfix\r\n"))
|
|
conn.SetDeadline(time.Now().Add(5 * time.Minute))
|
|
scanner := bufio.NewScanner(conn)
|
|
var mailFrom, rcptTo string
|
|
authLoginStage := 0 // 0=none,1=expect username,2=expect password (both base64)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" { continue }
|
|
parts := strings.SplitN(line, " ", 2)
|
|
cmd := strings.ToUpper(parts[0])
|
|
arg := ""; if len(parts)>1 { arg = parts[1] }
|
|
|
|
if authLoginStage == 1 { // expecting base64 username
|
|
userBytes, _ := base64.StdEncoding.DecodeString(line)
|
|
user := string(userBytes)
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "smtp", Details: map[string]string{"event":"auth_login_username","username":user}})
|
|
_, _ = conn.Write([]byte("334 UGFzc3dvcmQ6\r\n")) // "Password:" base64
|
|
authLoginStage = 2
|
|
continue
|
|
}
|
|
if authLoginStage == 2 { // expecting base64 password
|
|
passBytes, _ := base64.StdEncoding.DecodeString(line)
|
|
pass := string(passBytes)
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "smtp", Details: map[string]string{"event":"auth_login_password","password":pass}})
|
|
_, _ = conn.Write([]byte("535 Authentication failed\r\n"))
|
|
authLoginStage = 0
|
|
continue
|
|
}
|
|
|
|
switch cmd {
|
|
case "HELO":
|
|
_, _ = conn.Write([]byte("250 mail.example.com\r\n"))
|
|
case "EHLO":
|
|
_, _ = conn.Write([]byte("250-mail.example.com\r\n250-AUTH PLAIN LOGIN\r\n250 OK\r\n"))
|
|
case "AUTH":
|
|
fields := strings.Fields(arg)
|
|
if len(fields) > 0 {
|
|
method := strings.ToUpper(fields[0])
|
|
switch method {
|
|
case "PLAIN":
|
|
if len(fields) > 1 {
|
|
if b, err := base64.StdEncoding.DecodeString(fields[1]); err == nil {
|
|
p := strings.Split(string(b), "\x00")
|
|
if len(p) >= 3 {
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "smtp", Details: map[string]string{"event":"auth_attempt","method":"PLAIN","username":p[1],"password":p[2]}})
|
|
}
|
|
}
|
|
} else {
|
|
// prompt for base64 blob
|
|
_, _ = conn.Write([]byte("334 \r\n"))
|
|
continue
|
|
}
|
|
_, _ = conn.Write([]byte("535 Authentication failed\r\n"))
|
|
case "LOGIN":
|
|
// 334 Username:
|
|
_, _ = conn.Write([]byte("334 VXNlcm5hbWU6\r\n"))
|
|
authLoginStage = 1
|
|
default:
|
|
_, _ = conn.Write([]byte("504 Authentication method not supported\r\n"))
|
|
}
|
|
}
|
|
case "MAIL":
|
|
mailFrom = arg
|
|
_, _ = conn.Write([]byte("250 OK\r\n"))
|
|
case "RCPT":
|
|
rcptTo = arg
|
|
_, _ = conn.Write([]byte("250 OK\r\n"))
|
|
case "DATA":
|
|
_, _ = conn.Write([]byte("354 End data with <CR><LF>.<CR><LF>\r\n"))
|
|
// read until single '.' on a line
|
|
var bodyLines []string
|
|
for scanner.Scan() {
|
|
l := scanner.Text()
|
|
if l == "." { break }
|
|
bodyLines = append(bodyLines, l)
|
|
}
|
|
// log summary
|
|
snippet := strings.Join(bodyLines, "\n")
|
|
if len(snippet) > 500 { snippet = snippet[:500] }
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "smtp", Details: map[string]string{"event":"message","mail_from":mailFrom,"rcpt_to":rcptTo}, RawPayload: snippet})
|
|
_, _ = conn.Write([]byte("250 OK queued as 12345\r\n"))
|
|
case "QUIT":
|
|
_, _ = conn.Write([]byte("221 Bye\r\n")); return
|
|
default:
|
|
_, _ = conn.Write([]byte("502 Command not implemented\r\n"))
|
|
}
|
|
}
|
|
}
|
|
}
|