• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

duesee / imap-codec / 14934160338

09 May 2025 05:15PM UTC coverage: 91.702% (+0.09%) from 91.609%
14934160338

Pull #654

github

web-flow
Merge 7c91363d3 into f06c874fc
Pull Request #654: feat: Implement "quirk_trailing_space_{cqpability,id,search}" + fix panic

77 of 79 new or added lines in 4 files covered. (97.47%)

9 existing lines in 1 file now uncovered.

11549 of 12594 relevant lines covered (91.7%)

888.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.85
/imap-codec/src/response.rs
1
#[cfg(not(feature = "quirk_crlf_relaxed"))]
2
use abnf_core::streaming::crlf;
3
#[cfg(feature = "quirk_crlf_relaxed")]
4
use abnf_core::streaming::crlf_relaxed as crlf;
5
use abnf_core::streaming::sp;
6
use base64::{engine::general_purpose::STANDARD as _base64, Engine};
7
#[cfg(feature = "ext_condstore_qresync")]
8
use imap_types::sequence::SequenceSet;
9
use imap_types::{
10
    core::{Text, Vec1},
11
    fetch::MessageDataItem,
12
    response::{
13
        Bye, Capability, Code, CodeOther, CommandContinuationRequest, Data, Greeting, GreetingKind,
14
        Response, Status, StatusBody, StatusKind, Tagged,
15
    },
16
};
17
#[cfg(feature = "quirk_missing_text")]
18
use nom::combinator::peek;
19
use nom::{
20
    branch::alt,
21
    bytes::streaming::{tag, tag_no_case, take_until, take_while},
22
    combinator::{map, map_res, opt, value},
23
    multi::separated_list1,
24
    sequence::{delimited, preceded, terminated, tuple},
25
};
26

27
#[cfg(feature = "ext_id")]
28
use crate::extensions::id::id_response;
29
#[cfg(feature = "ext_metadata")]
30
use crate::extensions::metadata::metadata_code;
31
use crate::{
32
    core::{atom, charset, nz_number, tag_imap, text},
33
    decode::IMAPResult,
34
    extensions::{
35
        enable::enable_data,
36
        uidplus::{resp_code_apnd, resp_code_copy},
37
    },
38
    fetch::msg_att,
39
    flag::flag_perm,
40
    mailbox::mailbox_data,
41
};
42
#[cfg(feature = "ext_condstore_qresync")]
43
use crate::{extensions::condstore_qresync::mod_sequence_value, sequence::sequence_set};
44

45
// ----- greeting -----
46

47
/// `greeting = "*" SP (resp-cond-auth / resp-cond-bye) CRLF`
48
pub(crate) fn greeting(input: &[u8]) -> IMAPResult<&[u8], Greeting> {
136✔
49
    let mut parser = delimited(
136✔
50
        tag(b"* "),
136✔
51
        alt((
136✔
52
            resp_cond_auth,
136✔
53
            map(resp_cond_bye, |resp_text| (GreetingKind::Bye, resp_text)),
140✔
54
        )),
136✔
55
        crlf,
136✔
56
    );
136✔
57

58
    let (remaining, (kind, (code, text))) = parser(input)?;
136✔
59

60
    Ok((remaining, Greeting { kind, code, text }))
64✔
61
}
136✔
62

63
/// `resp-cond-auth = ("OK" / "PREAUTH") SP resp-text`
64
///
65
/// Authentication condition
66
#[allow(clippy::type_complexity)]
67
pub(crate) fn resp_cond_auth(
120✔
68
    input: &[u8],
120✔
69
) -> IMAPResult<&[u8], (GreetingKind, (Option<Code>, Text))> {
120✔
70
    let mut parser = tuple((
120✔
71
        alt((
120✔
72
            value(GreetingKind::Ok, tag_no_case(b"OK ")),
120✔
73
            value(GreetingKind::PreAuth, tag_no_case(b"PREAUTH ")),
120✔
74
        )),
120✔
75
        resp_text,
120✔
76
    ));
120✔
77

78
    let (remaining, (kind, resp_text)) = parser(input)?;
120✔
79

80
    Ok((remaining, (kind, resp_text)))
64✔
81
}
120✔
82

