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

duesee / imap-codec / 18421636743

11 Oct 2025 12:31AM UTC coverage: 91.075% (-0.7%) from 91.768%
18421636743

Pull #669

github

web-flow
Merge 47424d20e into 450fdf51c
Pull Request #669: feat: Implement RFC 2342 (NAMESPACE) in imap-codec

15 of 129 new or added lines in 6 files covered. (11.63%)

5 existing lines in 1 file now uncovered.

10306 of 11316 relevant lines covered (91.07%)

934.56 hits per line

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

86.84
/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
        #[cfg(feature = "ext_namespace")]
1,216✔
172
        namespace,
1,216✔
173
    ))(input)
1,216✔
174
}
1,216✔
175

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

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

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

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

209
    let (remaining, mailbox) = parser(input)?;
1,216✔
210

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

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

220
    let (remaining, mailbox) = parser(input)?;
1,184✔
221

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

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

234
    let (remaining, mailbox) = parser(input)?;
1,088✔
235

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

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

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

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

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

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

269
    let (remaining, (_, reference, _, mailbox_wildcard)) = parser(input)?;
864✔
270

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

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

286
    let (remaining, (_, mailbox, _, new_mailbox)) = parser(input)?;
832✔
287

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

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

306
    let (remaining, mailbox) = parser(input)?;
784✔
307

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

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

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

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

403
/// FROM RFC 2342:
404
///
405
/// ```abnf
406
/// namespace = "NAMESPACE"
407
/// ```
408
#[cfg(feature = "ext_namespace")]
409
pub(crate) fn namespace(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
508✔
410
    let parser = tag_no_case(b"NAMESPACE");
508✔
411

412
    let (remaining, _) = parser(input)?;
508✔
413

NEW
414
    Ok((remaining, CommandBody::Namespace))
×
415
}
508✔
416

417
/// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"`
418
pub(crate) fn status(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
716✔
419
    let mut parser = tuple((
716✔
420
        tag_no_case(b"STATUS "),
716✔
421
        mailbox,
716✔
422
        delimited(tag(b" ("), separated_list0(sp, status_att), tag(b")")),
716✔
423
    ));
716✔
424

425
    let (remaining, (_, mailbox, item_names)) = parser(input)?;
716✔
426

427
    Ok((
20✔
428
        remaining,
20✔
429
        CommandBody::Status {
20✔
430
            mailbox,
20✔
431
            item_names: item_names.into(),
20✔
432
        },
20✔
433
    ))
20✔
434
}
716✔
435

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

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

442
    Ok((remaining, CommandBody::Subscribe { mailbox }))
16✔
443
}
696✔
444

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

449
    let (remaining, mailbox) = parser(input)?;
680✔
450

451
    Ok((remaining, CommandBody::Unsubscribe { mailbox }))
16✔
452
}
680✔
453

454
// # Command NonAuth
455

456
/// `command-nonauth = login / authenticate / "STARTTLS"`
457
///
458
/// Note: Valid only when in Not Authenticated state
459
pub(crate) fn command_nonauth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
508✔
460
    let mut parser = alt((
508✔
461
        login,
462
        map(authenticate, |(mechanism, initial_response)| {
508✔
463
            CommandBody::Authenticate {
×
464
                mechanism,
×
465
                initial_response,
×
466
            }
×
467
        }),
×
468
        #[cfg(feature = "starttls")]
469
        value(CommandBody::StartTLS, tag_no_case(b"STARTTLS")),
508✔
470
    ));
471

472
    let (remaining, parsed_command_nonauth) = parser(input)?;
508✔
473

474
    Ok((remaining, parsed_command_nonauth))
136✔
475
}
508✔
476

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

481
    let (remaining, (_, _, username, _, password)) = parser(input)?;
508✔
482

483
    Ok((
104✔
484
        remaining,
104✔
485
        CommandBody::Login {
104✔
486
            username,
104✔
487
            password: Secret::new(password),
104✔
488
        },
104✔
489
    ))
104✔
490
}
508✔
491

492
#[inline]
493
/// `userid = astring`
494
pub(crate) fn userid(input: &[u8]) -> IMAPResult<&[u8], AString> {
104✔
495
    astring(input)
104✔
496
}
104✔
497

498
#[inline]
499
/// `password = astring`
500
pub(crate) fn password(input: &[u8]) -> IMAPResult<&[u8], AString> {
104✔
501
    astring(input)
104✔
502
}
104✔
503

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

532
    let (remaining, (_, auth_type, raw_data)) = parser(input)?;
404✔
533

534
    // Server must send continuation ("+ ") at this point...
535

536
    Ok((remaining, (auth_type, raw_data)))
×
537
}
404✔
538

539
// # Command Select
540

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

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

573
    let (remaining, (_, _, sequence_set, _, mailbox)) = parser(input)?;
316✔
574

575
    Ok((
48✔
576
        remaining,
48✔
577
        CommandBody::Copy {
48✔
578
            sequence_set,
48✔
579
            mailbox,
48✔
580
            uid: false,
48✔
581
        },
48✔
582
    ))
48✔
583
}
316✔
584

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

624
    #[cfg(not(feature = "ext_condstore_qresync"))]
625
    let (remaining, (_, sequence_set, macro_or_item_names)) = parser(input)?;
626

627
    #[cfg(feature = "ext_condstore_qresync")]
