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

duesee / imap-codec / 18634705484

19 Oct 2025 06:56PM UTC coverage: 91.776% (+0.008%) from 91.768%
18634705484

push

github

duesee
feat: Implement `UTF8={ACCEPT,ONLY}`

44 of 66 new or added lines in 11 files covered. (66.67%)

219 existing lines in 3 files now uncovered.

10311 of 11235 relevant lines covered (91.78%)

941.33 hits per line

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

85.53
/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
        r#move::r#move,
52
        quota::{getquota, getquotaroot, setquota},
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(
×
386
                        sp,
×
387
                        delimited(
×
UNCOV
388
                            char('('),
×
389
                            separated_pair(sequence_set, sp, sequence_set),
×
390
                            char(')'),
×
391
                        ),
×
392
                    )),
×
393
                )),
×
394
                char(')'),
×
395
            ),
396
            |(uid_validity, mod_sequence_value, known_uids, seq_match_data)| {
×
UNCOV
397
                SelectParameter::QResync {
×
398
                    uid_validity,
×
399
                    mod_sequence_value,
×
UNCOV
400
                    known_uids,
×
UNCOV
401
                    seq_match_data,
×
UNCOV
402
                }
×
UNCOV
403
            },
×
404
        ),
UNCOV
405
    ))(input)
×
UNCOV
406
}
×
407

408
/// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"`
409
pub(crate) fn status(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
716✔
410
    let mut parser = tuple((
716✔
411
        tag_no_case(b"STATUS "),
716✔
412
        mailbox,
716✔
413
        delimited(tag(b" ("), separated_list0(sp, status_att), tag(b")")),
716✔
414
    ));
716✔
415

416
    let (remaining, (_, mailbox, item_names)) = parser(input)?;
716✔
417

418
    Ok((
20✔
419
        remaining,
20✔
420
        CommandBody::Status {
20✔
421
            mailbox,
20✔
422
            item_names: item_names.into(),
20✔
423
        },
20✔
424
    ))
20✔
425
}
716✔
426

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

431
    let (remaining, mailbox) = parser(input)?;
696✔
432

433
    Ok((remaining, CommandBody::Subscribe { mailbox }))
16✔
434
}
696✔
435

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

440
    let (remaining, mailbox) = parser(input)?;
680✔
441

442
    Ok((remaining, CommandBody::Unsubscribe { mailbox }))
16✔
443
}
680✔
444

445
// # Command NonAuth
446

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

463
    let (remaining, parsed_command_nonauth) = parser(input)?;
508✔
464

465
    Ok((remaining, parsed_command_nonauth))
136✔
466
}
508✔
467

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

472
    let (remaining, (_, _, username, _, password)) = parser(input)?;
508✔
473

474
    Ok((
104✔
475
        remaining,
104✔
476
        CommandBody::Login {
104✔
477
            username,
104✔
478
            password: Secret::new(password),
104✔
479
        },
104✔
480
    ))
104✔
481
}
508✔
482

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

489
#[inline]
490
/// `password = astring`
491
pub(crate) fn password(input: &[u8]) -> IMAPResult<&[u8], AString> {
104✔
492
    astring(input)
104✔
493
}
104✔
494

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

523
    let (remaining, (_, auth_type, raw_data)) = parser(input)?;
404✔
524

525
    // Server must send continuation ("+ ") at this point...
526

UNCOV
527
    Ok((remaining, (auth_type, raw_data)))
×
528
}
404✔
529

530
// # Command Select
531

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

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

564
    let (remaining, (_, _, sequence_set, _, mailbox)) = parser(input)?;
316✔
565

566
    Ok((
48✔
567
        remaining,
48✔
568
        CommandBody::Copy {
48✔
569
            sequence_set,
48✔
570
            mailbox,
48✔
571
            uid: false,
48✔
572
        },
48✔
573
    ))
48✔
574
}
316✔
575

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

615
    #[cfg(not(feature = "ext_condstore_qresync"))]
616
    let (remaining, (_, sequence_set, macro_or_item_names)) = parser(input)?;
617

618
    #[cfg(feature = "ext_condstore_qresync")]
619
    let (remaining, (_, sequence_set, macro_or_item_names, modifiers)) = parser(input)?;
270✔
620

621
    Ok((
66✔
622
        remaining,
66✔
623
        CommandBody::Fetch {
66✔
624
            sequence_set,
66✔
625
            macro_or_item_names,
66✔
626
            uid: false,
66✔
627
            #[cfg(feature = "ext_condstore_qresync")]
66✔
628
            modifiers,
66✔
629
        },
66✔
630
    ))
