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

duesee / imap-codec / 16085468327

05 Jul 2025 06:35AM UTC coverage: 90.913% (-0.8%) from 91.715%
16085468327

push

github

duesee
chore: fix Clippy lints

39 of 44 new or added lines in 17 files covered. (88.64%)

31 existing lines in 7 files now uncovered.

10315 of 11346 relevant lines covered (90.91%)

932.91 hits per line

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

86.86
/imap-codec/src/command.rs
1
use std::borrow::Cow;
2

3
#[cfg(not(feature = "quirk_crlf_relaxed"))]
4
use abnf_core::streaming::crlf;
5
#[cfg(feature = "quirk_crlf_relaxed")]
6
use abnf_core::streaming::crlf_relaxed as crlf;
7
use abnf_core::streaming::sp;
8
#[cfg(feature = "ext_condstore_qresync")]
9
use imap_types::command::{FetchModifier, SelectParameter, StoreModifier};
10
use imap_types::{
11
    auth::AuthMechanism,
12
    command::{Command, CommandBody},
13
    core::AString,
14
    extensions::binary::LiteralOrLiteral8,
15
    fetch::{Macro, MacroOrMessageDataItemNames},
16
    flag::{Flag, StoreResponse, StoreType},
17
    secret::Secret,
18
};
19
#[cfg(feature = "ext_condstore_qresync")]
20
use nom::character::streaming::char;
21
#[cfg(feature = "ext_condstore_qresync")]
22
use nom::sequence::separated_pair;
23
use nom::{
24
    branch::alt,
25
    bytes::streaming::{tag, tag_no_case},
26
    combinator::{map, opt, value},
27
    multi::{separated_list0, separated_list1},
28
    sequence::{delimited, preceded, terminated, tuple},
29
};
30

31
#[cfg(feature = "ext_condstore_qresync")]
32
use crate::core::nz_number;
33
#[cfg(feature = "ext_condstore_qresync")]
34
use crate::extensions::condstore_qresync::mod_sequence_value;
35
#[cfg(feature = "ext_condstore_qresync")]
36
use crate::extensions::condstore_qresync::mod_sequence_valzer;
37
#[cfg(feature = "ext_id")]
38
use crate::extensions::id::id;
39
#[cfg(feature = "ext_metadata")]
40
use crate::extensions::metadata::{getmetadata, setmetadata};
41
use crate::{
42
    auth::auth_type,
43
    core::{astring, base64, literal, tag_imap},
44
    datetime::date_time,
45
    decode::{IMAPErrorKind, IMAPResult},
46
    extensions::{
47
        binary::literal8,
48
        compress::compress,
49
        enable::enable,
50
        idle::idle,
51
        quota::{getquota, getquotaroot, setquota},
52
        r#move::r#move,
53
        sort::sort,
54
        thread::thread,
55
        uidplus::uid_expunge,
56
    },
57
    fetch::fetch_att,
58
    flag::{flag, flag_list},
59
    mailbox::{list_mailbox, mailbox},
60
    search::search,
61
    sequence::sequence_set,
62
    status::status_att,
63
};
64

65
/// `command = tag SP (
66
///                     command-any /
67
///                     command-auth /
68
///                     command-nonauth /
69
///                     command-select
70
///                   ) CRLF`
71
pub(crate) fn command(input: &[u8]) -> IMAPResult<&[u8], Command> {
1,474✔
72
    let mut parser_tag = terminated(tag_imap, sp);
1,474✔
73
    let mut parser_body = terminated(
1,474✔
74
        alt((command_any, command_auth, command_nonauth, command_select)),
1,474✔
75
        crlf,
76
    );
77

78
    let (remaining, obtained_tag) = parser_tag(input)?;
1,474✔
79

80
    match parser_body(remaining) {
1,458✔
81
        Ok((remaining, body)) => Ok((
1,382✔
82
            remaining,
1,382✔
83
            Command {
1,382✔
84
                tag: obtained_tag,
1,382✔
85
                body,
1,382✔
86
            },
1,382✔
87
        )),
1,382✔
88
        Err(mut error) => {
76✔
89
            // If we got an `IMAPErrorKind::Literal`, we fill in the missing `tag`.
90
            if let nom::Err::Error(ref mut err) | nom::Err::Failure(ref mut err) = error {
76✔
91
                if let IMAPErrorKind::Literal { ref mut tag, .. } = err.kind {
20✔
92
                    *tag = Some(obtained_tag);
12✔
93
                }
12✔
94
            }
56✔
95

96
            Err(error)
76✔
97
        }
98
    }
99
}
1,474✔
100

101
// # Command Any
102

