package mail
import (
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/Queueue0/jums/internal/config"
"github.com/Queueue0/jums/internal/smtp/packets"
)
const (
initialTimeout = 5 * time.Minute
mailTimeout = 5 * time.Minute
rcptTimeout = 5 * time.Minute
dataInitTimeout = 2 * time.Minute
dataBlockTimeout = 3 * time.Minute
dataTerminationTimeout = 10 * time.Minute
)
func createSMTPConn(domain string) (net.Conn, error) {
mxrs, err := net.LookupMX(domain)
if err != nil {
return nil, err
}
fmt.Printf("Found mail servers: %v\n", mxrs)
var c net.Conn
MXLoop:
for i, mx := range mxrs {
c, err = connectStartTLS(mx.Host, ":25")
if err != nil {
fmt.Printf("Couldn't establish connection: %s\n", err.Error())
if i == len(mxrs)-1 {
return nil, err
}
} else {
break MXLoop
}
}
return c, nil
}
// read a potential multi-line response
func readResponse(c net.Conn) ([]byte, error) {
read := []byte{}
lastLine := false
for !lastLine {
line := []byte{}
for len(line) < 2 || string(line[len(line)-2:]) != "\r\n" {
next := make([]byte, 1)
_, err := c.Read(next)
if err != nil {
return nil, err
}
line = append(line, next...)
}
if line[3] == byte(' ') {
lastLine = true
}
read = append(read, line...)
}
fmt.Println(string(read))
return read, nil
}
// Attempts to connect and elevate to TLS with starttls
// If the server doesn't support starttls it will return a non-encrypted connection
func connectStartTLS(domain, port string) (net.Conn, error) {
addr := domain + port
fmt.Printf("dialing %s...\n", addr)
c, err := net.DialTimeout("tcp", addr, initialTimeout)
if err != nil {
return nil, errors.New("connectStartTLS: " + err.Error())
}
fmt.Println("reading status...")
stat, err := readAndParseStatus(c)
if err != nil {
c.Close()
return nil, errors.New("connectStartTLS: " + err.Error())
}
if stat.Code() != 220 {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: non-220 connection response: " + stat.String())
}
conf := config.GetConfig()
if err = packets.NewCommand("EHLO", conf.Mxdomain).Send(c); err != nil {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: " + err.Error())
}
stat, err = readAndParseStatus(c)
if err != nil {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: " + err.Error())
}
if stat.Code() != 250 {
// If EHLO unrecognized
if stat.Code() == 500 {
err = packets.NewCommand("HELO", conf.Mxdomain).Send(c)
if err != nil {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: " + err.Error())
}
stat, err = readAndParseStatus(c)
if err != nil {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: " + err.Error())
}
if stat.Code() != 250 {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: couldn't greet server: " + stat.String())
}
return c, nil
}
return nil, errors.New("connectStartTLS: couldn't greet server: " + stat.String())
}
if strings.Contains(strings.ToUpper(stat.String()), "STARTTLS") {
err = packets.NewCommand("STARTTLS").Send(c)
if err != nil {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: " + err.Error())
}
stat, err := readAndParseStatus(c)
if err != nil {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: " + err.Error())
}
if stat.Code() != 220 {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: unexpected response to STARTTLS: " + stat.String())
}
c = tls.Client(c, &tls.Config{ServerName: domain})
err = c.(*tls.Conn).Handshake()
if err != nil {
c.Close()
return nil, errors.New("connectStartTLS: handshake error: " + err.Error())
}
err = packets.NewCommand("EHLO", conf.Mxdomain).Send(c)
if err != nil {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: post-handshake greeting error: " + err.Error())
}
stat, err = readAndParseStatus(c)
if err != nil {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: " + err.Error())
}
if stat.Code() != 250 {
packets.NewCommand("QUIT").Send(c)
c.Close()
return nil, errors.New("connectStartTLS: unexpected response to post-handshake greeting: " + stat.String())
}
}
return c, nil
}
func readAndParseStatus(c net.Conn) (*packets.Status, error) {
b, err := readResponse(c)
if err != nil {
return nil, errors.New("readAndParseStatus: error reading response: " + err.Error())
}
stat, err := packets.ParseStatus(b)
if err != nil {
return nil, errors.New("readAndParseStatus: error parsing status: " + err.Error())
}
return stat, nil
}