///|
/// The high-level SMTP client: drives a full session over a `SmtpTransport`
/// from a rendered message and an envelope, and returns the diagnostic trace.
pub struct SmtpClient[T] {
  session : SmtpSession[T]
}

///|
pub fn[T] SmtpClient::new(transport : T, config : SmtpConfig) -> SmtpClient[T] {
  { session: SmtpSession::new(transport, config) }
}

///|
/// Access the underlying session (and through it the transport), for
/// inspection in tests and tooling.
pub fn[T] SmtpClient::session(self : SmtpClient[T]) -> SmtpSession[T] {
  self.session
}

///|
/// Access the transport captured inside the client (post-send state).
pub fn[T] SmtpClient::transport(self : SmtpClient[T]) -> T {
  self.session.transport
}

///|
/// Render a `MailMessage` and send it over the session.
pub async fn[T : SmtpTransport] SmtpClient::send(
  self : SmtpClient[T],
  message : MailMessage,
  envelope : Envelope,
  render : RenderConfig,
) -> SmtpTrace raise MailFailure {
  let wire = render_message(message, render)
  self.send_rendered(wire, envelope)
}

///|
/// Send an already rendered wire message (CRLF line endings) over the session.
/// The message is dot-stuffed and line-length checked before transmission.
pub async fn[T : SmtpTransport] SmtpClient::send_rendered(
  self : SmtpClient[T],
  wire : String,
  envelope : Envelope,
) -> SmtpTrace raise MailFailure {
  self.session.connect()
  self.session.ehlo()
  match self.session.config.auth {
    Some(a) => self.session.auth(a)
    None => ()
  }
  // enforce the server SIZE limit and the RFC 5322 998-octet line limit
  // before starting the mail transaction
  let caps = self.session.caps()
  match caps.size {
    Some(max) =>
      if string_to_bytes(wire).length() > max {
        raise MailFailure::content_length(
          "message size \{string_to_bytes(wire).length()} exceeds server SIZE limit \{max}",
        )
      }
    None => ()
  }
  check_line_lengths(wire, 998)
  self.session.mail_from(envelope.mail_from())
  for path in envelope.rcpt_to() {
    self.session.rcpt_to(path)
  }
  let stuffed = dot_stuff(normalize_crlf(wire))
  let lines = to_lines(stuffed)
  self.session.send_data(lines)
  self.session.quit()
  self.session.trace()
}