103
/// ```abnf
104
/// command-any = "CAPABILITY" /
105
///               "LOGOUT" /
106
///               "NOOP" /
107
///               x-command /
108
///               id ; adds id command to command_any (See RFC 2971)
109
/// ```
110
///
111
/// Note: Valid in all states
112
pub(crate) fn command_any(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,458✔
113
    alt((
1,458✔
114
        value(CommandBody::Capability, tag_no_case(b"CAPABILITY")),
1,458✔
115
        value(CommandBody::Logout, tag_no_case(b"LOGOUT")),
1,458✔
116
        value(CommandBody::Noop, tag_no_case(b"NOOP")),
1,458✔
117
        // x-command = "X" atom <experimental command arguments>
118
        #[cfg(feature = "ext_id")]
119
        map(id, |parameters| CommandBody::Id { parameters }),
1,466✔
120
    ))(input)
1,458✔
121
}
1,458✔
122

123
// # Command Auth
124

125
/// ```abnf
126
/// command-auth = append /
127
///                create /
128
///                delete /
129
///                examine /
130
///                list /
131
///                lsub /
132
///                rename /
133
///                select /
134
///                status /
135
///                subscribe /
136
///                unsubscribe /
137
///                idle /         ; RFC 2177
138
///                enable /       ; RFC 5161
139
///                compress /     ; RFC 4978
140
///                getquota /     ; RFC 9208
141
///                getquotaroot / ; RFC 9208
142
///                setquota /     ; RFC 9208
143
///                setmetadata /  ; RFC 5464
144
///                getmetadata    ; RFC 5464
145
/// ```
146
///
147
/// Note: Valid only in Authenticated or Selected state
148
pub(crate) fn command_auth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,216✔
149
    alt((
1,216✔
150
        append,
1,216✔
151
        create,
1,216✔
152
        delete,
1,216✔
153
        examine,
1,216✔
154
        list,
1,216✔
155
        lsub,
1,216✔
156
        rename,
1,216✔
157
        select,
1,216✔
158
        status,
1,216✔
159
        subscribe,
1,216✔
160
        unsubscribe,
1,216✔
161
        idle,
1,216✔
162
        enable,
1,216✔
163
        compress,
1,216✔
164
        getquota,
1,216✔
165
        getquotaroot,
1,216✔
166
        setquota,
1,216✔
167
        #[cfg(feature = "ext_metadata")]
1,216✔
168
        setmetadata,
1,216✔
169
        #[cfg(feature = "ext_metadata")]
1,216✔
170
        getmetadata,
1,216✔
171
    ))(input)
1,216✔
172
}
1,216✔
173

174
/// `append = "APPEND" SP mailbox [SP flag-list] [SP date-time] SP literal`
175
pub(crate) fn append(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,216✔
176
    let mut parser = tuple((
1,216✔
177
        tag_no_case(b"APPEND "),
1,216✔
178
        mailbox,
1,216✔
179
        opt(preceded(sp, flag_list)),
1,216✔
180
        opt(preceded(sp, date_time)),
1,216✔
181
        sp,
1,216✔
182
        alt((
1,216✔
183
            map(literal, LiteralOrLiteral8::Literal),
1,216✔
184
            map(literal8, LiteralOrLiteral8::Literal8),
1,216✔
185
        )),
1,216✔
186
    ));
1,216✔
187

188
    let (remaining, (_, mailbox, flags, date, _, message)) = parser(input)?;
1,216✔
189

190
    Ok((
×
191
        remaining,
×
192
        CommandBody::Append {
×
193
            mailbox,
×
194
            flags: flags.unwrap_or_default(),
×
195
            date,
×
196
            message,
×
197
        },
×
198
    ))
×
199
}
1,216✔
200

201
/// `create = "CREATE" SP mailbox`
202
///
203
/// Note: Use of INBOX gives a NO error
204
pub(crate) fn create(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,216✔
205
    let mut parser = preceded(tag_no_case(b"CREATE "), mailbox);
1,216✔
206

207
    let (remaining, mailbox) = parser(input)?;
1,216✔
208

209
    Ok((remaining, CommandBody::Create { mailbox }))
32✔
210
}
1,216✔
211

212
/// `delete = "DELETE" SP mailbox`
213
///
214
/// Note: Use of INBOX gives a NO error
215
pub(crate) fn delete(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,184✔
216
    let mut parser = preceded(tag_no_case(b"DELETE "), mailbox);
1,184✔
217

218
    let (remaining, mailbox) = parser(input)?;
1,184✔
219

220
    Ok((remaining, CommandBody::Delete { mailbox }))
96✔
221
}
1,184✔
222