83
/// `resp-text = ["[" resp-text-code "]" SP] text`
84
pub(crate) fn resp_text(input: &[u8]) -> IMAPResult<&[u8], (Option<Code>, Text)> {
1,764✔
85
    // When the text starts with "[", we insist on parsing a code.
86
    // Otherwise, a broken code could be interpreted as text.
87
    let (_, start) = opt(tag(b"["))(input)?;
1,764✔
88

89
    if start.is_some() {
1,756✔
90
        tuple((
332✔
91
            preceded(
332✔
92
                tag(b"["),
332✔
93
                map(
332✔
94
                    alt((
332✔
95
                        terminated(resp_text_code, tag(b"]")),
332✔
96
                        map(
332✔
97
                            terminated(
332✔
98
                                take_while(|b: u8| b != b']' && b != b'\r' && b != b'\n'),
362✔
99
                                tag(b"]"),
332✔
100
                            ),
332✔
101
                            |bytes: &[u8]| Code::Other(CodeOther::unvalidated(bytes)),
335✔
102
                        ),
332✔
103
                    )),
332✔
104
                    Some,
332✔
105
                ),
332✔
106
            ),
332✔
107
            #[cfg(not(feature = "quirk_missing_text"))]
332✔
108
            preceded(sp, text),
332✔
109
            #[cfg(feature = "quirk_missing_text")]
332✔
110
            alt((
332✔
111
                preceded(sp, text),
332✔
112
                map(peek(crlf), |_| {
333✔
113
                    log::warn!("Rectified missing `text` to \"...\"");
2✔
114

115
                    Text::unvalidated("...")
2✔
116
                }),
333✔
117
            )),
332✔
118
        ))(input)
332✔
119
    } else {
120
        map(text, |text| (None, text))(input)
1,622✔
121
    }
122
}
1,764✔
123

124
/// ```abnf
125
/// resp-text-code = "ALERT" /
126
///                  "BADCHARSET" [SP "(" charset *(SP charset) ")" ] /
127
///                  capability-data /
128
///                  "PARSE" /
129
///                  "PERMANENTFLAGS" SP "(" [flag-perm *(SP flag-perm)] ")" /
130
///                  "READ-ONLY" /
131
///                  "READ-WRITE" /
132
///                  "TRYCREATE" /
133
///                  "UIDNEXT" SP nz-number /
134
///                  "UIDVALIDITY" SP nz-number /
135
///                  "UNSEEN" SP nz-number /
136
///                  "COMPRESSIONACTIVE" / ; RFC 4978
137
///                  "OVERQUOTA" /         ; RFC 9208
138
///                  "TOOBIG" /            ; RFC 4469
139
///                  "METADATA" SP (       ; RFC 5464
140
///                    "LONGENTRIES" SP number /
141
///                    "MAXSIZE" SP number /
142
///                    "TOOMANY" /
143
///                    "NOPRIVATE"
144
///                  ) /
145
///                  "UNKNOWN-CTE" /       ; RFC 3516
146
///                  "HIGHESTMODSEQ" SP mod-sequence-value / ; RFC7162
147
///                  "NOMODSEQ"                            / ; RFC7162
148
///                  "MODIFIED" SP sequence-set            / ; RFC7162
149
///                  "CLOSED"                              / ; RFC7162
150
///                  atom [SP 1*<any TEXT-CHAR except "]">]
151
/// ```
152
///
153
/// Note: See errata id: 261
154
pub(crate) fn resp_text_code(input: &[u8]) -> IMAPResult<&[u8], Code> {
332✔
155
    alt((
332✔
156
        value(Code::Alert, tag_no_case(b"ALERT")),
332✔
157
        map(
332✔
158
            preceded(
332✔
159
                tag_no_case(b"BADCHARSET"),
332✔
160
                opt(delimited(
332✔
161
                    tag(b" ("),
332✔
162
                    separated_list1(sp, charset),
332✔
163
                    tag(b")"),
332✔
164
                )),
332✔
165
            ),
332✔
166
            |maybe_charsets| Code::BadCharset {
334✔
167
                allowed: maybe_charsets.unwrap_or_default(),
4✔
168
            },
334✔
169
        ),
332✔
170
        map(capability_data, Code::Capability),
332✔
171
        value(Code::Parse, tag_no_case(b"PARSE")),
332✔
172
        map(
332✔
173
            preceded(
332✔
174
                tag_no_case(b"PERMANENTFLAGS "),
332✔
175
                delimited(
332✔
176
                    tag(b"("),
332✔
177
                    map(opt(separated_list1(sp, flag_perm)), |maybe_flags| {
336✔
178
                        maybe_flags.unwrap_or_default()
32✔
179
                    }),
336✔
180
                    tag(b")"),
332✔
181
                ),
332✔
182
            ),
332✔
183
            Code::PermanentFlags,
332✔
184
        ),
332✔
185
        value(Code::ReadOnly, tag_no_case(b"READ-ONLY")),
332✔
186
        value(Code::ReadWrite, tag_no_case(b"READ-WRITE")),
332✔
187
        value(Code::TryCreate, tag_no_case(b"TRYCREATE")),
332✔
188
        map(preceded(tag_no_case(b"UIDNEXT "), nz_number), Code::UidNext),
332✔
189
        map(
332✔
190
            preceded(tag_no_case(b"UIDVALIDITY "), nz_number),
332✔
191
            Code::UidValidity,
332✔
192
        ),
332✔
193
        map(preceded(tag_no_case(b"UNSEEN "), nz_number), Code::Unseen),
332✔
194
        value(Code::CompressionActive, tag_no_case(b"COMPRESSIONACTIVE")),
332✔
195
        value(Code::OverQuota, tag_no_case(b"OVERQUOTA")),
332✔
196
        value(Code::TooBig, tag_no_case(b"TOOBIG")),
332✔
197
        #[cfg(feature = "ext_metadata")]
332✔
198
        map(
332✔
199
            preceded(tag_no_case("METADATA "), metadata_code),
332✔
200
            Code::Metadata,
332✔
201
        ),
332✔
202
        value(Code::UnknownCte, tag_no_case(b"UNKNOWN-CTE")),
332✔
203
        resp_code_apnd,
332✔
204
        resp_code_copy,
332✔
205
        value(Code::UidNotSticky, tag_no_case(b"UIDNOTSTICKY")),
332✔
206
        #[cfg(feature = "ext_condstore_qresync")]
332✔
207
        alt((
332✔
208
            map(
332✔
209
                preceded(tag_no_case(b"HIGHESTMODSEQ "), mod_sequence_value),
332✔
210
                Code::HighestModSeq,
332✔
211
            ),
332✔
212
            value(Code::NoModSeq, tag_no_case(b"NOMODSEQ")),
332✔
213
            map(
332✔
214
                preceded(tag_no_case(b"MODIFIED "), sequence_set),
332✔
215
                Code::Modified,
332✔
216
            ),
332✔
217
            value(Code::Closed, tag_no_case(b"UIDNOTSTICKY")),
332✔
218
        )),
332✔
219
    ))(input)
332✔
220
}
332✔
221

