Smtp client notify

use "net"
use "email"
use "debug"
use "buffered"

use @exit[None](x: I32)
use @printf[U32](fmt: Pointer[U8] tag, ...)

class SMTPClientNotify is TCPConnectionNotify
  """
  This class is the center of the SMTP client.  It contains all the logic
  that is used to manage the connection.

  The current API is intended to provide maximum instrumentation until we
  have fully explored all the odd corners in the implementation.

  Fundamentally, this Notifier takes an SMTPConfiguration val (which
  contains all the relay server configuration) and an EMail val

  As the SMTP transaction is performed, a log of the session is created.
  Once complete the EMail object is returned to the client with the
  session-log.
  """

  var client_state: SMTPClientState = SMTPClientStateNoConnection
  let config: SMTPConfiguration val
  let reader: Reader = Reader
  let outgoingreader: Reader = Reader
  var email: EMail val
  var rcpttos: Array[String] = []
  var currentto: String val = ""
  var sessionlog: Reader iso = recover iso Reader end
  var status: Bool = false

  new create(config': SMTPConfiguration val, email': EMail val) =>
    config = config'
    email = consume email'

    for to in email.to'.values() do
      rcpttos.push(to)
    end
    for cc in email.cc'.values() do
      rcpttos.push(cc)
    end
    for bcc in email.bcc'.values() do
      rcpttos.push(bcc)
    end

  fun ref connect_failed(conn: TCPConnection ref) =>
    var tsessionlog: Reader iso = sessionlog = recover iso Reader end
    config.callback(status, email, consume tsessionlog)

  fun ref connected(conn: TCPConnection ref) =>

    debug("←→ Connection Established with " + config.destination)
  fun ref sent(conn: TCPConnection ref, data: ByteSeq): ByteSeq =>
    outgoingreader.append(data)
    try
      while true do
        debug("→ " + outgoingreader.line()?)
      end
    end
    data

  fun ref sentv(conn: TCPConnection ref, data: ByteSeqIter): ByteSeqIter => data
  fun ref received(conn: TCPConnection ref, data: Array[U8] iso, times: USize): Bool =>
    reader.append(consume data)
    match client_state
    | let x: SMTPClientStateNoConnection => recv_noconnection(conn)
    | let x: SMTPClientStateConnected => recv_connected(conn)
    | let x: SMTPClientStateAcceptedEHLO => recv_accepted_ehlo(conn)
    | let x: SMTPClientStateSendingRcptTo => recv_sending_rcpt_to(conn)
    | let x: SMTPClientStateReadyForMessage => recv_ready_for_message(conn)
    | let x: SMTPClientStatePendingOK => recv_pending_ok(conn)
    else
      try
        while true do
          let line: String val = reader.line()?
          debug("→ " + line)
        end
      else
        debug("→ reader is empty ←")
      end
    end
    true
  fun ref closed(conn: TCPConnection ref) =>
    var tsessionlog: Reader iso = sessionlog = recover iso Reader end
    config.callback(status, email, consume tsessionlog)

  fun ref recv_pending_ok(conn: TCPConnection ref) =>
    try
      let line: String val = reader.line()?
      debug("← " + line)
      debug("→ None ←")
      client_state = None
      status = true
      conn.write("QUIT\r\n")
    end

  fun ref recv_ready_for_message(conn: TCPConnection ref) =>
    try
      let response: String val = reader.line()?
      debug("← " + response)
      conn.write(email.render())
      conn.write(".\r\n")
      client_state = SMTPClientStatePendingOK
      debug("→ SMTPClientStatePendingOK ←")
    end



  fun ref recv_sending_rcpt_to(conn: TCPConnection ref) =>
    try
      let response: String val = reader.line()?
      debug("← " + response + " <<<<" + currentto + ">>>>")
      if (response.at_offset(0)? != '2') then
        conn.hard_close()
        client_state = None
      end
      if (rcpttos.size() == 0) then
        client_state = SMTPClientStateReadyForMessage
        debug("→ SMTPClientStateReadyForMessage ←")

        conn.write("DATA\r\n")
        return
      end
      currentto = rcpttos.pop()?
      conn.write("RCPT TO: " + currentto + "\r\n")
    end

  fun ref recv_accepted_ehlo(conn: TCPConnection ref) =>
    try
      let response: String val = reader.line()?
      debug("← " + response + " <<<<(for Mail from)>>>>")
      debug("→ SMTPClientStateSendingRcptTo ←")

      if (rcpttos.size() > 0) then
        currentto = rcpttos.pop()?
        conn.write("RCPT TO: " + currentto + "\r\n")
        client_state = SMTPClientStateSendingRcptTo
      else
        conn.write("QUIT\r\n")
        client_state = None
      end

    end

  fun ref recv_connected(conn: TCPConnection ref) =>
    try
      while true do
        let line: String val = reader.line()?
        debug("← " + line)
        if (line.at(" ", 3)) then
          debug("→ SMTPClientStateAcceptedEHLO ←")
          conn.write("MAIL FROM: " + email.from' + "\r\n")
          client_state = SMTPClientStateAcceptedEHLO
          break
        end
      end
    end



  fun ref recv_noconnection(conn: TCPConnection ref) =>
    try
      let line: String val = reader.line()?
      debug("← " + line)
    end
    conn.write("EHLO " + config.mydomain + "\r\n")
    debug("→ SMTPClientStateConnected ←")
    client_state = SMTPClientStateConnected

  fun ref debug(data: String val) =>
    sessionlog.append(data + "\n")
 //   Debug.out(data)


  fun die(err: String val = "", loc: SourceLoc val = __loc) =>
    @printf("%s\n".cstring(), err.cstring())
    @printf("FATAL: %s:%s\n".cstring(), loc.file().cstring(), loc.line().string().cstring())
//    @exit(-1)