223
/// ```abnf
224
/// examine = "EXAMINE" SP mailbox [select-params]
225
///                                ^^^^^^^^^^^^^^^
226
///                                |
227
///                                RFC 4466: modifies the original IMAP EXAMINE command to accept optional parameters
228
/// ```
229
pub(crate) fn examine(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,088✔
230
    let mut parser = preceded(tag_no_case(b"EXAMINE "), mailbox);
1,088✔
231

232
    let (remaining, mailbox) = parser(input)?;
1,088✔
233

234
    #[cfg(feature = "ext_condstore_qresync")]
235
    let (remaining, parameters) =
16✔
236
        map(opt(select_params), |params| params.unwrap_or_default())(remaining)?;
18✔
237

238
    Ok((
16✔
239
        remaining,
16✔
240
        CommandBody::Examine {
16✔
241
            mailbox,
16✔
242
            #[cfg(feature = "ext_condstore_qresync")]
16✔
243
            parameters,
16✔
244
        },
16✔
245
    ))
16✔
246
}
1,088✔
247

248
/// `list = "LIST" SP mailbox SP list-mailbox`
249
pub(crate) fn list(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,072✔
250
    let mut parser = tuple((tag_no_case(b"LIST "), mailbox, sp, list_mailbox));
1,072✔
251

252
    let (remaining, (_, reference, _, mailbox_wildcard)) = parser(input)?;
1,072✔
253

254
    Ok((
208✔
255
        remaining,
208✔
256
        CommandBody::List {
208✔
257
            reference,
208✔
258
            mailbox_wildcard,
208✔
259
        },
208✔
260
    ))
208✔
261
}
1,072✔
262

263
/// `lsub = "LSUB" SP mailbox SP list-mailbox`
264
pub(crate) fn lsub(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
864✔
265
    let mut parser = tuple((tag_no_case(b"LSUB "), mailbox, sp, list_mailbox));
864✔
266

267
    let (remaining, (_, reference, _, mailbox_wildcard)) = parser(input)?;
864✔
268

269
    Ok((
32✔
270
        remaining,
32✔
271
        CommandBody::Lsub {
32✔
272
            reference,
32✔
273
            mailbox_wildcard,
32✔
274
        },
32✔
275
    ))
32✔
276
}
864✔
277

278
/// `rename = "RENAME" SP mailbox SP mailbox`
279
///
280
/// Note: Use of INBOX as a destination gives a NO error
281
pub(crate) fn rename(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
832✔
282
    let mut parser = tuple((tag_no_case(b"RENAME "), mailbox, sp, mailbox));
832✔
283

284
    let (remaining, (_, mailbox, _, new_mailbox)) = parser(input)?;
832✔
285

286
    Ok((
48✔
287
        remaining,
48✔
288
        CommandBody::Rename {
48✔
289
            from: mailbox,
48✔
290
            to: new_mailbox,
48✔
291
        },
48✔
292
    ))
48✔
293
}
832✔
294

295
/// ```abnf
296
/// select = "SELECT" SP mailbox [select-params]
297
///                              ^^^^^^^^^^^^^^^
298
///                              |
299
///                              RFC 4466: modifies the original IMAP SELECT command to accept optional parameters
300
/// ```
301
pub(crate) fn select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
784✔
302
    let mut parser = preceded(tag_no_case(b"SELECT "), mailbox);
784✔
303

304
    let (remaining, mailbox) = parser(input)?;
784✔
305

306
    #[cfg(feature = "ext_condstore_qresync")]
307
    let (remaining, parameters) =
48✔
308
        map(opt(select_params), |params| params.unwrap_or_default())(remaining)?;
60✔
309

310
    Ok((
48✔
311
        remaining,
48✔
312
        CommandBody::Select {
48✔
313
            mailbox,
48✔
314
            #[cfg(feature = "ext_condstore_qresync")]
48✔
315
            parameters,
48✔
316
        },
48✔
317
    ))
48✔
318
}
784✔
319

320
/// FROM RFC4466:
321
///
322
/// ```abnf
323
/// select-params = SP "(" select-param *(SP select-param) ")"
324
/// ```
325
#[cfg(feature = "ext_condstore_qresync")]
326
pub(crate) fn select_params(input: &[u8]) -> IMAPResult<&[u8], Vec<SelectParameter>> {
64✔
327
    delimited(tag(" ("), separated_list1(sp, select_param), tag(")"))(input)
64✔
328
}
64✔
329

