///|
/// One step of a FakeTransport script: the client command to expect (verbatim,
/// or `""` for a spontaneous server reply such as the greeting, or `"*"` to
/// accept any line) and the reply lines to send back.
pub struct FakeStep {
expect : String
send : Array[String]
} derive(Eq, Debug)
///|
pub fn FakeStep::new(expect : String, send : Array[String]) -> FakeStep {
{ expect, send }
}
///|
pub fn FakeStep::greeting(send : Array[String]) -> FakeStep {
{ expect: "", send }
}
///|
pub fn FakeStep::reply(expect : String, send : Array[String]) -> FakeStep {
{ expect, send }
}
///|
/// A deterministic, scripted SMTP server used for offline testing. It
/// implements `SmtpTransport` and records every client command so tests can
/// assert the exact command sequence and the message body transmitted.
pub struct FakeTransport {
script : Array[FakeStep]
mut step_index : Int
mut reply_queue : Array[String]
mut expecting : String
mut data_mode : Bool
received : Array[String]
data_lines : Array[String]
mut connected : Bool
mut failure : String?
}
///|
pub fn FakeTransport::new(script : Array[FakeStep]) -> FakeTransport {
{
script,
step_index: 0,
reply_queue: [],
expecting: "",
data_mode: false,
received: [],
data_lines: [],
connected: false,
failure: None,
}
}
///|
/// Every client command line received (verbatim).
pub fn FakeTransport::received(self : FakeTransport) -> Array[String] {
self.received
}
///|
/// The message body lines received during the DATA phase.
pub fn FakeTransport::data_lines(self : FakeTransport) -> Array[String] {
self.data_lines
}
///|
/// The first script mismatch description, if any.
pub fn FakeTransport::failure(self : FakeTransport) -> String? {
self.failure
}
///|
/// Whether the transport is connected.
pub fn FakeTransport::is_connected(self : FakeTransport) -> Bool {
self.connected
}
///|
/// True when every script step was consumed (the conversation completed).
pub fn FakeTransport::is_complete(self : FakeTransport) -> Bool {
self.step_index >= self.script.length()
}
///|
/// Queue spontaneous (greeting) steps and set the next expected command.
fn FakeTransport::advance(self : FakeTransport) -> Unit {
while self.step_index < self.script.length() {
let step = self.script[self.step_index]
if step.expect == "" {
self.reply_queue = self.reply_queue + step.send
self.step_index += 1
} else {
self.expecting = step.expect
return
}
}
self.expecting = ""
}
///|
/// Match a client command against the current script step and queue the reply.
fn FakeTransport::match_command(
self : FakeTransport,
line : String,
) -> Unit raise MailFailure {
if self.expecting == "" {
self.failure = Some("unexpected command '\{line}' (script exhausted)")
raise MailFailure::transport("FakeTransport: unexpected command '\{line}'")
}
if self.expecting != "*" && self.expecting != line {
self.failure = Some("expected '\{self.expecting}' but got '\{line}'")
raise MailFailure::transport(
"FakeTransport: expected '\{self.expecting}' but got '\{line}'",
)
}
let step = self.script[self.step_index]
self.reply_queue = self.reply_queue + step.send
if step.expect == "DATA" &&
step.send.length() > 0 &&
step.send[0].has_prefix("354") {
self.data_mode = true
}
self.step_index += 1
self.advance()
}
///|
pub impl SmtpTransport for FakeTransport with fn connect(self, _config) {
@async.pause() catch {
_ => ()
}
if self.script.is_empty() {
raise MailFailure::transport("FakeTransport: empty script")
}
self.connected = true
self.step_index = 0
self.reply_queue = []
self.data_mode = false
self.expecting = ""
self.advance()
}
///|
pub impl SmtpTransport for FakeTransport with fn write_line(self, line) {
@async.pause() catch {
_ => ()
}
self.received.push(line)
if self.data_mode {
if line == "." {
self.data_mode = false
self.match_command(line)
} else {
self.data_lines.push(line)
}
return
}
self.match_command(line)
}
///|
pub impl SmtpTransport for FakeTransport with fn read_line(self) {
@async.pause() catch {
_ => ()
}
if self.reply_queue.is_empty() {
self.failure = Some("read on empty reply queue")
raise MailFailure::transport("FakeTransport: reply queue exhausted")
}
let line = self.reply_queue[0]
let _ = self.reply_queue.remove(0)
line
}
///|
pub impl SmtpTransport for FakeTransport with fn close(self) {
self.connected = false
}