///|
priv enum Phase {
Normal
AppendWaiting
AppendReady
AuthWaiting
AuthReady
AuthCancelled
IdleWaiting
IdleActive
TlsHandshake
} derive(Debug, Eq)
///|
pub struct Session {
priv mut status : State
priv mut serial : Int
priv mut pending : (String, String)?
priv mut phase : Phase
priv mut literal_size : Int
priv decoder : Decoder
priv caps : Array[String]
priv mut tls_active : Bool
} derive(Debug)
///|
pub fn Session::new() -> Session {
{
status: Greeting,
serial: 0,
pending: None,
phase: Normal,
literal_size: 0,
decoder: Decoder::new(),
caps: [],
tls_active: false,
}
}
///|
pub fn Session::state(self : Session) -> State {
self.status
}
///|
pub fn Session::capabilities(self : Session) -> Array[String] {
self.caps.copy()
}
///|
pub fn Session::has_pending(self : Session) -> Bool {
self.pending != None
}
///|
pub fn Session::command(
self : Session,
command : String,
args : Array[String],
) -> String raise ImapError {
if self.status == Greeting ||
self.status == Closed ||
self.pending != None ||
self.phase == TlsHandshake {
raise Invalid("session not ready")
}
let verb = command.to_upper()
if verb == "STARTTLS" && self.tls_active {
raise Invalid("TLS is already active")
}
let tail = command_tail(self.status, verb, args)
if self.serial >= 1000000000 {
raise Invalid("session command limit")
}
self.serial += 1
let tag = "A" + self.serial.to_string()
self.pending = Some((tag, verb))
self.phase = match verb {
"APPEND" => AppendWaiting
"AUTHENTICATE" => AuthWaiting
"IDLE" => IdleWaiting
_ => Normal
}
if verb == "APPEND" {
self.literal_size = @strconv.parse_int(args[1]) catch { _ => 0 }
}
// A new SELECT/EXAMINE deselects the old mailbox even if the new selection fails.
if verb == "SELECT" || verb == "EXAMINE" {
self.status = Authenticated
}
tag + " " + verb + (if tail.is_empty() { "" } else { " " + tail }) + "\r\n"
}
///|
pub fn Session::append(
self : Session,
mailbox : String,
size : Int,
flags? : String = "",
) -> String raise ImapError {
if size < 0 {
raise Invalid("negative literal size")
}
self.command("APPEND", [mailbox, size.to_string(), flags])
}
///|
/// Send a synchronizing APPEND literal only after the server's continuation.
pub fn Session::literal(self : Session, bytes : Bytes) -> Bytes raise ImapError {
if self.status == Closed ||
self.phase != AppendReady ||
bytes.length() != self.literal_size {
raise Invalid("APPEND literal state/length")
}
self.phase = Normal
bytes + b"\r\n"
}
///|
/// A pre-encoded PLAIN response or '*' to cancel. Credentials are encoded by the host.
pub fn Session::authenticate_response(
self : Session,
base64 : String,
) -> String raise ImapError {
if self.status == Closed || self.phase != AuthReady {
raise Invalid("authentication continuation state")
}
if base64 != "*" {
if base64.is_empty() || base64.length() > 65536 || base64.length() % 4 != 0 {
raise Invalid("SASL response length")
}
let mut padding = 0
for c in base64.iter() {
if c == '=' {
padding += 1
} else if padding > 0 ||
!((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '+' ||
c == '/') {
raise Invalid("SASL response alphabet")
}
}
if padding > 2 {
raise Invalid("SASL response padding")
}
}
self.phase = if base64 == "*" { AuthCancelled } else { Normal }
base64 + "\r\n"
}
///|
pub fn Session::done(self : Session) -> String raise ImapError {
if self.status == Closed || self.phase != IdleActive {
raise Invalid("IDLE is not active")
}
self.phase = Normal
"DONE\r\n"
}
///|
fn Session::learn_capabilities(self : Session, line : String) -> Unit {
let upper = line.to_upper()
let values = if upper.has_prefix("* CAPABILITY ") || upper == "* CAPABILITY" {
words(upper)[2:].to_owned()
} else {
match upper.find("[CAPABILITY ") {
Some(start) => {
let rest = upper[start + 12:].to_owned()
match rest.find("]") {
Some(end) => words(rest[:end].to_owned())
None => return
}
}
None => return
}
}
self.caps.clear()
for cap in values {
if !self.caps.contains(cap) {
self.caps.push(cap)
}
}
}
///|
pub fn Session::feed(
self : Session,
bytes : Bytes,
) -> Array[Response] raise ImapError {
if self.status == Closed && self.pending == None {
raise Invalid("session closed")
}
errdefer {
self.status = Closed
self.pending = None
self.phase = Normal
}
if self.phase == TlsHandshake && !bytes.is_empty() {
raise Invalid("plaintext during TLS handshake")
}
let responses = self.decoder.feed(bytes)
for r in responses {
if self.phase == TlsHandshake {
raise Invalid("plaintext after STARTTLS completion")
}
let fields = words(r.line)
let code = response_word(r.line)
if self.status == Greeting {
if fields.length() < 2 || fields[0] != "*" || !r.literals.is_empty() {
raise Invalid("invalid greeting")
}
self.status = match code {
"OK" => NotAuthenticated
"PREAUTH" => Authenticated
"BYE" => Closed
_ => raise Invalid("invalid greeting")
}
self.learn_capabilities(r.line)
continue
}
if self.status == Closed {
match self.pending {
Some((_, "LOGOUT")) => ()
_ => raise Invalid("response after BYE")
}
}
if r.line.has_prefix("* ") {
if code == "BYE" {
self.status = Closed
}
if code == "PREAUTH" {
raise Invalid("PREAUTH after greeting")
}
// Only status and CAPABILITY responses can update advertised capabilities.
if code == "CAPABILITY" || code == "OK" {
self.learn_capabilities(r.line)
}
continue
}
if r.line == "+" || r.line.has_prefix("+ ") {
self.phase = match self.phase {
AppendWaiting => AppendReady
AuthWaiting => AuthReady
IdleWaiting => IdleActive
_ => raise Invalid("unexpected continuation")
}
continue
}
match self.pending {
None => raise Invalid("unsolicited tagged response")
Some((tag, cmd)) => {
if fields.length() < 2 || fields[0] != tag || !r.literals.is_empty() {
raise Invalid("tag mismatch")
}
if code != "OK" && code != "NO" && code != "BAD" {
raise Invalid("completion status")
}
if code == "OK" && self.phase != Normal {
raise Invalid("completion before continuation exchange finished")
}
if code == "OK" && self.status != Closed {
if cmd == "LOGIN" || cmd == "AUTHENTICATE" {
self.status = Authenticated
self.caps.clear()
}
if cmd == "SELECT" || cmd == "EXAMINE" {
self.status = Selected
}
if cmd == "CLOSE" || cmd == "UNSELECT" {
self.status = Authenticated
}
if cmd == "LOGOUT" {
self.status = Closed
}
}
self.learn_capabilities(r.line)
self.pending = None
if cmd == "STARTTLS" && code == "OK" {
// Capabilities in the unprotected completion are not trusted after TLS.
self.caps.clear()
self.phase = TlsHandshake
self.decoder.finish()
} else {
self.phase = Normal
}
}
}
}
responses
}
///|
pub fn Session::finish(self : Session) -> Unit raise ImapError {
errdefer {
self.status = Closed
self.pending = None
}
self.decoder.finish()
if self.pending != None ||
self.status == Greeting ||
self.phase == TlsHandshake {
raise Invalid("connection ended before command completion")
}
self.status = Closed
}
///|
/// The transport must verify the certificate and hostname before calling this method.
pub fn Session::tls_established(self : Session) -> Unit raise ImapError {
if self.status != NotAuthenticated || self.phase != TlsHandshake {
raise Invalid("TLS handshake state")
}
self.decoder.finish()
self.tls_active = true
self.caps.clear()
self.phase = Normal
}