330
/// FROM RFC4466:
331
///
332
/// ```abnf
333
/// select-param = select-param-name [SP select-param-value]
334
///                ;; a parameter to SELECT may contain one or more atoms and/or strings and/or lists.
335
///
336
/// select-param-name = tagged-ext-label
337
///
338
/// select-param-value = tagged-ext-val
339
///                      ;; This non-terminal shows recommended syntax for future extensions.
340
/// ```
341
///
342
/// FROM RFC 7162 (CONDSTORE/QRESYNC):
343
///
344
/// ```abnf
345
/// select-param =/ condstore-param
346
///              ;; Conforms to the generic "select-param" non-terminal syntax defined in [RFC4466].
347
///
348
/// condstore-param = "CONDSTORE"
349
///
350
/// select-param =/ "QRESYNC" SP "("
351
///                   uidvalidity SP
352
///                   mod-sequence-value [SP known-uids]
353
///                   [SP seq-match-data]
354
///                 ")"
355
///                 ;; Conforms to the generic select-param syntax defined in [RFC4466].
356
///
357
/// uidvalidity = nz-number
358
///
359
/// known-uids = sequence-set
360
///              ;; Sequence of UIDs; "*" is not allowed.
361
///
362
/// seq-match-data = "(" known-sequence-set SP known-uid-set ")"
363
///
364
/// known-sequence-set = sequence-set
365
///                    ;; Set of message numbers corresponding to
366
///                    ;; the UIDs in known-uid-set, in ascending order.
367
///                    ;; * is not allowed.
368
///
369
/// known-uid-set = sequence-set
370
///                 ;; Set of UIDs corresponding to the messages in
371
///                 ;; known-sequence-set, in ascending order.
372
///                 ;; * is not allowed.
373
/// ```
374
#[cfg(feature = "ext_condstore_qresync")]
375
pub(crate) fn select_param(input: &[u8]) -> IMAPResult<&[u8], SelectParameter> {
×
376
    alt((
×
377
        value(SelectParameter::CondStore, tag_no_case("CONDSTORE")),
×
378
        map(
×
379
            delimited(
×
380
                tag_no_case("QRESYNC ("),
×
381
                tuple((
×
382
                    terminated(nz_number, sp),
×
383
                    mod_sequence_value,
×
384
                    opt(preceded(sp, sequence_set)),
×
385
                    opt(preceded(sp, separated_pair(sequence_set, sp, sequence_set))),
×
386
                )),
×
387
                char(')'),
×
388
            ),
389
            |(uid_validity, mod_sequence_value, known_uids, seq_match_data)| {
×
390
                SelectParameter::QResync {
×
391
                    uid_validity,
×
392
                    mod_sequence_value,
×
393
                    known_uids,
×
394
                    seq_match_data,
×
395
                }
×
396
            },
×
397
        ),
398
    ))(input)
×
399
}
×
400

401
/// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"`
402
pub(crate) fn status(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
716✔
403
    let mut parser = tuple((
716✔
404
        tag_no_case(b"STATUS "),
716✔
405
        mailbox,
716✔
406
        delimited(tag(b" ("), separated_list0(sp, status_att), tag(b")")),
716✔
407
    ));
716✔
408

409
    let (remaining, (_, mailbox, item_names)) = parser(input)?;
716✔
410

411
    Ok((
20✔
412
        remaining,
20✔
413
        CommandBody::Status {
20✔
414
            mailbox,
20✔
415
            item_names: item_names.into(),
20✔
416
        },
20✔
417
    ))
20✔
418
}
716✔
419

420
/// `subscribe = "SUBSCRIBE" SP mailbox`
421
pub(crate) fn subscribe(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
696✔
422
    let mut parser = preceded(tag_no_case(b"SUBSCRIBE "), mailbox);
696✔
423

424
    let (remaining, mailbox) = parser(input)?;
696✔
425

426
    Ok((remaining, CommandBody::Subscribe { mailbox }))
16✔
427
}
696✔
428

429
/// `unsubscribe = "UNSUBSCRIBE" SP mailbox`
430
pub(crate) fn unsubscribe(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
680✔
431
    let mut parser = preceded(tag_no_case(b"UNSUBSCRIBE "), mailbox);
680✔
432

433
    let (remaining, mailbox) = parser(input)?;
680✔
434

435
    Ok((remaining, CommandBody::Unsubscribe { mailbox }))
16✔
436
}
680✔
437

438
// # Command NonAuth
439

440
/// `command-nonauth = login / authenticate / "STARTTLS"`
441
///
442
/// Note: Valid only when in Not Authenticated state
443
pub(crate) fn command_nonauth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
508✔
444
    let mut parser = alt((
508✔
445
        login,
446
        map(authenticate, |(mechanism, initial_response)| {
508✔
447
            CommandBody::Authenticate {
×
448
                mechanism,
×
449
                initial_response,
×
450
            }
×
UNCOV
451
        }),
×
452
        #[cfg(feature = "starttls")]
453
        value(CommandBody::StartTLS, tag_no_case(b"STARTTLS")),
508✔
454
    ));
