package smtp
import (
"context"
"fmt"
"net"
"net/smtp"
"strconv"
"time"
"github.com/Mzack9999/goja"
"github.com/praetorian-inc/fingerprintx/pkg/plugins"
"github.com/projectdiscovery/nuclei/v3/pkg/js/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
pluginsmtp "github.com/praetorian-inc/fingerprintx/pkg/plugins/services/smtp"
)
type (
SMTPResponse struct {
IsSMTP bool
Banner string
}
)
type (
Client struct {
nj *utils.NucleiJS
host string
port string
}
)
func NewSMTPClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object {
c := &Client{nj: utils.NewNucleiJS(runtime)}
c.nj.ObjectSig = "Client(host, port)"
host, _ := c.nj.GetArg(call.Arguments, 0).(string)
port, _ := c.nj.GetArg(call.Arguments, 1).(string)
c.nj.Require(host != "", "host cannot be empty")
c.nj.Require(port != "", "port cannot be empty")
portInt, err := strconv.Atoi(port)
c.nj.Require(err == nil && portInt > 0 && portInt < 65536, "port must be a valid number")
c.host = host
c.port = port
executionId := c.nj.ExecutionId()
c.nj.Require(protocolstate.IsHostAllowed(executionId, host+":"+port), protocolstate.ErrHostDenied.Msgf(host+":"+port).Error())
return utils.LinkConstructor(call, runtime, c)
}
func (c *Client) IsSMTP() (SMTPResponse, error) {
resp := SMTPResponse{}
c.nj.Require(c.host != "", "host cannot be empty")
c.nj.Require(c.port != "", "port cannot be empty")
timeout := 5 * time.Second
executionId := c.nj.ExecutionId()
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
return SMTPResponse{}, fmt.Errorf("dialers not initialized for %s", executionId)
}
conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", net.JoinHostPort(c.host, c.port))
if err != nil {
return resp, err
}
defer func() {
_ = conn.Close()
}()
smtpPlugin := pluginsmtp.SMTPPlugin{}
service, err := smtpPlugin.Run(conn, timeout, plugins.Target{Host: c.host})
if err != nil {
return resp, err
}
if service == nil {
return resp, nil
}
resp.Banner = service.Version
resp.IsSMTP = true
return resp, nil
}
func (c *Client) IsOpenRelay(msg *SMTPMessage) (bool, error) {
c.nj.Require(c.host != "", "host cannot be empty")
c.nj.Require(c.port != "", "port cannot be empty")
executionId := c.nj.ExecutionId()
dialer := protocolstate.GetDialersWithId(executionId)
if dialer == nil {
return false, fmt.Errorf("dialers not initialized for %s", executionId)
}
addr := net.JoinHostPort(c.host, c.port)
conn, err := dialer.Fastdialer.Dial(context.TODO(), "tcp", addr)
if err != nil {
return false, err
}
defer func() {
_ = conn.Close()
}()
client, err := smtp.NewClient(conn, c.host)
if err != nil {
return false, err
}
if err := client.Mail(msg.from); err != nil {
return false, err
}
if len(msg.to) == 0 || len(msg.to) > 1 {
return false, fmt.Errorf("invalid number of recipients: required 1, got %d", len(msg.to))
}
if err := client.Rcpt(msg.to[0]); err != nil {
return false, err
}
wc, err := client.Data()
if err != nil {
return false, err
}
_, err = wc.Write([]byte(msg.String()))
if err != nil {
return false, err
}
err = wc.Close()
if err != nil {
return false, err
}
err = client.Quit()
if err != nil {
return false, err
}
return true, nil
}
func (c *Client) SendMail(msg *SMTPMessage) (bool, error) {
c.nj.Require(c.host != "", "host cannot be empty")
c.nj.Require(c.port != "", "port cannot be empty")
var auth smtp.Auth
if msg.user != "" && msg.pass != "" {
auth = smtp.PlainAuth("", msg.user, msg.pass, c.host)
}
addr := net.JoinHostPort(c.host, c.port)
if err := smtp.SendMail(addr, auth, msg.from, msg.to, []byte(msg.String())); err != nil {
c.nj.Throw("failed to send mail with message(%s) got %v", msg.String(), err)
}
return true, nil
}