module smtp

/*
*
* smtp module
* Created by: nedimf (07/2020)
*/
import net
import encoding.base64
import strings
import time

const (
	recv_size  = 128
)

enum ReplyCode {
	ready      = 220
	close      = 221
	auth_ok    = 235
	action_ok  = 250
	mail_start = 354
}

pub enum BodyType {
	text
	html
}

pub struct Client {
mut:
	socket   net.Socket
pub:
	server   string
	port     int = 25
	username string
	password string
	from     string
pub mut:
    is_open  bool
}

pub struct Mail {
	from      string
	to        string
	cc        string
	bcc       string
	date      time.Time = time.now()
	subject   string
	body_type BodyType
	body      string
}

// new_client returns a new SMTP client and connects to it
pub fn new_client(config Client) ?Client {
	mut c := config
	c.reconnect()?
	return c
}

// reconnect reconnects to the SMTP server if the connection was closed
pub fn (mut c Client) reconnect() ? {
	if c.is_open { return error('Already connected to server') }

	socket := net.dial(c.server, c.port) or { return error('Connecting to server failed') }
	c.socket = socket

	c.expect_reply(.ready) or { return error('Received invalid response from server') }
	c.send_ehlo() or { return error('Sending EHLO packet failed') }
	c.send_auth() or { return error('Authenticating to server failed') }
	c.is_open = true
}

// send sends an email
pub fn (c Client) send(config Mail) ? {
	if !c.is_open { return error('Disconnected from server') }
	from := if config.from != '' { config.from } else { c.from }
	c.send_mailfrom(from) or { return error('Sending mailfrom failed') }
	c.send_mailto(config.to) or { return error('Sending mailto failed') }
	c.send_data() or { return error('Sending mail data failed') }
	c.send_body({ config | from: from }) or { return error('Sending mail body failed') }
}

// quit closes the connection to the server
pub fn (mut c Client) quit() ? {
	c.send_str('QUIT\r\n')
	c.expect_reply(.close)
	c.socket.close()?
	c.is_open = false
}

// expect_reply checks if the SMTP server replied with the expected reply code
fn (c Client) expect_reply(expected ReplyCode) ? {
	bytes, len := c.socket.recv(recv_size)

	str := tos(bytes, len).trim_space()
	$if smtp_debug? {
		eprintln('\n\n[RECV START]')
		eprint(str)
	}

	// Read remaining data in the socket
	if len >= recv_size {
		for {
			tbytes, tlen := c.socket.recv(recv_size)
			str2 := tos(tbytes, tlen)
			$if smtp_debug? { eprint(str2) }
			if tlen < recv_size { break }
		}
	}

	$if smtp_debug? {
		eprintln('\n[RECV END]')
	}

	if len >= 3 {
		status := str[..3].int()
		if status != expected {
			return error('Received unexpected status code $status, expecting $expected')
		}
	} else { return error('Recieved unexpected SMTP data: $str') }
}

[inline]
fn (c Client) send_str(s string) ? {
	$if smtp_debug? {
		eprintln('\n\n[SEND START]')
		eprint(s.trim_space())
		eprintln('\n[SEND END]')
	}
	c.socket.send_string(s)?
}

[inline]
fn (c Client) send_ehlo() ? {
	c.send_str('EHLO $c.server\r\n')?
	c.expect_reply(.action_ok)?
}

[inline]
fn (c Client) send_auth() ? {
	if c.username.len == 0 {
		return
	}    
	mut sb := strings.new_builder(100)
	sb.write_b(0)
	sb.write(c.username)
	sb.write_b(0)
	sb.write(c.password)
	a := sb.str()
	auth := 'AUTH PLAIN ${base64.encode(a)}\r\n'
	c.send_str(auth)?
	c.expect_reply(.auth_ok)?
}

fn (c Client) send_mailfrom(from string) ? {
	c.send_str('MAIL FROM: <$from>\r\n')?
	c.expect_reply(.action_ok)?
}

fn (c Client) send_mailto(to string) ? {
	c.send_str('RCPT TO: <$to>\r\n')?
	c.expect_reply(.action_ok)?
}

fn (c Client) send_data() ? {
	c.send_str('DATA\r\n')?
	c.expect_reply(.mail_start)
}

fn (c Client) send_body(cfg Mail) ? {
	is_html := cfg.body_type == .html
	date := cfg.date.utc_string().trim_right(' UTC') // TODO
	mut sb := strings.new_builder(200)
	sb.write('From: $cfg.from\r\n')
	sb.write('To: <$cfg.to>\r\n')
	sb.write('Cc: <$cfg.cc>\r\n')
	sb.write('Bcc: <$cfg.bcc>\r\n')
	sb.write('Date: $date\r\n')
	sb.write('Subject: $cfg.subject\r\n')
	if is_html {
		sb.write('Content-Type: text/html; charset=ISO-8859-1')
	}
	sb.write('\r\n\r\n')
	sb.write(cfg.body)
	sb.write('\r\n.\r\n')
	c.send_str(sb.str())?
	c.expect_reply(.action_ok)?
}