455

456
    let (remaining, parsed_command_nonauth) = parser(input)?;
508✔
457

458
    Ok((remaining, parsed_command_nonauth))
136✔
459
}
508✔
460

461
/// `login = "LOGIN" SP userid SP password`
462
pub(crate) fn login(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
508✔
463
    let mut parser = tuple((tag_no_case(b"LOGIN"), sp, userid, sp, password));
508✔
464

465
    let (remaining, (_, _, username, _, password)) = parser(input)?;
508✔
466

467
    Ok((
104✔
468
        remaining,
104✔
469
        CommandBody::Login {
104✔
470
            username,
104✔
471
            password: Secret::new(password),
104✔
472
        },
104✔
473
    ))
104✔
474
}
508✔
475

476
#[inline]
477
/// `userid = astring`
478
pub(crate) fn userid(input: &[u8]) -> IMAPResult<&[u8], AString> {
104✔
479
    astring(input)
104✔
480
}
104✔
481

482
#[inline]
483
/// `password = astring`
484
pub(crate) fn password(input: &[u8]) -> IMAPResult<&[u8], AString> {
104✔
485
    astring(input)
104✔
486
}
104✔
487

488
/// `authenticate = "AUTHENTICATE" SP auth-type *(CRLF base64)` (edited)
489
///
490
/// ```text
491
///                                            Added by SASL-IR
492
///                                            |
493
///                                            vvvvvvvvvvvvvvvvvvv
494
/// authenticate = "AUTHENTICATE" SP auth-type [SP (base64 / "=")] *(CRLF base64)
495
///                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
496
///                |
497
///                This is parsed here.
498
///                CRLF is parsed by upper command parser.
499
/// ```
500
#[allow(clippy::type_complexity)]
501
pub(crate) fn authenticate(
404✔
502
    input: &[u8],
404✔
503
) -> IMAPResult<&[u8], (AuthMechanism, Option<Secret<Cow<[u8]>>>)> {
404✔
504
    let mut parser = tuple((
404✔
505
        tag_no_case(b"AUTHENTICATE "),
404✔
506
        auth_type,
507
        opt(preceded(
404✔
508
            sp,
509
            alt((
404✔
510
                map(base64, |data| Secret::new(Cow::Owned(data))),
404✔
511
                value(Secret::new(Cow::Borrowed(&b""[..])), tag("=")),
404✔
512
            )),
513
        )),
514
    ));
515

516
    let (remaining, (_, auth_type, raw_data)) = parser(input)?;
404✔
517

518
    // Server must send continuation ("+ ") at this point...
519

520
    Ok((remaining, (auth_type, raw_data)))
×
521
}
404✔
522

523
// # Command Select
524

525
/// `command-select = "CHECK" /
526
///                   "CLOSE" /
527
///                   "EXPUNGE" /
528
///                   copy /
529
///                   fetch /
530
///                   store /
531
///                   uid /
532
///                   search`
533
///
534
/// Note: Valid only when in Selected state
535
pub(crate) fn command_select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
372✔
536
    alt((
372✔
537
        value(CommandBody::Check, tag_no_case(b"CHECK")),
372✔
538
        value(CommandBody::Close, tag_no_case(b"CLOSE")),
372✔
539
        value(CommandBody::Expunge, tag_no_case(b"EXPUNGE")),
372✔
540
        uid_expunge,
372✔
541
        copy,
372✔
542
        fetch,
372✔
543
        store,
372✔
544
        uid,
372✔
545
        search,
372✔
546
        sort,
372✔
547
        thread,
372✔
548
        value(CommandBody::Unselect, tag_no_case(b"UNSELECT")),
372✔
549
        r#move,
372✔
550
    ))(input)
372✔
551
}
372✔
552

553
/// `copy = "COPY" SP sequence-set SP mailbox`
554
pub(crate) fn copy(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
316✔
555
    let mut parser = tuple((tag_no_case(b"COPY"), sp, sequence_set, sp, mailbox));
316✔
556

557
    let (remaining, (_, _, sequence_set, _, mailbox)) = parser(input)?;
316✔
558

559
    Ok((
48✔
560
        remaining,
48✔
561
        CommandBody::Copy {
48✔
562
            sequence_set,
48✔
563
            mailbox,
48✔
564
            uid: false,
48✔
565
        },
48✔
566
    ))
48✔
567
}
316✔
568

