// diagnose.mbt — structural and semantic checks above the grammar.
//
// RFC 4475 asks for two different kinds of judgement, and conflating them is the
// usual mistake:
//
//   * §3.1.2.x  messages are syntactically invalid  -> the grammar layer must
//     fail to parse them.
//   * §3.3.x    messages are syntactically valid but semantically rejectable
//     -> the grammar layer must succeed, and the problem must be reported here.
//
// Every rule below cites the torture case it exists for, so the test report in
// `tests/rfc4475` maps one-to-one onto the specification text.

///|
pub enum Severity {
  Error
  Warning
}

///|
pub extend Severity with Show::{to_string, output}

///|
pub impl Show for Severity with fn to_string(self : Severity) -> String {
  match self {
    Error => "error"
    Warning => "warning"
  }
}

///|
/// Diagnostic codes. Deliberately fine-grained: a coarse "invalid message" verdict
/// cannot show a reviewer which rule fired.
pub(all) enum DiagCode {
  MissingRequiredHeader
  MissingViaBranch
  CSeqMethodMismatch
  ContentLengthMismatch
  MultipleContentLengthValues
  DuplicateSingleValueHeader
  UnsupportedVersion
  StatusCodeOutOfRange
  ScalarOutOfRange
  UnknownRequestUriScheme
  TrailingOctets
  ExtraneousSeparator
  UnbracketedUriWithHeaders
  MissingContactInDialogRequest
  UnsupportedExtension
  EscapedHeadersInRequestUri
  MalformedDate
  MalformedFieldValue
  InvalidUtf8
}

///|
/// Structural equality by discriminant. Derive(Eq) does not take effect on
/// this toolchain, so the comparison is written out variant by variant --
/// verbose, but nothing left to go subtly wrong.
impl Eq for DiagCode with fn equal(self : DiagCode, that : DiagCode) -> Bool {
  (self is MissingRequiredHeader && that is MissingRequiredHeader) ||
  (self is MissingViaBranch && that is MissingViaBranch) ||
  (self is CSeqMethodMismatch && that is CSeqMethodMismatch) ||
  (self is ContentLengthMismatch && that is ContentLengthMismatch) ||
  (self is MultipleContentLengthValues && that is MultipleContentLengthValues) ||
  (self is DuplicateSingleValueHeader && that is DuplicateSingleValueHeader) ||
  (self is UnsupportedVersion && that is UnsupportedVersion) ||
  (self is StatusCodeOutOfRange && that is StatusCodeOutOfRange) ||
  (self is ScalarOutOfRange && that is ScalarOutOfRange) ||
  (self is UnknownRequestUriScheme && that is UnknownRequestUriScheme) ||
  (self is TrailingOctets && that is TrailingOctets) ||
  (self is ExtraneousSeparator && that is ExtraneousSeparator) ||
  (self is UnbracketedUriWithHeaders && that is UnbracketedUriWithHeaders) ||
  (
    self is MissingContactInDialogRequest &&
    that is MissingContactInDialogRequest
  ) ||
  (self is UnsupportedExtension && that is UnsupportedExtension) ||
  (self is EscapedHeadersInRequestUri && that is EscapedHeadersInRequestUri) ||
  (self is MalformedDate && that is MalformedDate) ||
  (self is MalformedFieldValue && that is MalformedFieldValue) ||
  (self is InvalidUtf8 && that is InvalidUtf8)
}

///|
pub extend DiagCode with Show::{to_string, output}

///|
pub impl Show for DiagCode with fn to_string(self : DiagCode) -> String {
  match self {
    MissingRequiredHeader => "missing-required-header"
    MissingViaBranch => "missing-via-branch"
    CSeqMethodMismatch => "cseq-method-mismatch"
    ContentLengthMismatch => "content-length-mismatch"
    MultipleContentLengthValues => "multiple-content-length-values"
    DuplicateSingleValueHeader => "duplicate-single-value-header"
    UnsupportedVersion => "unsupported-version"
    StatusCodeOutOfRange => "status-code-out-of-range"
    ScalarOutOfRange => "scalar-out-of-range"
    UnknownRequestUriScheme => "unknown-request-uri-scheme"
    TrailingOctets => "trailing-octets"
    ExtraneousSeparator => "extraneous-separator"
    UnbracketedUriWithHeaders => "unbracketed-uri-with-headers"
    MissingContactInDialogRequest => "missing-contact-in-dialog-request"
    UnsupportedExtension => "unsupported-extension"
    MalformedFieldValue => "malformed-field-value"
    InvalidUtf8 => "invalid-utf8"
    EscapedHeadersInRequestUri => "escaped-headers-in-request-uri"
    MalformedDate => "malformed-date"
  }
}