66✔
631
}
270✔
632

633
#[cfg(feature = "ext_condstore_qresync")]
634
/// From RFC 4466:
635
///
636
/// ```abnf
637
/// fetch-modifiers = SP "(" fetch-modifier *(SP fetch-modifier) ")"
638
/// ```
639
pub(crate) fn fetch_modifiers(input: &[u8]) -> IMAPResult<&[u8], Vec<FetchModifier>> {
66✔
640
    delimited(tag(" ("), separated_list1(sp, fetch_modifier), char(')'))(input)
66✔
641
}
66✔
642

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

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

694
    #[cfg(not(feature = "ext_condstore_qresync"))]
695
    let (remaining, (_, sequence_set, (kind, response, flags))) = parser(input)?;
696

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

700
    Ok((
32✔
701
        remaining,
32✔
702
        CommandBody::Store {
32✔
703
            sequence_set,
32✔
704
            kind,
32✔
705
            response,
32✔
706
            flags,
32✔
707
            uid: false,
32✔
708
            #[cfg(feature = "ext_condstore_qresync")]
32✔
709
            modifiers,
32✔
710
        },
32✔
711
    ))
32✔
712
}
204✔
713

714
#[cfg(feature = "ext_condstore_qresync")]
715
/// From RFC 4466:
716
///
717
/// ```abnf
718
/// store-modifiers = SP "(" store-modifier *(SP store-modifier) ")"
719
/// ```
720
pub(crate) fn store_modifiers(input: &[u8]) -> IMAPResult<&[u8], Vec<StoreModifier>> {
32✔
721
    delimited(tag(" ("), separated_list1(sp, store_modifier), char(')'))(input)
32✔
722
}
32✔
723

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

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

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

776
    Ok((remaining, (store_type, store_response, flag_list)))
32✔
777
}
32✔
778

779
/// `uid = "UID" SP (copy / fetch / search / store)`
780
///
781
/// Note: Unique identifiers used instead of message sequence numbers
782
pub(crate) fn uid(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
168✔
783
    let mut parser = tuple((
168✔
784
        tag_no_case(b"UID"),
168✔
785
        sp,
168✔
786
        alt((copy, fetch, search, store, r#move)),
168✔
787
    ));
168✔
788

789
    let (remaining, (_, _, mut cmd)) = parser(input)?;
168✔
790

791
    match cmd {
20✔
UNCOV
792
        CommandBody::Copy { ref mut uid, .. }
×
793
        | CommandBody::Fetch { ref mut uid, .. }
16✔
UNCOV
794
        | CommandBody::Search { ref mut uid, .. }
×
UNCOV
795
        | CommandBody::Store { ref mut uid, .. }
×
796
        | CommandBody::Move { ref mut uid, .. } => *uid = true,
20✔
UNCOV
797
        _ => unreachable!(),
×
798
    }
799

800
    Ok((remaining, cmd))
20✔
801
}
168✔
802

803
#[cfg(test)]
804
mod tests {
805
    use std::num::NonZeroU32;
806

807
    use imap_types::{
808
        core::Tag,
809
        fetch::{MessageDataItemName, Section},
810
    };
811

812
    use super::*;
813
    use crate::{CommandCodec, encode::Encoder};
814

815
    #[test]
816
    fn test_parse_fetch() {
2✔
817
        println!("{:#?}", fetch(b"fetch 1:1 (flags)???"));
2✔
818
    }
2✔
819

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

867
        let expected_remainder = "???".as_bytes();
2✔
868

869
        for (expected, test) in tests {
30✔
870
            let (got_remainder, got) = fetch_att(test.as_bytes()).unwrap();
28✔
871

872
            assert_eq!(expected, got);
28✔
873
            assert_eq!(expected_remainder, got_remainder);
28✔
874
        }
875
    }
2✔
876

877
    #[test]
878
    fn test_that_empty_ir_is_encoded_correctly() {
2✔
879
        let command = Command::new(
2✔
880
            Tag::try_from("A").unwrap(),
2✔
881
            CommandBody::Authenticate {
2✔
882
                mechanism: AuthMechanism::Plain,
2✔
883
                initial_response: Some(Secret::new(Cow::Borrowed(&b""[..]))),
2✔
884
            },
2✔
885
        )
886
        .unwrap();
2✔
887

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

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