569
/// ```abnf
570
/// fetch = "FETCH" SP sequence-set SP ("ALL" /
571
///                                     "FULL" /
572
///                                     "FAST" /
573
///                                     fetch-att / "(" fetch-att *(SP fetch-att) ")")
574
///                                     [fetch-modifiers] ; FROM RFC 4466
575
/// ```
576
pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
270✔
577
    let mut parser = tuple((
270✔
578
        tag_no_case(b"FETCH"),
270✔
579
        preceded(sp, sequence_set),
270✔
580
        preceded(
270✔
581
            sp,
582
            alt((
270✔
583
                value(
270✔
584
                    MacroOrMessageDataItemNames::Macro(Macro::All),
270✔
585
                    tag_no_case(b"ALL"),
270✔
586
                ),
587
                value(
270✔
588
                    MacroOrMessageDataItemNames::Macro(Macro::Fast),
270✔
589
                    tag_no_case(b"FAST"),
270✔
590
                ),
591
                value(
270✔
592
                    MacroOrMessageDataItemNames::Macro(Macro::Full),
270✔
593
                    tag_no_case(b"FULL"),
270✔
594
                ),
595
                map(fetch_att, |fetch_att| {
274✔
596
                    MacroOrMessageDataItemNames::MessageDataItemNames(vec![fetch_att])
32✔
597
                }),
32✔
598
                map(
270✔
599
                    delimited(tag(b"("), separated_list0(sp, fetch_att), tag(b")")),
270✔
600
                    MacroOrMessageDataItemNames::MessageDataItemNames,
601
                ),
602
            )),
603
        ),
604
        #[cfg(feature = "ext_condstore_qresync")]
605
        map(opt(fetch_modifiers), Option::unwrap_or_default),
270✔
606
    ));
607

608
    #[cfg(not(feature = "ext_condstore_qresync"))]
609
    let (remaining, (_, sequence_set, macro_or_item_names)) = parser(input)?;
610

611
    #[cfg(feature = "ext_condstore_qresync")]
612
    let (remaining, (_, sequence_set, macro_or_item_names, modifiers)) = parser(input)?;
270✔
613

614
    Ok((
66✔
615
        remaining,
66✔
616
        CommandBody::Fetch {
66✔
617
            sequence_set,
66✔
618
            macro_or_item_names,
66✔
619
            uid: false,
66✔
620
            #[cfg(feature = "ext_condstore_qresync")]
66✔
621
            modifiers,
66✔
622
        },
66✔
623
    ))
66✔
624
}
270✔
625

626
#[cfg(feature = "ext_condstore_qresync")]
627
/// From RFC 4466:
628
///
629
/// ```abnf
630
/// fetch-modifiers = SP "(" fetch-modifier *(SP fetch-modifier) ")"
631
/// ```
632
pub(crate) fn fetch_modifiers(input: &[u8]) -> IMAPResult<&[u8], Vec<FetchModifier>> {
66✔
633
    delimited(tag(" ("), separated_list1(sp, fetch_modifier), char(')'))(input)
66✔
634
}
66✔
635

636
#[cfg(feature = "ext_condstore_qresync")]
637
/// From RFC 4466:
638
///
639
/// ```abnf
640
/// fetch-modifier = fetch-modifier-name [ SP fetch-modif-params ]
641
///
642
/// fetch-modif-params = tagged-ext-val
643
///                      ;; This non-terminal shows recommended syntax
644
///                      ;; for future extensions.
645
///
646
/// fetch-modifier-name = tagged-ext-label
647
/// ```
648
///
649
/// From RFC 7162 (CONDSTORE/QRESYNC):
650
///
651
/// ```abnf
652
/// fetch-modifier =/ chgsince-fetch-mod
653
///                   ;; Conforms to the generic "fetch-modifier" syntax defined in [RFC4466].
654
///
655
/// chgsince-fetch-mod = "CHANGEDSINCE" SP mod-sequence-value
656
///                      ;; CHANGEDSINCE FETCH modifier conforms to the fetch-modifier syntax.
657
///
658
/// rexpunges-fetch-mod = "VANISHED"
659
///                     ;; VANISHED UID FETCH modifier conforms to the fetch-modifier syntax defined in [RFC4466].
660
///                     ;; It is only allowed in the UID FETCH command.
661
/// ```
662
pub(crate) fn fetch_modifier(input: &[u8]) -> IMAPResult<&[u8], FetchModifier> {
×
663
    alt((
×
664
        map(
×
665
            preceded(tag_no_case("CHANGEDSINCE "), mod_sequence_value),
×
666
            FetchModifier::ChangedSince,
×
667
        ),
×
668
        value(FetchModifier::Vanished, tag_no_case("VANISHED")),
×
669
    ))(input)
×
670
}
×
671