222
/// `capability-data = "CAPABILITY" *(SP capability) SP "IMAP4rev1" *(SP capability)`
223
///
224
/// Servers MUST implement the STARTTLS, AUTH=PLAIN, and LOGINDISABLED capabilities
225
/// Servers which offer RFC 1730 compatibility MUST list "IMAP4" as the first capability.
226
pub(crate) fn capability_data(input: &[u8]) -> IMAPResult<&[u8], Vec1<Capability>> {
458✔
227
    map(
458✔
228
        #[cfg(not(feature = "quirk_trailing_space_capability"))]
458✔
229
        preceded(tag_no_case("CAPABILITY "), separated_list1(sp, capability)),
458✔
230
        #[cfg(feature = "quirk_trailing_space_capability")]
458✔
231
        terminated(
458✔
232
            preceded(tag_no_case("CAPABILITY "), separated_list1(sp, capability)),
458✔
233
            opt(sp),
458✔
234
        ),
458✔
235
        Vec1::unvalidated,
458✔
236
    )(input)
458✔
237
}
458✔
238

239
/// `capability = ("AUTH=" auth-type) /
240
///               "COMPRESS=" algorithm / ; RFC 4978
241
///               atom`
242
pub(crate) fn capability(input: &[u8]) -> IMAPResult<&[u8], Capability> {
474✔
243
    map(atom, Capability::from)(input)
474✔
244
}
474✔
245

246
/// `resp-cond-bye = "BYE" SP resp-text`
247
pub(crate) fn resp_cond_bye(input: &[u8]) -> IMAPResult<&[u8], (Option<Code>, Text)> {
1,616✔
248
    preceded(tag_no_case(b"BYE "), resp_text)(input)
1,616✔
249
}
1,616✔
250

251
// ----- response -----
252

253
/// `response = *(continue-req / response-data) response-done`
254
pub(crate) fn response(input: &[u8]) -> IMAPResult<&[u8], Response> {
3,226✔
255
    // Divert from standard here for better usability.
3,226✔
256
    // response_data already contains the bye response, thus
3,226✔
257
    // response_done could also be response_tagged.
3,226✔
258
    //
3,226✔
259
    // However, I will keep it as it is for now.
3,226✔
260
    alt((
3,226✔
261
        #[cfg(feature = "quirk_empty_continue_req")]
3,226✔
262
        map(empty_continue_req, Response::CommandContinuationRequest),
3,226✔
263
        map(continue_req, Response::CommandContinuationRequest),
3,226✔
264
        response_data,
3,226✔
265
        map(response_done, Response::Status),
3,226✔
266
    ))(input)
3,226✔
267
}
3,226✔
268

269
/// Parser that allows a spaceless, empty continuation request `+\r\n`.
270
#[cfg(feature = "quirk_empty_continue_req")]
271
pub(crate) fn empty_continue_req(input: &[u8]) -> IMAPResult<&[u8], CommandContinuationRequest> {
3,226✔
272
    let mut parser = tuple((tag(b"+"), crlf));
3,226✔
273
    let (remaining, _) = parser(input)?;
3,226✔
NEW
274
    log::warn!("Rectified faulty continuation request `+\r\n` to `+ ...\r\n`");
×
NEW
275
    let req = CommandContinuationRequest::basic(None, "...").unwrap();
×
276

×
277
    Ok((remaining, req))
×
278
}
3,226✔
279