///|
pub struct Diagnostic {
  code : DiagCode
  severity : Severity
  offset : Int32
  detail : String
}

///|
/// The rule that fired.
pub fn Diagnostic::code(self : Diagnostic) -> DiagCode {
  self.code
}

///|
/// Whether this is an error or a warning. Only errors change the verdict.
pub fn Diagnostic::severity(self : Diagnostic) -> Severity {
  self.severity
}

///|
/// Byte offset into the input where the problem was seen.
pub fn Diagnostic::offset(self : Diagnostic) -> Int32 {
  self.offset
}

///|
/// Human-readable explanation. Never part of a contract; matches on `code`.
pub fn Diagnostic::detail(self : Diagnostic) -> String {
  self.detail
}

///|
pub extend Diagnostic with Show::{to_string, output}

///|
pub impl Show for Diagnostic with fn to_string(self : Diagnostic) -> String {
  "\{self.severity}:\{self.code}@\{self.offset} \{self.detail}"
}

///|
fn err(code : DiagCode, offset : Int32, detail : String) -> Diagnostic {
  { code, severity: Severity::Error, offset, detail, }
}

///|
fn warn(code : DiagCode, offset : Int32, detail : String) -> Diagnostic {
  { code, severity: Severity::Warning, offset, detail, }
}

///|
/// Header fields that RFC 3261 §7.3.1/§8.1.1.5 treat as single-valued. Duplicates
/// are what RFC 4475 §3.3.8 ("multi01") is about.
fn is_single_valued(name : String) -> Bool {
  for
    cand in [
      "to", "from", "cseq", "call-id", "i", "max-forwards", "content-length", "l",
      "content-type", "c", "date", "subject", "s", "expires", "user-agent", "server",
    ] {
    if bytes_eq_ignore_ascii_case(
        ascii_bytes_of(name).exact_view(),
        ascii_bytes_of(cand).exact_view(),
      ) {
      return true
    }
  }
  false
}

///|
/// Requests that must carry all six core headers (RFC 3261 §8.1.1).
fn request_requires_headers(verb : Bytes) -> Bool {
  // ACK/CANCEL also need them, and treating every request uniformly is both
  // simpler and stricter; RFC 4475 §3.3.1 exercises a plain INVITE.
  ignore(verb)
  true
}

///|
/// Extracts a `;name=value` parameter from a header field value. Returns the value
/// octets (quote-stripped) and the offset of the value.
fn param_of(value : BytesView, want : String) -> (Bytes?, Int32) {
  let n = value.length()
  let mut i = 0
  while i < n {
    if value[i] == SEMI {
      i = i + 1
      while i < n && is_lws(value[i]) {
        i = i + 1
      }
      let ns = i
      while i < n && is_token_char(value[i]) {
        i = i + 1
      }
      let name = value.exact_view(start=ns, end=i)
      let mut val : Bytes? = None
      let mut voff = i
      // Whitespace may surround `=`; RFC 4475 §3.1.1.1 does exactly that.
      let mut j = i
      while j < n && is_lws(value[j]) {
        j = j + 1
      }
      if j < n && value[j] == EQUALS {
        i = j + 1
        while i < n && is_lws(value[i]) {
          i = i + 1
        }
        voff = i
        if i < n && value[i] == DQUOTE {
          i = i + 1
          let vs = i
          while i < n && value[i] != DQUOTE {
            i = i + 1
          }
          val = Some(copy_range(value, vs, i))
          if i < n {
            i = i + 1
          }
        } else {
          let vs = i
          while i < n &&
                value[i] != SEMI &&
                value[i] != COMMA &&
                !is_lws(value[i]) {
            i = i + 1
          }
          val = Some(copy_range(value, vs, i))
        }
      }
      if bytes_eq_ignore_ascii_case(name, ascii_bytes_of(want).exact_view()) {
        return (val, voff)
      }
    } else {
      i = i + 1
    }
  }
  (None, 0)
}

///|
/// Reads a leading decimal run from a value, returning `(value, ok)`.
fn leading_decimal(v : BytesView) -> (Int32, Bool) {
  let mut i = 0
  let mut n = 0
  let mut overflow = false
  while i < v.length() && is_digit(v[i]) {
    if n > 214748363 {
      overflow = true
    }
    n = n * 10 + (v[i] - b'0').to_int()
    i = i + 1
  }
  (if overflow { 2147483647 } else { n }, i > 0)
}