672
/// ```abnf
673
/// store = "STORE" SP sequence-set [store-modifiers] SP store-att-flags
674
///                                 ^^^^^^^^^^^^^^^^^
675
///                                 |
676
///                                 RFC 4466
677
/// ```
678
pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
204✔
679
    let mut parser = tuple((
204✔
680
        tag_no_case(b"STORE"),
204✔
681
        preceded(sp, sequence_set),
204✔
682
        #[cfg(feature = "ext_condstore_qresync")]
204✔
683
        map(opt(store_modifiers), Option::unwrap_or_default),
204✔
684
        preceded(sp, store_att_flags),
204✔
685
    ));
204✔
686

687
    #[cfg(not(feature = "ext_condstore_qresync"))]
688
    let (remaining, (_, sequence_set, (kind, response, flags))) = parser(input)?;
689

690
    #[cfg(feature = "ext_condstore_qresync")]
691
    let (remaining, (_, sequence_set, modifiers, (kind, response, flags))) = parser(input)?;
204✔
692

693
    Ok((
32✔
694
        remaining,
32✔
695
        CommandBody::Store {
32✔
696
            sequence_set,
32✔
697
            kind,
32✔
698
            response,
32✔
699
            flags,
32✔
700
            uid: false,
32✔
701
            #[cfg(feature = "ext_condstore_qresync")]
32✔
702
            modifiers,
32✔
703
        },
32✔
704
    ))
32✔
705
}
204✔
706

707
#[cfg(feature = "ext_condstore_qresync")]
708
/// From RFC 4466:
709
///
710
/// ```abnf
711
/// store-modifiers = SP "(" store-modifier *(SP store-modifier) ")"
712
/// ```
713
pub(crate) fn store_modifiers(input: &[u8]) -> IMAPResult<&[u8], Vec<StoreModifier>> {
32✔
714
    delimited(tag(" ("), separated_list1(sp, store_modifier), char(')'))(input)
32✔
715
}
32✔
716

717
#[cfg(feature = "ext_condstore_qresync")]
718
/// From RFC 4466:
719
///
720
/// ```abnf
721
/// store-modifier = store-modifier-name [SP store-modif-params]
722
///
723
/// store-modif-params = tagged-ext-val
724
///
725
/// store-modifier-name = tagged-ext-label
726
/// ```
727
///
728
/// From RFC 7162 (CONDSTORE/QRESYNC):
729
///
730
/// ```abnf
731
/// store-modifier =/ "UNCHANGEDSINCE" SP mod-sequence-valzer
732
///                ;; Only a single "UNCHANGEDSINCE" may be specified in a STORE operation.
733
/// ```
734
pub(crate) fn store_modifier(input: &[u8]) -> IMAPResult<&[u8], StoreModifier> {
×
735
    map(
×
736
        preceded(tag_no_case(b"UNCHANGEDSINCE "), mod_sequence_valzer),
×
737
        StoreModifier::UnchangedSince,
×
738
    )(input)
×
739
}
×
740

741
/// `store-att-flags = (["+" / "-"] "FLAGS" [".SILENT"]) SP (flag-list / (flag *(SP flag)))`
742
pub(crate) fn store_att_flags(
32✔
743
    input: &[u8],
32✔
744
) -> IMAPResult<&[u8], (StoreType, StoreResponse, Vec<Flag>)> {
32✔
745
    let mut parser = tuple((
32✔
746
        tuple((
32✔
747
            map(
32✔
748
                opt(alt((
32✔
749
                    value(StoreType::Add, tag(b"+")),
32✔
750
                    value(StoreType::Remove, tag(b"-")),
32✔
751
                ))),
32✔
752
                |type_| match type_ {
32✔
753
                    Some(type_) => type_,
32✔
754
                    None => StoreType::Replace,
×
755
                },
32✔
756
            ),
757
            tag_no_case(b"FLAGS"),
32✔
758
            map(opt(tag_no_case(b".SILENT")), |x| match x {
36✔
759
                Some(_) => StoreResponse::Silent,
×
760
                None => StoreResponse::Answer,
32✔
761
            }),
32✔
762
        )),
763
        sp,
764
        alt((flag_list, separated_list1(sp, flag))),
32✔
765
    ));
766

767
    let (remaining, ((store_type, _, store_response), _, flag_list)) = parser(input)?;
32✔
768

769
    Ok((remaining, (store_type, store_response, flag_list)))
32✔
770
}
32✔
771