280
/// `continue-req = "+" SP (resp-text / base64) CRLF`
281
pub(crate) fn continue_req(input: &[u8]) -> IMAPResult<&[u8], CommandContinuationRequest> {
3,218✔
282
    // We can't map the output of `resp_text` directly to `Continue::basic()` because we might end
283
    // up with a subset of `Text` that is valid base64 and will panic on `unwrap()`. Thus, we first
284
    // let the parsing finish and only later map to `Continue`.
285

286
    // A helper struct to postpone the unification to `Continue` in the `alt` combinator below.
287
    enum Either<A, B> {
288
        Base64(A),
289
        Basic(B),
290
    }
291

292
    let mut parser = tuple((
3,218✔
293
        tag(b"+ "),
3,218✔
294
        alt((
3,218✔
295
            #[cfg(not(feature = "quirk_crlf_relaxed"))]
3,218✔
296
            map(
3,218✔
297
                map_res(take_until("\r\n"), |input| _base64.decode(input)),
3,218✔
298
                Either::Base64,
3,218✔
299
            ),
3,218✔
300
            #[cfg(feature = "quirk_crlf_relaxed")]
3,218✔
301
            map(
3,218✔
302
                map_res(take_until("\n"), |input: &[u8]| {
3,220✔
303
                    if !input.is_empty() && input[input.len().saturating_sub(1)] == b'\r' {
16✔
304
                        _base64.decode(&input[..input.len().saturating_sub(1)])
16✔
305
                    } else {
306
                        _base64.decode(input)
×
307
                    }
308
                }),
3,220✔
309
                Either::Base64,
3,218✔
310
            ),
3,218✔
311
            map(resp_text, Either::Basic),
3,218✔
312
        )),
3,218✔
313
        crlf,
3,218✔
314
    ));
3,218✔
315

316
    let (remaining, (_, either, _)) = parser(input)?;
3,218✔
317

318
    let continue_request = match either {
16✔
319
        Either::Base64(data) => CommandContinuationRequest::base64(data),
×
320
        Either::Basic((code, text)) => CommandContinuationRequest::basic(code, text).unwrap(),
16✔
321
    };
322

323
    Ok((remaining, continue_request))
16✔
324
}
3,218✔
325

326
/// ```abnf
327
/// response-data = "*" SP (
328
///                    resp-cond-state /
329
///                    resp-cond-bye /
330
///                    mailbox-data /
331
///                    message-data /
332
///                    capability-data /
333
///                    id_response ; (See RFC 2971)
334
///                  ) CRLF
335
/// ```
336
pub(crate) fn response_data(input: &[u8]) -> IMAPResult<&[u8], Response> {
3,214✔
337
    delimited(
3,214✔
338
        tag(b"* "),
3,214✔
339
        alt((
3,214✔
340
            map(resp_cond_state, |(kind, code, text)| {
3,286✔
341
                Response::Status(Status::Untagged(StatusBody { kind, code, text }))
384✔
342
            }),
3,286✔
343
            map(resp_cond_bye, |(code, text)| {
3,222✔
344
                Response::Status(Status::Bye(Bye { code, text }))
52✔
345
            }),
3,222✔
346
            map(mailbox_data, Response::Data),
3,214✔
347
            map(message_data, Response::Data),
3,214✔
348
            map(capability_data, |caps| {
3,239✔
349
                Response::Data(Data::Capability(caps))
134✔
350
            }),
3,239✔
351
            map(enable_data, Response::Data),
3,214✔
352
            #[cfg(feature = "ext_id")]
3,214✔
353
            map(id_response, |parameters| {
3,216✔
354
                Response::Data(Data::Id { parameters })
4✔
355
            }),
3,216✔
356
        )),
3,214✔
357
        crlf,
3,214✔
358
    )(input)
3,214✔
359
}
3,214✔
360

361
/// `resp-cond-state = ("OK" / "NO" / "BAD") SP resp-text`
362
///
363
/// Status condition
364
pub(crate) fn resp_cond_state(input: &[u8]) -> IMAPResult<&[u8], (StatusKind, Option<Code>, Text)> {
3,206✔
365
    let mut parser = tuple((
3,206✔
366
        alt((
3,206✔
367
            value(StatusKind::Ok, tag_no_case("OK ")),
3,206✔
368
            value(StatusKind::No, tag_no_case("NO ")),
3,206✔
369
            value(StatusKind::Bad, tag_no_case("BAD ")),
3,206✔
370
        )),
3,206✔
371
        resp_text,
3,206✔
372
    ));
3,206✔
373

374
    let (remaining, (kind, (maybe_code, text))) = parser(input)?;
3,206✔
375

376
    Ok((remaining, (kind, maybe_code, text)))
1,596✔
377
}
3,206✔
378