///|
/// True when a value contains `;` immediately followed by `;`, `,` or end of value,
/// i.e. a separator with no parameter attached (RFC 4475 §3.1.2.1 "badinv01").
fn has_extraneous_separator(v : BytesView) -> Bool {
  let n = v.length()
  let mut i = 0
  while i < n {
    if v[i] == SEMI {
      let mut j = i + 1
      while j < n && is_lws(v[j]) {
        j = j + 1
      }
      if j >= n || v[j] == SEMI || v[j] == COMMA {
        return true
      }
    } else if v[i] == COMMA {
      let mut j = i + 1
      while j < n && is_lws(v[j]) {
        j = j + 1
      }
      if j >= n || v[j] == COMMA || v[j] == SEMI {
        return true
      }
    }
    i = i + 1
  }
  false
}

///|
/// Runs all semantic checks over a successfully parsed message.
pub fn diagnose(msg : SipMessage) -> Array[Diagnostic] {
  let out : Array[Diagnostic] = []
  // --- version -------------------------------------------------------------
  let version_ok = match msg.start {
    Request(r) => is_sip_2_0(r.version)
    Response(r) => is_sip_2_0(r.version)
  }
  if !version_ok {
    out.push(err(UnsupportedVersion, 0, "start line version is not SIP/2.0"))
  }
  // --- status code ---------------------------------------------------------
  match msg.start {
    Response(r) =>
      if r.code > 699 || r.code < 100 {
        // RFC 4475 §3.1.2.19 "bigcode": must not break the receiver.
        out.push(
          err(StatusCodeOutOfRange, 0, "status code \{r.code} outside 100..699"),
        )
      }
    Request(_) => ()
  }
  // --- duplicated single-valued headers (§3.3.8 "multi01") -----------------
  for h in msg.headers {
    let name = ascii_of(h.name.exact_view())
    if is_single_valued(name) {
      let same = msg.all_headers(name)
      if same.length() > 1 {
        out.push(
          err(
            DuplicateSingleValueHeader,
            h.offset,
            "\{same.length()} values for single-valued header \{name}",
          ),
        )
      }
    }
  }
  // --- Content-Length consistency -----------------------------------------
  let cls = msg.all_headers("Content-Length")
  let cls2 = msg.all_headers("l")
  let cl_all = if cls.length() > 0 { cls } else { cls2 }
  if cl_all.length() > 1 {
    // RFC 4475 §3.3.9 "doubleCL"
    out.push(
      err(
        MultipleContentLengthValues,
        cl_all[0].offset,
        "\{cl_all.length()} Content-Length headers",
      ),
    )
  }
  for cl in cl_all {
    // The grammar layer already rejects a non-decimal or negative length
    // (SyntaxKind::BadContentLength), so the only question left here is whether
    // the declared length agrees with the framing that was actually produced.
    // Checks that can never fire are not kept as decoration.
    let declared = leading_decimal(cl.value.exact_view()).0
    if !(msg.trailing.length() > 0) && declared != msg.body.length() {
      // Declared longer than the datagram: §3.1.2.2 "clrf" style defect.
      out.push(
        err(
          ContentLengthMismatch,
          cl.offset,
          "declared \{declared} but body is \{msg.body.length()}",
        ),
      )
    }
  }
  // --- trailing octets (§3.1.1.8 "trailingspace") -------------------------
  if msg.trailing.length() > 0 {
    out.push(
      warn(
        TrailingOctets,
        msg.consumed,
        "\{msg.trailing.length()} octets after the declared body",
      ),
    )
  }
  // --- scalar ranges (apply to requests and responses alike) ---------------
  // RFC 4475 §3.1.2.4 "scalar02" and §3.1.2.5 "scalarlg". A value that saturates
  // the reader is by definition out of the 32-bit range the grammar allows.
  match msg.header("CSeq") {
    Some(cseq) => {
      let v = cseq.value.exact_view()
      let mut i = 0
      while i < v.length() && is_digit(v[i]) {
        i = i + 1
      }
      let digits = v.exact_view(start=0, end=i)
      let (seq, ok) = leading_decimal(v)
      if !ok || seq >= 2147483647 || digits.length() > 10 {
        out.push(
          err(
            ScalarOutOfRange,
            cseq.offset,
            "CSeq sequence number outside 0..2^32-1",
          ),
        )
      }
    }
    None => ()
  }
  match msg.header("Retry-After") {
    Some(ra) => {
      let v = ra.value.exact_view()
      let mut i = 0
      while i < v.length() && is_digit(v[i]) {
        i = i + 1
      }
      let (val, ok) = leading_decimal(v)
      if !ok || val >= 2147483647 || i > 10 {
        out.push(
          err(ScalarOutOfRange, ra.offset, "Retry-After outside 0..2^32-1"),
        )
      }
    }
    None => ()
  }
  // --- extensions we do not implement -------------------------------------
  // This library implements no SIP extensions, so any non-empty Require or
  // Proxy-Require names a feature the message demands and we cannot honour
  // (RFC 4475 §3.3.5 "bext01", which expects 420 Bad Extension).
  for hn in ["Require", "Proxy-Require"] {
    match msg.header(hn) {
      Some(f) =>
        if f.value.length() > 0 {
          out.push(
            err(
              UnsupportedExtension,
              f.offset,
              "unsupported extension: \{ascii_of(f.value.exact_view())}",
            ),
          )
        }
      None => ()
    }
  }
  // --- request-specific checks --------------------------------------------
  match msg.start {
    Request(r) => {
      let verb = r.verb
      if request_requires_headers(verb) {
        for required in ["To", "From", "CSeq", "Call-ID", "Via"] {
          if msg.all_headers(required).length() == 0 {
            // RFC 4475 §3.3.1 "insuf"
            out.push(
              err(MissingRequiredHeader, 0, "request has no \{required} header"),
            )
          }
        }
        // Max-Forwards is a SHOULD in RFC 3261 §8.1.1.6 and absent from RFC 2543
        // messages (§3.4.1 "inv2543"), so its absence is a warning, not a defect.
        if msg.all_headers("Max-Forwards").length() == 0 {
          out.push(
            warn(MissingRequiredHeader, 0, "request has no Max-Forwards header"),
          )
        }
      }
      match r.uri {
        Other(raw) =>
          // RFC 4475 §3.3.2 "unkscm" / §3.3.3 "novelsc"
          out.push(
            err(
              UnknownRequestUriScheme,
              0,
              "Request-URI scheme is not sip/sips: \{ascii_of(raw.exact_view())}",
            ),
          )
        Parsed(u) => {
          // RFC 4475 §3.1.2.11 "escruri": a Request-URI must not smuggle headers
          // past a proxy by escaping them. The vector escapes the angle brackets
          // of a Route value as %3C/%3E, which no legitimate Request-URI needs.
          let raw = u.raw().exact_view()
          if contains_ignore_case(raw, b"%3c") ||
            contains_ignore_case(raw, b"%3e") {
            out.push(
              err(
                EscapedHeadersInRequestUri,
                0,
                "Request-URI contains escaped headers",
              ),
            )
          }
        }
      }
      // CSeq method agreement (§3.1.2.17 "mismatch01")
      match msg.header("CSeq") {
        Some(cseq) => {
          let v = cseq.value.exact_view()
          let mut i = 0
          while i < v.length() && is_digit(v[i]) {
            i = i + 1
          }
          while i < v.length() && is_lws(v[i]) {
            i = i + 1
          }
          let mstart = i
          while i < v.length() && is_token_char(v[i]) {
            i = i + 1
          }
          let cm = v.exact_view(start=mstart, end=i)
          if cm.length() > 0 &&
            !bytes_eq_ignore_ascii_case(cm, verb.exact_view()) {
            out.push(
              err(
                CSeqMethodMismatch,
                cseq.offset,
                "CSeq method \{ascii_of(cm.exact_view())} != start line \{ascii_of(verb.exact_view())}",
              ),
            )
          }
        }
        None => ()
      }
      // Max-Forwards range (§3.1.2.4)
      match msg.header("Max-Forwards") {
        Some(mf) => {
          let (v, ok) = leading_decimal(mf.value.exact_view())
          if !ok || v > 255 {
            out.push(
              err(ScalarOutOfRange, mf.offset, "Max-Forwards outside 0..255"),
            )
          }
        }
        None => ()
      }
      // Via branch (§3.2.1 "nobranch", §3.4.1 "inv2543")
      let vias = msg.all_headers("Via")
      for via in vias {
        let (branch, at) = param_of(via.value.exact_view(), "branch")
        match branch {
          Some(b) =>
            // `z9hG4bK` alone advertises RFC 3261 transaction matching yet carries
            // no identifier; that is the defect §3.2.1 is about.
            if b.length() == 0 ||
              bytes_eq_ignore_ascii_case(
                b.exact_view(),
                b"z9hG4bK".exact_view(),
              ) {
              out.push(
                err(
                  MissingViaBranch,
                  via.offset + at,
                  "branch has no transaction identifier",
                ),
              )
            }
          None =>
            // No branch at all is legal for RFC 2543 senders (§3.4.1), which fall
            // back to the older transaction matching rules. Report, do not reject.
            out.push(
              warn(
                MissingViaBranch,
                via.offset,
                "Via has no branch parameter (RFC 2543 style)",
              ),
            )
        }
      }
      // Contact framing (§3.1.2.13 "regbadct")
      for c in msg.all_headers("Contact") {
        let v = c.value.exact_view()
        let mut i = 0
        while i < v.length() && is_lws(v[i]) {
          i = i + 1
        }
        if i < v.length() && v[i] != LT && v[i] != DQUOTE {
          let has_q = match v.find(b"?".exact_view()) {
            Some(_) => true
            None => false
          }
          if has_q {
            out.push(
              err(
                UnbracketedUriWithHeaders,
                c.offset,
                "addr-spec with URI headers must use angle brackets",
              ),
            )
          }
        }
        // §3.1.2.1 "badinv01": `Contact: ;;;;`
        if has_extraneous_separator(v) {
          out.push(
            err(ExtraneousSeparator, c.offset, "separator without a parameter"),
          )
        }
      }
      for via in vias {
        if has_extraneous_separator(via.value.exact_view()) {
          out.push(
            err(
              ExtraneousSeparator,
              via.offset,
              "separator without a parameter",
            ),
          )
        }
      }
      // INVITE establishes a dialog and must carry Contact (RFC 3261 §8.1.1.6).
      if bytes_eq_ignore_ascii_case(verb, b"INVITE".exact_view()) &&
        msg.all_headers("Contact").length() == 0 {
        out.push(
          warn(MissingContactInDialogRequest, 0, "INVITE has no Contact"),
        )
      }
    }
    Response(_) => ()
  }
  // --- structural syntax for the remaining §20 fields + TEXT-UTF8 checks ---
  check_field_syntax(msg, out)
  out
}