772
/// `uid = "UID" SP (copy / fetch / search / store)`
773
///
774
/// Note: Unique identifiers used instead of message sequence numbers
775
pub(crate) fn uid(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
168✔
776
    let mut parser = tuple((
168✔
777
        tag_no_case(b"UID"),
168✔
778
        sp,
168✔
779
        alt((copy, fetch, search, store, r#move)),
168✔
780
    ));
168✔
781

782
    let (remaining, (_, _, mut cmd)) = parser(input)?;
168✔
783

784
    match cmd {
20✔
785
        CommandBody::Copy { ref mut uid, .. }
×
786
        | CommandBody::Fetch { ref mut uid, .. }
16✔
787
        | CommandBody::Search { ref mut uid, .. }
×
788
        | CommandBody::Store { ref mut uid, .. }
×
789
        | CommandBody::Move { ref mut uid, .. } => *uid = true,
20✔
790
        _ => unreachable!(),
×
791
    }
792

793
    Ok((remaining, cmd))
20✔
794
}
168✔
795

796
#[cfg(test)]
797
mod tests {
798
    use std::num::NonZeroU32;
799

800
    use imap_types::{
801
        core::Tag,
802
        fetch::{MessageDataItemName, Section},
803
    };
804

805
    use super::*;
806
    use crate::{encode::Encoder, CommandCodec};
807

808
    #[test]
809
    fn test_parse_fetch() {
2✔
810
        println!("{:#?}", fetch(b"fetch 1:1 (flags)???"));
2✔
811
    }
2✔
812

813
    #[test]
814
    fn test_parse_fetch_att() {
2✔
815
        let tests = [
2✔
816
            (MessageDataItemName::Envelope, "ENVELOPE???"),
2✔
817
            (MessageDataItemName::Flags, "FLAGS???"),
2✔
818
            (MessageDataItemName::InternalDate, "INTERNALDATE???"),
2✔
819
            (MessageDataItemName::Rfc822, "RFC822???"),
2✔
820
            (MessageDataItemName::Rfc822Header, "RFC822.HEADER???"),
2✔
821
            (MessageDataItemName::Rfc822Size, "RFC822.SIZE???"),
2✔
822
            (MessageDataItemName::Rfc822Text, "RFC822.TEXT???"),
2✔
823
            (MessageDataItemName::Body, "BODY???"),
2✔
824
            (MessageDataItemName::BodyStructure, "BODYSTRUCTURE???"),
2✔
825
            (MessageDataItemName::Uid, "UID???"),
2✔
826
            (
2✔
827
                MessageDataItemName::BodyExt {
2✔
828
                    partial: None,
2✔
829
                    peek: false,
2✔
830
                    section: None,
2✔
831
                },
2✔
832
                "BODY[]???",
2✔
833
            ),
2✔
834
            (
2✔
835
                MessageDataItemName::BodyExt {
2✔
836
                    partial: None,
2✔
837
                    peek: true,
2✔
838
                    section: None,
2✔
839
                },
2✔
840
                "BODY.PEEK[]???",
2✔
841
            ),
2✔
842
            (
2✔
843
                MessageDataItemName::BodyExt {
2✔
844
                    partial: None,
2✔
845
                    peek: true,
2✔
846
                    section: Some(Section::Text(None)),
2✔
847
                },
2✔
848
                "BODY.PEEK[TEXT]???",
2✔
849
            ),
2✔
850
            (
2✔
851
                MessageDataItemName::BodyExt {
2✔
852
                    partial: Some((42, NonZeroU32::try_from(1337).unwrap())),
2✔
853
                    peek: true,
2✔
854
                    section: Some(Section::Text(None)),
2✔
855
                },
2✔
856
                "BODY.PEEK[TEXT]<42.1337>???",
2✔
857
            ),
2✔
858
        ];
2✔
859

860
        let expected_remainder = "???".as_bytes();
2✔
861

862
        for (expected, test) in tests {
30✔
863
            let (got_remainder, got) = fetch_att(test.as_bytes()).unwrap();
28✔
864

865
            assert_eq!(expected, got);
28✔
866
            assert_eq!(expected_remainder, got_remainder);
28✔
867
        }
868
    }
2✔
869

870
    #[test]
871
    fn test_that_empty_ir_is_encoded_correctly() {
2✔
872
        let command = Command::new(
2✔
873
            Tag::try_from("A").unwrap(),
2✔
874
            CommandBody::Authenticate {
2✔
875
                mechanism: AuthMechanism::Plain,
2✔
876
                initial_response: Some(Secret::new(Cow::Borrowed(&b""[..]))),
2✔
877
            },
2✔
878
        )
879
        .unwrap();
2✔
880

881
        let buffer = CommandCodec::default().encode(&command).dump();
2✔
882

883
        assert_eq!(buffer, b"A AUTHENTICATE PLAIN =\r\n")
2✔
884
    }
2✔
885
}
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

© 2025 Coveralls, Inc