379
/// `response-done = response-tagged / response-fatal`
380
pub(crate) fn response_done(input: &[u8]) -> IMAPResult<&[u8], Status> {
1,230✔
381
    alt((response_tagged, response_fatal))(input)
1,230✔
382
}
1,230✔
383

384
/// `response-tagged = tag SP resp-cond-state CRLF`
385
pub(crate) fn response_tagged(input: &[u8]) -> IMAPResult<&[u8], Status> {
1,230✔
386
    let mut parser = tuple((tag_imap, sp, resp_cond_state, crlf));
1,230✔
387

388
    let (remaining, (tag, _, (kind, code, text), _)) = parser(input)?;
1,230✔
389

390
    Ok((
1,212✔
391
        remaining,
1,212✔
392
        Status::Tagged(Tagged {
1,212✔
393
            tag,
1,212✔
394
            body: StatusBody { kind, code, text },
1,212✔
395
        }),
1,212✔
396
    ))
1,212✔
397
}
1,230✔
398

399
/// `response-fatal = "*" SP resp-cond-bye CRLF`
400
///
401
/// Server closes connection immediately
402
pub(crate) fn response_fatal(input: &[u8]) -> IMAPResult<&[u8], Status> {
18✔
403
    let mut parser = delimited(tag(b"* "), resp_cond_bye, crlf);
18✔
404

405
    let (remaining, (code, text)) = parser(input)?;
18✔
406

407
    Ok((remaining, Status::Bye(Bye { code, text })))
×
408
}
18✔
409

410
/// ```abnf
411
/// message-data = nz-number SP ("EXPUNGE" / ("FETCH" SP msg-att))
412
/// ```
413
///
414
/// From RFC 7162:
415
///
416
/// ```abnf
417
/// message-data =/ expunged-resp
418
///
419
/// expunged-resp = "VANISHED" [SP "(EARLIER)"] SP known-uids
420
/// ```
421
pub(crate) fn message_data(input: &[u8]) -> IMAPResult<&[u8], Data> {
474✔
422
    #[derive(Clone)]
423
    enum TmpData<'a> {
424
        Expunge,
425
        Fetch(Vec1<MessageDataItem<'a>>),
426
        #[cfg(feature = "ext_condstore_qresync")]
427
        Vanished(bool, SequenceSet),
428
    }
429

430
    let (remaining, (seq, tmp)) = tuple((
474✔
431
        terminated(nz_number, sp),
474✔
432
        alt((
474✔
433
            value(TmpData::Expunge, tag_no_case(b"EXPUNGE")),
474✔
434
            map(preceded(tag_no_case(b"FETCH "), msg_att), TmpData::Fetch),
474✔
435
            #[cfg(feature = "ext_condstore_qresync")]
474✔
436
            map(
474✔
437
                tuple((
474✔
438
                    tag_no_case("VANISHED"),
474✔
439
                    opt(tag_no_case(" (EARLIER)")),
474✔
440
                    preceded(sp, sequence_set),
474✔
441
                )),
474✔
442
                |(_, earlier, known_uids)| TmpData::Vanished(earlier.is_some(), known_uids),
474✔
443
            ),
474✔
444
        )),
474✔
445
    ))(input)?;
474✔
446

447
    Ok((
448
        remaining,
288✔
449
        match tmp {
288✔
450
            TmpData::Expunge => Data::Expunge(seq),
100✔
451
            TmpData::Fetch(items) => Data::Fetch { seq, items },
188✔
452
            #[cfg(feature = "ext_condstore_qresync")]
453
            TmpData::Vanished(earlier, known_uids) => Data::Vanished {
×
454
                earlier,
×
455
                known_uids,
×
456
            },
×
457
        },
458
    ))
459
}
474✔
460

