///|
/// Execute `f`, retrying up to `max_retries` total attempts when it fails with
/// a retryable error (transport or transient SMTP failure). Permanent errors
/// (`SmtpPermanent`, `Auth`, `Config`, `ContentLength`, `Timeout`) are raised
/// immediately. This gives callers bounded retry with no external waiting
/// beyond a single scheduler pause between attempts.
pub async fn[T] with_retry(
  max_retries : Int,
  f : () -> T raise MailFailure,
) -> T raise MailFailure {
  let mut attempt = 0
  let mut value : T? = None
  while value is None {
    attempt += 1
    try {
      value = Some(f())
    } catch {
      MailFailure(err) => {
        if attempt >= max_retries || !is_retryable(err) {
          raise MailFailure(err)
        }
        @async.pause() catch {
          _ => ()
        }
      }
    }
  }
  value.unwrap()
}

///|
/// True when a failure is worth retrying.
pub fn is_retryable(err : MailError) -> Bool {
  match err.kind() {
    Transport => true
    SmtpTransient => true
    Io => true
    _ => false
  }
}