628
    let (remaining, (_, sequence_set, macro_or_item_names, modifiers)) = parser(input)?;
270✔
629

630
    Ok((
66✔
631
        remaining,
66✔
632
        CommandBody::Fetch {
66✔
633
            sequence_set,
66✔
634
            macro_or_item_names,
66✔
635
            uid: false,
66✔
636
            #[cfg(feature = "ext_condstore_qresync")]
66✔
637
            modifiers,
66✔
638
        },
66✔
639
    ))
66✔
640
}
270✔
641

642
#[cfg(feature = "ext_condstore_qresync")]
643
/// From RFC 4466:
644
///
645
/// ```abnf
646
/// fetch-modifiers = SP "(" fetch-modifier *(SP fetch-modifier) ")"
647
/// ```
648
pub(crate) fn fetch_modifiers(input: &[u8]) -> IMAPResult<&[u8], Vec<FetchModifier>> {
66✔
649
    delimited(tag(" ("), separated_list1(sp, fetch_modifier), char(')'))(input)
66✔
650
}
66✔
651

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

688
/// ```abnf
689
/// store = "STORE" SP sequence-set [store-modifiers] SP store-att-flags
690
///                                 ^^^^^^^^^^^^^^^^^
691
///                                 |
692
///                                 RFC 4466
693
/// ```
694
pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
204✔
695
    let mut parser = tuple((
204✔
696
        tag_no_case(b"STORE"),
204✔
697
        preceded(sp, sequence_set),
204✔
698
        #[cfg(feature = "ext_condstore_qresync")]
204✔
699
        map(opt(store_modifiers), Option::unwrap_or_default),
204✔
700
        preceded(sp, store_att_flags),
204✔
701
    ));
204✔
702

703
    #[cfg(not(feature = "ext_condstore_qresync"))]
704
    let (remaining, (_, sequence_set, (kind, response, flags))) = parser(input)?;
705

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

709
    Ok((
32✔
710
        remaining,
32✔
711
        CommandBody::Store {
32✔
712
            sequence_set,
32✔
713
            kind,
32✔
714
            response,
32✔
715
            flags,
32✔
716
            uid: false,
32✔
717
            #[cfg(feature = "ext_condstore_qresync")]
32✔
718
            modifiers,
32✔
719
        },
32✔
720
    ))
32✔
721
}
204✔
722

723
#[cfg(feature = "ext_condstore_qresync")]
724
/// From RFC 4466:
725
///
726
/// ```abnf
727
/// store-modifiers = SP "(" store-modifier *(SP store-modifier) ")"
728
/// ```
729
pub(crate) fn store_modifiers(input: &[u8]) -> IMAPResult<&[u8], Vec<StoreModifier>> {
32✔
730
    delimited(tag(" ("), separated_list1(sp, store_modifier), char(')'))(input)
32✔
731
}
32✔
732

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

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

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

785
    Ok((remaining, (store_type, store_response, flag_list)))
32✔
786
}
32✔
787

788
/// `uid = "UID" SP (copy / fetch / search / store)`
789
///
790
/// Note: Unique identifiers used instead of message sequence numbers
791
pub(crate) fn uid(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
168✔
792
    let mut parser = tuple((
168✔
793
        tag_no_case(b"UID"),
168✔
794
        sp,
168✔
795
        alt((copy, fetch, search, store, r#move)),
168✔
796
    ));
168✔
797

798
    let (remaining, (_, _, mut cmd)) = parser(input)?;
168✔
799

800
    match cmd {
20✔
801
        CommandBody::Copy { ref mut uid, .. }
×
802
        | CommandBody::Fetch { ref mut uid, .. }
16✔
803
        | CommandBody::Search { ref mut uid, .. }
×
804
        | CommandBody::Store { ref mut uid, .. }
×
805
        | CommandBody::Move { ref mut uid, .. } => *uid = true,
20✔
806
        _ => unreachable!(),
×
807
    }
808

809
    Ok((remaining, cmd))
20✔
810
}
168✔
811

812
#[cfg(test)]
813
mod tests {
814
    use std::num::NonZeroU32;
815

816
    use imap_types::{
817
        core::Tag,
818
        fetch::{MessageDataItemName, Section},
819
    };
820

821
    use super::*;
822
    use crate::{CommandCodec, encode::Encoder};
823

824
    #[test]
825
    fn test_parse_fetch() {
2✔
826
        println!("{:#?}", fetch(b"fetch 1:1 (flags)???"));
2✔
827
    }
2✔
828

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

876
        let expected_remainder = "???".as_bytes();
2✔
877

878
        for (expected, test) in tests {
30✔
879
            let (got_remainder, got) = fetch_att(test.as_bytes()).unwrap();
28✔
880

881
            assert_eq!(expected, got);
28✔
882
            assert_eq!(expected_remainder, got_remainder);
28✔
883
        }
884
    }
2✔
885

886
    #[test]
887
    fn test_that_empty_ir_is_encoded_correctly() {
2✔
888
        let command = Command::new(
2✔
889
            Tag::try_from("A").unwrap(),
2✔
890
            CommandBody::Authenticate {
2✔
891
                mechanism: AuthMechanism::Plain,
2✔
892
                initial_response: Some(Secret::new(Cow::Borrowed(&b""[..]))),
2✔
893
            },
2✔
894
        )
895
        .unwrap();
2✔
896

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

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