///|
/// Splits a header value on `sep` octets that live outside quoted strings, so a
/// comma inside `warn-text` does not split a Warning into two.
fn split_outside_quotes(v : BytesView, sep : Byte) -> Array[BytesView] {
  let out : Array[BytesView] = []
  let mut start = 0
  let mut in_quotes = false
  let mut i = 0
  while i < v.length() {
    let b = v[i]
    if in_quotes {
      if b == BSLASH {
        i = i + 2
        continue
      }
      if b == DQUOTE {
        in_quotes = false
      }
      i = i + 1
    } else if b == DQUOTE {
      in_quotes = true
      i = i + 1
    } else if b == sep {
      out.push(v.exact_view(start~, end=i))
      start = i + 1
      i = i + 1
    } else {
      i = i + 1
    }
  }
  out.push(v.exact_view(start~, end=v.length()))
  out
}

///|
/// True when every octet in the range is a decimal digit and the range is not
/// empty.
fn all_digits(v : BytesView) -> Bool {
  if v.length() == 0 {
    return false
  }
  for i in 0.. Unit {
  // Warning (§20.47): 1#warning-value, warn-code = 3DIGIT, values 100..299.
  for w in msg.all_headers("Warning") {
    for part in split_outside_quotes(w.value.exact_view(), COMMA) {
      let t = trim_ows(part)
      if t.length() < 5 ||
        !all_digits(t.exact_view(start=0, end=3)) ||
        t[3] != SP {
        out.push(
          err(
            MalformedFieldValue,
            w.offset,
            "Warning: expected 3DIGIT SP warn-agent SP warn-text",
          ),
        )
        continue
      }
      // The grammar constrains warn-code to 3DIGIT (checked above); the 1xx/2xx
      // semantics are descriptive, not a syntactic constraint -- scalarlg's
      // four-digit 1812 is caught by the length check alone.
    }
  }
  // Expires / Min-Expires (§20.19, §20.23): delta-seconds or SIP-date.
  for hn in ["Expires", "Min-Expires"] {
    for f in msg.all_headers(hn) {
      let v = trim_ows(f.value.exact_view())
      if v.length() == 0 {
        out.push(err(MalformedFieldValue, f.offset, "\{hn}: empty value"))
        continue
      }
      if all_digits(v) {
        let val = leading_decimal(v).0
        if val >= SATURATED {
          out.push(err(ScalarOutOfRange, f.offset, "\{hn} outside 0..2^32-1"))
        }
      } else if !is_sip_date(v) {
        out.push(
          err(
            MalformedFieldValue,
            f.offset,
            "\{hn}: neither delta-seconds nor SIP-date",
          ),
        )
      }
    }
  }
  // RSeq (RFC 3262 §7): 1*DIGIT below 2^31.
  for f in msg.all_headers("RSeq") {
    let v = trim_ows(f.value.exact_view())
    if !all_digits(v) || v.length() > 10 {
      out.push(err(MalformedFieldValue, f.offset, "RSeq must be 1*DIGIT"))
    }
    ignore(leading_decimal(v))
  }
  // Rack (RFC 3262 §7): response-num SP sequence SP method.
  for f in msg.all_headers("Rack") {
    let parts = split_outside_quotes(trim_ows(f.value.exact_view()), SP)
    if parts.length() != 3 ||
      !all_digits(parts[0]) ||
      !all_digits(parts[1]) ||
      parts[2].length() == 0 {
      out.push(
        err(
          MalformedFieldValue,
          f.offset,
          "Rack: expected response-num SP sequence SP method",
        ),
      )
    }
  }
  // Priority (§20.26): the four standard values or an extension token.
  for f in msg.all_headers("Priority") {
    let v = trim_ows(f.value.exact_view())
    let head = if contains_ignore_case(v, b";") {
      let mut cut = v.length()
      for i in 0.. 0
    if ok {
      for i in 0.. 2 {
      out.push(
        err(
          MalformedFieldValue,
          f.offset,
          "Timestamp: expected time [SP delay]",
        ),
      )
      continue
    }
    for t in tokens {
      // digits [. digits]
      let ok = (fn() {
        let mut seen_dot = false
        let mut digits = 0
        for i in 0.. 0
      })()
      if !ok {
        out.push(
          err(MalformedFieldValue, f.offset, "Timestamp: bad time or delay"),
        )
        break
      }
    }
  }
  // MIME-Version (§20.24): 1*DIGIT "." 1*DIGIT.
  for f in msg.all_headers("MIME-Version") {
    let v = trim_ows(f.value.exact_view())
    let dot = (fn() {
      for i in 0.. 0 &&
      dot < v.length() - 1 &&
      all_digits(v.exact_view(start=0, end=dot)) &&
      all_digits(v.exact_view(start=dot + 1, end=v.length()))
    if !ok {
      out.push(
        err(
          MalformedFieldValue,
          f.offset,
          "MIME-Version: expected 1*DIGIT \".\" 1*DIGIT",
        ),
      )
    }
  }
  // Content-Type (§20.15): type "/" subtype, both tokens.
  for f in msg.all_headers("Content-Type") {
    let v = trim_ows(f.value.exact_view())
    let slash = (fn() {
      for i in 0.. 0 &&
      slash < v.length() - 1 &&
      is_token_char(v[0]) &&
      is_token_char(v[slash - 1]) &&
      is_token_char(v[slash + 1])
    if !ok {
      out.push(
        err(
          MalformedFieldValue,
          f.offset,
          "Content-Type: expected type \"/\" subtype",
        ),
      )
    }
  }
  // In-Reply-To (§20.14): at least one Call-ID.
  for f in msg.all_headers("In-Reply-To") {
    if trim_ows(f.value.exact_view()).length() == 0 {
      out.push(err(MalformedFieldValue, f.offset, "In-Reply-To: empty"))
    }
  }
  // TEXT-UTF8-TRIM fields (§25.1): well-formed UTF-8 required.
  match msg.start_line() {
    Response(sl) =>
      if !is_valid_utf8(sl.reason().exact_view()) {
        out.push(err(InvalidUtf8, 0, "Reason-Phrase is not well-formed UTF-8"))
      }
    Request(_) => ()
  }
  for hn in ["Subject", "Organization", "User-Agent", "Server"] {
    for f in msg.all_headers(hn) {
      if !is_valid_utf8(f.value.exact_view()) {
        out.push(err(InvalidUtf8, f.offset, "\{hn} is not well-formed UTF-8"))
      }
    }
  }
}