461
#[cfg(test)]
462
mod tests {
463
    use std::num::NonZeroU32;
464

465
    use imap_types::{
466
        body::{
467
            BasicFields, Body, BodyExtension, BodyStructure, Disposition, Language, Location,
468
            SinglePartExtensionData, SpecificFields,
469
        },
470
        core::{IString, NString, QuotedChar, Tag},
471
        flag::FlagNameAttribute,
472
    };
473

474
    use super::*;
475
    use crate::testing::{kat_inverse_greeting, kat_inverse_response, known_answer_test_encode};
476

477
    #[test]
478
    fn test_kat_inverse_greeting() {
2✔
479
        kat_inverse_greeting(&[
2✔
480
            (
2✔
481
                b"* OK [badcharset] ...\r\n".as_slice(),
2✔
482
                b"".as_slice(),
2✔
483
                Greeting::ok(Some(Code::BadCharset { allowed: vec![] }), "...").unwrap(),
2✔
484
            ),
2✔
485
            (
2✔
486
                b"* OK [UnSEEN 12345] ...\r\naaa".as_slice(),
2✔
487
                b"aaa".as_slice(),
2✔
488
                Greeting::ok(
2✔
489
                    Some(Code::Unseen(NonZeroU32::try_from(12345).unwrap())),
2✔
490
                    "...",
2✔
491
                )
2✔
492
                .unwrap(),
2✔
493
            ),
2✔
494
            (
2✔
495
                b"* OK [unseen 12345]  \r\n ".as_slice(),
2✔
496
                b" ".as_slice(),
2✔
497
                Greeting::ok(
2✔
498
                    Some(Code::Unseen(NonZeroU32::try_from(12345).unwrap())),
2✔
499
                    " ",
2✔
500
                )
2✔
501
                .unwrap(),
2✔
502
            ),
2✔
503
            (
2✔
504
                b"* PREAUTH [ALERT] hello\r\n".as_ref(),
2✔
505
                b"".as_ref(),
2✔
506
                Greeting::new(GreetingKind::PreAuth, Some(Code::Alert), "hello").unwrap(),
2✔
507
            ),
2✔
508
        ]);
2✔
509
    }
2✔
510

511
    #[test]
512
    fn test_kat_inverse_response_data() {
2✔
513
        kat_inverse_response(&[
2✔
514
            (
2✔
515
                b"* CAPABILITY IMAP4REV1\r\n".as_ref(),
2✔
516
                b"".as_ref(),
2✔
517
                Response::Data(Data::Capability(Vec1::from(Capability::Imap4Rev1))),
2✔
518
            ),
2✔
519
            (
2✔
520
                b"* LIST (\\Noselect) \"/\" bbb\r\n",
2✔
521
                b"",
2✔
522
                Response::Data(Data::List {
2✔
523
                    items: vec![FlagNameAttribute::Noselect],
2✔
524
                    delimiter: Some(QuotedChar::try_from('/').unwrap()),
2✔
525
                    mailbox: "bbb".try_into().unwrap(),
2✔
526
                }),
2✔
527
            ),
2✔
528
            (
2✔
529
                b"* SEARCH 1 2 3 42\r\n",
2✔
530
                b"",
2✔
531
                Response::Data(Data::Search(
2✔
532
                    vec![
2✔
533
                        1.try_into().unwrap(),
2✔
534
                        2.try_into().unwrap(),
2✔
535
                        3.try_into().unwrap(),
2✔
536
                        42.try_into().unwrap(),
2✔
537
                    ],
2✔
538
                    #[cfg(feature = "ext_condstore_qresync")]
2✔
539
                    None,
2✔
540
                )),
2✔
541
            ),
2✔
542
            (b"* 42 EXISTS\r\n", b"", Response::Data(Data::Exists(42))),
2✔
543
            (
2✔
544
                b"* 12345 RECENT\r\n",
2✔
545
                b"",
2✔
546
                Response::Data(Data::Recent(12345)),
2✔
547
            ),
2✔
548
            (
2✔
549
                b"* 123 EXPUNGE\r\n",
2✔
550
                b"",
2✔
551
                Response::Data(Data::Expunge(123.try_into().unwrap())),
2✔
552
            ),
2✔
553
        ]);
2✔
554
    }
2✔
555

556
    #[test]
557
    fn test_kat_inverse_response_status() {
2✔
558
        kat_inverse_response(&[
2✔
559
            // tagged; Ok, No, Bad
2✔
560
            (
2✔
561
                b"A1 OK [ALERT] hello\r\n".as_ref(),
2✔
562
                b"".as_ref(),
2✔
563
                Response::Status(
2✔
564
                    Status::ok(
2✔
565
                        Some(Tag::try_from("A1").unwrap()),
2✔
566
                        Some(Code::Alert),
2✔
567
                        "hello",
2✔
568
                    )
2✔
569
                    .unwrap(),
2✔
570
                ),
2✔
571
            ),
2✔
572
            (
2✔
573
                b"A1 NO [ALERT] hello\r\n",
2✔
574
                b"".as_ref(),
2✔
575
                Response::Status(
2✔
576
                    Status::no(
2✔
577
                        Some(Tag::try_from("A1").unwrap()),
2✔
578
                        Some(Code::Alert),
2✔
579
                        "hello",
2✔
580
                    )
2✔
581
                    .unwrap(),
2✔
582
                ),
2✔
583
            ),
2✔
584
            (
2✔
585
                b"A1 BAD [ALERT] hello\r\n",
2✔
586
                b"".as_ref(),
2✔
587
                Response::Status(
2✔
588
                    Status::bad(
2✔
589
                        Some(Tag::try_from("A1").unwrap()),
2✔
590
                        Some(Code::Alert),
2✔
591
                        "hello",
2✔
592
                    )
2✔
593
                    .unwrap(),
2✔
594
                ),
2✔
595
            ),
2✔
596
            (
2✔
597
                b"A1 OK hello\r\n",
2✔
598
                b"".as_ref(),
2✔
599
                Response::Status(
2✔
600
                    Status::ok(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(),
2✔
601
                ),
2✔
602
            ),
2✔
603
            (
2✔
604
                b"A1 NO hello\r\n",
2✔
605
                b"".as_ref(),
2✔
606
                Response::Status(
2✔
607
                    Status::no(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(),
2✔
608
                ),
2✔
609
            ),
2✔
610
            (
2✔
611
                b"A1 BAD hello\r\n",
2✔
612
                b"".as_ref(),
2✔
613
                Response::Status(
2✔
614
                    Status::bad(Some(Tag::try_from("A1").unwrap()), None, "hello").unwrap(),
2✔
615
                ),
2✔
616
            ),
2✔
617
            // untagged; Ok, No, Bad
2✔
618
            (
2✔
619
                b"* OK [ALERT] hello\r\n",
2✔
620
                b"".as_ref(),
2✔
621
                Response::Status(Status::ok(None, Some(Code::Alert), "hello").unwrap()),
2✔
622
            ),
2✔
623
            (
2✔
624
                b"* NO [ALERT] hello\r\n",
2✔
625
                b"".as_ref(),
2✔
626
                Response::Status(Status::no(None, Some(Code::Alert), "hello").unwrap()),
2✔
627
            ),
2✔
628
            (
2✔
629
                b"* BAD [ALERT] hello\r\n",
2✔
630
                b"".as_ref(),
2✔
631
                Response::Status(Status::bad(None, Some(Code::Alert), "hello").unwrap()),
2✔
632
            ),
2✔
633
            (
2✔
634
                b"* OK hello\r\n",
2✔
635
                b"".as_ref(),
2✔
636
                Response::Status(Status::ok(None, None, "hello").unwrap()),
2✔
637
            ),
2✔
638
            (
2✔
639
                b"* NO hello\r\n",
2✔
640
                b"".as_ref(),
2✔
641
                Response::Status(Status::no(None, None, "hello").unwrap()),
2✔
642
            ),
2✔
643
            (
2✔
644
                b"* BAD hello\r\n",
2✔
645
                b"".as_ref(),
2✔
646
                Response::Status(Status::bad(None, None, "hello").unwrap()),
2✔
647
            ),
2✔
648
            // bye
2✔
649
            (
2✔
650
                b"* BYE [ALERT] hello\r\n",
2✔
651
                b"".as_ref(),
2✔
652
                Response::Status(Status::bye(Some(Code::Alert), "hello").unwrap()),
2✔
653
            ),
2✔
654
        ]);
2✔
655
    }
2✔
656

657
    /*
658
    // TODO(#184)
659
    #[test]
660
    fn test_kat_inverse_continue() {
661
        kat_inverse_continue(&[
662
            (
663
                b"+ \x01\r\n".as_ref(),
664
                b"".as_ref(),
665
                Continue::basic(None, "\x01").unwrap(),
666
            ),
667
            (
668
                b"+ hello\r\n".as_ref(),
669
                b"".as_ref(),
670
                Continue::basic(None, "hello").unwrap(),
671
            ),
672
            (
673
                b"+ [READ-WRITE] hello\r\n",
674
                b"",
675
                Continue::basic(Some(Code::ReadWrite), "hello").unwrap(),
676
            ),
677
        ]);
678
    }
679
    */
680

681
    #[test]
682
    fn test_encode_body_structure() {
2✔
683
        let tests = [
2✔
684
            (
2✔
685
                BodyStructure::Single {
2✔
686
                    body: Body {
2✔
687
                        basic: BasicFields {
2✔
688
                            parameter_list: vec![],
2✔
689
                            id: NString(None),
2✔
690
                            description: NString::try_from("description").unwrap(),
2✔
691
                            content_transfer_encoding: IString::try_from("cte").unwrap(),
2✔
692
                            size: 123,
2✔
693
                        },
2✔
694
                        specific: SpecificFields::Basic {
2✔
695
                            r#type: IString::try_from("application").unwrap(),
2✔
696
                            subtype: IString::try_from("voodoo").unwrap(),
2✔
697
                        },
2✔
698
                    },
2✔
699
                    extension_data: None,
2✔
700
                },
2✔
701
                b"(\"application\" \"voodoo\" NIL NIL \"description\" \"cte\" 123)".as_ref(),
2✔
702
            ),
2✔
703
            (
2✔
704
                BodyStructure::Single {
2✔
705
                    body: Body {
2✔
706
                        basic: BasicFields {
2✔
707
                            parameter_list: vec![],
2✔
708
                            id: NString(None),
2✔
709
                            description: NString::try_from("description").unwrap(),
2✔
710
                            content_transfer_encoding: IString::try_from("cte").unwrap(),
2✔
711
                            size: 123,
2✔
712
                        },
2✔
713
                        specific: SpecificFields::Text {
2✔
714
                            subtype: IString::try_from("plain").unwrap(),
2✔
715
                            number_of_lines: 14,
2✔
716
                        },
2✔
717
                    },
2✔
718
                    extension_data: None,
2✔
719
                },
2✔
720
                b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14)",
2✔
721
            ),
2✔
722
            (
2✔
723
                BodyStructure::Single {
2✔
724
                    body: Body {
2✔
725
                        basic: BasicFields {
2✔
726
                            parameter_list: vec![],
2✔
727
                            id: NString(None),
2✔
728
                            description: NString::try_from("description").unwrap(),
2✔
729
                            content_transfer_encoding: IString::try_from("cte").unwrap(),
2✔
730
                            size: 123,
2✔
731
                        },
2✔
732
                        specific: SpecificFields::Text {
2✔
733
                            subtype: IString::try_from("plain").unwrap(),
2✔
734
                            number_of_lines: 14,
2✔
735
                        },
2✔
736
                    },
2✔
737
                    extension_data: Some(SinglePartExtensionData {
2✔
738
                        md5: NString::try_from("AABB").unwrap(),
2✔
739
                        tail: Some(Disposition {
2✔
740
                            disposition: None,
2✔
741
                            tail: Some(Language {
2✔
742
                                language: vec![],
2✔
743
                                tail: Some(Location {
2✔
744
                                    location: NString(None),
2✔
745
                                    extensions: vec![BodyExtension::List(Vec1::from(BodyExtension::Number(1337)))],
2✔
746
                                }),
2✔
747
                            }),
2✔
748
                        }),
2✔
749
                    }),
2✔
750
                },
2✔
751
                b"(\"TEXT\" \"plain\" NIL NIL \"description\" \"cte\" 123 14 \"AABB\" NIL NIL NIL (1337))",
2✔
752
            ),
2✔
753
        ];
2✔
754

755
        for test in tests {
8✔
756
            known_answer_test_encode(test);
6✔
757
        }
6✔
758
    }
2✔
759

760
    #[test]
761
    fn test_parse_response_negative() {
2✔
762
        let tests = [
2✔
763
            // TODO(#301,#184)
2✔
764
            // b"+ Nose[CAY a\r\n".as_ref()
2✔
765
        ];
2✔
766

767
        for test in tests {
2✔
768
            assert!(response(test).is_err());
×
769
        }
770
    }
2✔
771

772
    #[test]
773
    fn test_parse_resp_text_quirk() {
2✔
774
        #[cfg(not(feature = "quirk_missing_text"))]
2✔
775
        {
2✔
776
            assert!(resp_text(b"[IMAP4rev1]\r\n").is_err());
2✔
777
            assert!(resp_text(b"[IMAP4rev1]\r\n").is_err());
2✔
778
            assert!(resp_text(b"[IMAP4rev1] \r\n").is_err());
2✔
779
            assert!(resp_text(b"[IMAP4rev1]  \r\n").is_ok());
2✔
780
        }
2✔
781

2✔
782
        #[cfg(feature = "quirk_missing_text")]
2✔
783
        {
2✔
784
            assert!(resp_text(b"[IMAP4rev1]\r\n").is_ok());
2✔
785
            assert!(resp_text(b"[IMAP4rev1] \r\n").is_err());
2✔
786
            assert!(resp_text(b"[IMAP4rev1]  \r\n").is_ok());
2✔
787
        }
788
    }
2✔
789

790
    #[test]
791
    fn test_parse_resp_space_quirk() {
2✔
792
        assert!(response_data(b"* STATUS INBOX (MESSAGES 100 UNSEEN 0)\r\n").is_ok());
2✔
793
        assert!(response_data(b"* STATUS INBOX (MESSAGES 100 UNSEEN 0)  \r\n").is_err());
2✔
794

795
        #[cfg(not(feature = "quirk_trailing_space"))]
796
        assert!(response_data(b"* STATUS INBOX (MESSAGES 100 UNSEEN 0) \r\n").is_err());
797

798
        #[cfg(feature = "quirk_trailing_space")]
799
        assert!(response_data(b"* STATUS INBOX (MESSAGES 100 UNSEEN 0) \r\n").is_ok());
2✔
800
    }
2✔
801

802
    #[test]
803
    fn test_quirk_trailing_space_capability() {
2✔
804
        assert!(response_data(b"* CAPABILITY IMAP4REV1\r\n").is_ok());
2✔
805
        assert!(response_data(b"* CAPABILITY IMAP4REV1  \r\n").is_err());
2✔
806

807
        #[cfg(not(feature = "quirk_trailing_space_capability"))]
808
        assert!(response_data(b"* CAPABILITY IMAP4REV1 \r\n").is_err());
809

810
        #[cfg(feature = "quirk_trailing_space_capability")]
811
        assert!(response_data(b"* CAPABILITY IMAP4REV1 \r\n").is_ok());
2✔
812
    }
2✔
813
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc