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

duesee / imap-codec / 12757105626

13 Jan 2025 10:16PM UTC coverage: 92.896% (-0.1%) from 93.011%
12757105626

push

github

duesee
docs: fix warning

11350 of 12218 relevant lines covered (92.9%)

904.56 hits per line

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

95.35
/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
use imap_types::{
9
    auth::AuthMechanism,
10
    command::{Command, CommandBody},
11
    core::AString,
12
    extensions::binary::LiteralOrLiteral8,
13
    fetch::{Macro, MacroOrMessageDataItemNames},
14
    flag::{Flag, StoreResponse, StoreType},
15
    secret::Secret,
16
};
17
#[cfg(feature = "ext_condstore_qresync")]
18
use nom::character::streaming::char;
19
use nom::{
20
    branch::alt,
21
    bytes::streaming::{tag, tag_no_case},
22
    combinator::{map, opt, value},
23
    multi::{separated_list0, separated_list1},
24
    sequence::{delimited, preceded, terminated, tuple},
25
};
26

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

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

72
    let (remaining, obtained_tag) = parser_tag(input)?;
1,474✔
73

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

90
            Err(error)
76✔
91
        }
92
    }
93
}
1,474✔
94

95
// # Command Any
96

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

117
// # Command Auth
118

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

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

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

184
    Ok((
×
185
        remaining,
×
186
        CommandBody::Append {
×
187
            mailbox,
×
188
            flags: flags.unwrap_or_default(),
×
189
            date,
×
190
            message,
×
191
        },
×
192
    ))
×
193
}
1,216✔
194

195
/// `create = "CREATE" SP mailbox`
196
///
197
/// Note: Use of INBOX gives a NO error
198
pub(crate) fn create(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,216✔
199
    let mut parser = preceded(tag_no_case(b"CREATE "), mailbox);
1,216✔
200

201
    let (remaining, mailbox) = parser(input)?;
1,216✔
202

203
    Ok((remaining, CommandBody::Create { mailbox }))
32✔
204
}
1,216✔
205

206
/// `delete = "DELETE" SP mailbox`
207
///
208
/// Note: Use of INBOX gives a NO error
209
pub(crate) fn delete(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,184✔
210
    let mut parser = preceded(tag_no_case(b"DELETE "), mailbox);
1,184✔
211

212
    let (remaining, mailbox) = parser(input)?;
1,184✔
213

214
    Ok((remaining, CommandBody::Delete { mailbox }))
96✔
215
}
1,184✔
216

217
/// `examine = "EXAMINE" SP mailbox`
218
pub(crate) fn examine(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
1,088✔
219
    let mut parser = preceded(tag_no_case(b"EXAMINE "), mailbox);
1,088✔
220

221
    let (remaining, mailbox) = parser(input)?;
1,088✔
222

223
    Ok((remaining, CommandBody::Examine { mailbox }))
16✔
224
}
1,088✔
225

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

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

232
    Ok((
208✔
233
        remaining,
208✔
234
        CommandBody::List {
208✔
235
            reference,
208✔
236
            mailbox_wildcard,
208✔
237
        },
208✔
238
    ))
208✔
239
}
1,072✔
240

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

245
    let (remaining, (_, reference, _, mailbox_wildcard)) = parser(input)?;
864✔
246

247
    Ok((
32✔
248
        remaining,
32✔
249
        CommandBody::Lsub {
32✔
250
            reference,
32✔
251
            mailbox_wildcard,
32✔
252
        },
32✔
253
    ))
32✔
254
}
864✔
255

256
/// `rename = "RENAME" SP mailbox SP mailbox`
257
///
258
/// Note: Use of INBOX as a destination gives a NO error
259
pub(crate) fn rename(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
832✔
260
    let mut parser = tuple((tag_no_case(b"RENAME "), mailbox, sp, mailbox));
832✔
261

262
    let (remaining, (_, mailbox, _, new_mailbox)) = parser(input)?;
832✔
263

264
    Ok((
48✔
265
        remaining,
48✔
266
        CommandBody::Rename {
48✔
267
            from: mailbox,
48✔
268
            to: new_mailbox,
48✔
269
        },
48✔
270
    ))
48✔
271
}
832✔
272

273
/// `select = "SELECT" SP mailbox`
274
pub(crate) fn select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
784✔
275
    let mut parser = preceded(tag_no_case(b"SELECT "), mailbox);
784✔
276

277
    let (remaining, mailbox) = parser(input)?;
784✔
278

279
    Ok((remaining, CommandBody::Select { mailbox }))
48✔
280
}
784✔
281

282
/// `status = "STATUS" SP mailbox SP "(" status-att *(SP status-att) ")"`
283
pub(crate) fn status(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
716✔
284
    let mut parser = tuple((
716✔
285
        tag_no_case(b"STATUS "),
716✔
286
        mailbox,
716✔
287
        delimited(tag(b" ("), separated_list0(sp, status_att), tag(b")")),
716✔
288
    ));
716✔
289

290
    let (remaining, (_, mailbox, item_names)) = parser(input)?;
716✔
291

292
    Ok((
20✔
293
        remaining,
20✔
294
        CommandBody::Status {
20✔
295
            mailbox,
20✔
296
            item_names: item_names.into(),
20✔
297
        },
20✔
298
    ))
20✔
299
}
716✔
300

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

305
    let (remaining, mailbox) = parser(input)?;
696✔
306

307
    Ok((remaining, CommandBody::Subscribe { mailbox }))
16✔
308
}
696✔
309

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

314
    let (remaining, mailbox) = parser(input)?;
680✔
315

316
    Ok((remaining, CommandBody::Unsubscribe { mailbox }))
16✔
317
}
680✔
318

319
// # Command NonAuth
320

321
/// `command-nonauth = login / authenticate / "STARTTLS"`
322
///
323
/// Note: Valid only when in Not Authenticated state
324
pub(crate) fn command_nonauth(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
508✔
325
    let mut parser = alt((
508✔
326
        login,
508✔
327
        map(authenticate, |(mechanism, initial_response)| {
508✔
328
            CommandBody::Authenticate {
×
329
                mechanism,
×
330
                initial_response,
×
331
            }
×
332
        }),
508✔
333
        #[cfg(feature = "starttls")]
508✔
334
        value(CommandBody::StartTLS, tag_no_case(b"STARTTLS")),
508✔
335
    ));
508✔
336

337
    let (remaining, parsed_command_nonauth) = parser(input)?;
508✔
338

339
    Ok((remaining, parsed_command_nonauth))
136✔
340
}
508✔
341

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

346
    let (remaining, (_, _, username, _, password)) = parser(input)?;
508✔
347

348
    Ok((
104✔
349
        remaining,
104✔
350
        CommandBody::Login {
104✔
351
            username,
104✔
352
            password: Secret::new(password),
104✔
353
        },
104✔
354
    ))
104✔
355
}
508✔
356

357
#[inline]
358
/// `userid = astring`
359
pub(crate) fn userid(input: &[u8]) -> IMAPResult<&[u8], AString> {
104✔
360
    astring(input)
104✔
361
}
104✔
362

363
#[inline]
364
/// `password = astring`
365
pub(crate) fn password(input: &[u8]) -> IMAPResult<&[u8], AString> {
104✔
366
    astring(input)
104✔
367
}
104✔
368

369
/// `authenticate = "AUTHENTICATE" SP auth-type *(CRLF base64)` (edited)
370
///
371
/// ```text
372
///                                            Added by SASL-IR
373
///                                            |
374
///                                            vvvvvvvvvvvvvvvvvvv
375
/// authenticate = "AUTHENTICATE" SP auth-type [SP (base64 / "=")] *(CRLF base64)
376
///                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
377
///                |
378
///                This is parsed here.
379
///                CRLF is parsed by upper command parser.
380
/// ```
381
#[allow(clippy::type_complexity)]
382
pub(crate) fn authenticate(
404✔
383
    input: &[u8],
404✔
384
) -> IMAPResult<&[u8], (AuthMechanism, Option<Secret<Cow<[u8]>>>)> {
404✔
385
    let mut parser = tuple((
404✔
386
        tag_no_case(b"AUTHENTICATE "),
404✔
387
        auth_type,
404✔
388
        opt(preceded(
404✔
389
            sp,
404✔
390
            alt((
404✔
391
                map(base64, |data| Secret::new(Cow::Owned(data))),
404✔
392
                value(Secret::new(Cow::Borrowed(&b""[..])), tag("=")),
404✔
393
            )),
404✔
394
        )),
404✔
395
    ));
404✔
396

397
    let (remaining, (_, auth_type, raw_data)) = parser(input)?;
404✔
398

399
    // Server must send continuation ("+ ") at this point...
400

401
    Ok((remaining, (auth_type, raw_data)))
×
402
}
404✔
403

404
// # Command Select
405

406
/// `command-select = "CHECK" /
407
///                   "CLOSE" /
408
///                   "EXPUNGE" /
409
///                   copy /
410
///                   fetch /
411
///                   store /
412
///                   uid /
413
///                   search`
414
///
415
/// Note: Valid only when in Selected state
416
pub(crate) fn command_select(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
372✔
417
    alt((
372✔
418
        value(CommandBody::Check, tag_no_case(b"CHECK")),
372✔
419
        value(CommandBody::Close, tag_no_case(b"CLOSE")),
372✔
420
        value(CommandBody::Expunge, tag_no_case(b"EXPUNGE")),
372✔
421
        uid_expunge,
372✔
422
        copy,
372✔
423
        fetch,
372✔
424
        store,
372✔
425
        uid,
372✔
426
        search,
372✔
427
        sort,
372✔
428
        thread,
372✔
429
        value(CommandBody::Unselect, tag_no_case(b"UNSELECT")),
372✔
430
        r#move,
372✔
431
    ))(input)
372✔
432
}
372✔
433

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

438
    let (remaining, (_, _, sequence_set, _, mailbox)) = parser(input)?;
316✔
439

440
    Ok((
48✔
441
        remaining,
48✔
442
        CommandBody::Copy {
48✔
443
            sequence_set,
48✔
444
            mailbox,
48✔
445
            uid: false,
48✔
446
        },
48✔
447
    ))
48✔
448
}
316✔
449

450
/// ```abnf
451
/// fetch = "FETCH" SP sequence-set SP ("ALL" /
452
///                                     "FULL" /
453
///                                     "FAST" /
454
///                                     fetch-att / "(" fetch-att *(SP fetch-att) ")")
455
/// ```
456
///
457
/// From RFC 4466:
458
///
459
/// ```abnf
460
/// fetch = "FETCH" SP sequence-set SP ("ALL" /
461
///                                     "FULL" /
462
///                                     "FAST" /
463
///                                     fetch-att / "(" fetch-att *(SP fetch-att) ")")
464
///                                     [fetch-modifiers]
465
///                                     ;; modifies the original IMAP4 FETCH command to
466
///                                     ;; accept optional modifiers
467
///
468
/// fetch-modifiers = SP "(" fetch-modifier *(SP fetch-modifier) ")"
469
///
470
/// fetch-modifier = fetch-modifier-name [ SP fetch-modif-params ]
471
///
472
/// fetch-modif-params = tagged-ext-val
473
///                      ;; This non-terminal shows recommended syntax
474
///                      ;; for future extensions.
475
///
476
/// fetch-modifier-name = tagged-ext-label
477
/// ```
478
///
479
/// From RFC 7162 (CONDSTORE/QRESYNC):
480
///
481
/// ```abnf
482
/// fetch-modifier =/ chgsince-fetch-mod
483
///                   ;; Conforms to the generic "fetch-modifier" syntax defined in [RFC4466].
484
///
485
/// chgsince-fetch-mod = "CHANGEDSINCE" SP mod-sequence-value
486
///                      ;; CHANGEDSINCE FETCH modifier conforms to the fetch-modifier syntax.
487
/// ```
488
pub(crate) fn fetch(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
270✔
489
    let mut parser = tuple((
270✔
490
        tag_no_case(b"FETCH"),
270✔
491
        preceded(sp, sequence_set),
270✔
492
        preceded(
270✔
493
            sp,
270✔
494
            alt((
270✔
495
                value(
270✔
496
                    MacroOrMessageDataItemNames::Macro(Macro::All),
270✔
497
                    tag_no_case(b"ALL"),
270✔
498
                ),
270✔
499
                value(
270✔
500
                    MacroOrMessageDataItemNames::Macro(Macro::Fast),
270✔
501
                    tag_no_case(b"FAST"),
270✔
502
                ),
270✔
503
                value(
270✔
504
                    MacroOrMessageDataItemNames::Macro(Macro::Full),
270✔
505
                    tag_no_case(b"FULL"),
270✔
506
                ),
270✔
507
                map(fetch_att, |fetch_att| {
274✔
508
                    MacroOrMessageDataItemNames::MessageDataItemNames(vec![fetch_att])
32✔
509
                }),
274✔
510
                map(
270✔
511
                    delimited(tag(b"("), separated_list0(sp, fetch_att), tag(b")")),
270✔
512
                    MacroOrMessageDataItemNames::MessageDataItemNames,
270✔
513
                ),
270✔
514
            )),
270✔
515
        ),
270✔
516
        #[cfg(feature = "ext_condstore_qresync")]
270✔
517
        opt(delimited(
270✔
518
            tag(" ("),
270✔
519
            preceded(tag_no_case("CHANGEDSINCE "), mod_sequence_value),
270✔
520
            char(')'),
270✔
521
        )),
270✔
522
    ));
270✔
523

524
    #[cfg(not(feature = "ext_condstore_qresync"))]
525
    let (remaining, (_, sequence_set, macro_or_item_names)) = parser(input)?;
526

527
    #[cfg(feature = "ext_condstore_qresync")]
528
    let (remaining, (_, sequence_set, macro_or_item_names, changed_since)) = parser(input)?;
270✔
529

530
    Ok((
66✔
531
        remaining,
66✔
532
        CommandBody::Fetch {
66✔
533
            sequence_set,
66✔
534
            macro_or_item_names,
66✔
535
            uid: false,
66✔
536
            #[cfg(feature = "ext_condstore_qresync")]
66✔
537
            changed_since,
66✔
538
        },
66✔
539
    ))
66✔
540
}
270✔
541

542
/// ```abnf
543
/// store = "STORE" SP sequence-set SP store-att-flags
544
/// ```
545
///
546
/// With store-modifier from RFC 4466 (enabled by CONDSTORE/QRESYNC):
547
///
548
/// ```abnf
549
/// store = "STORE" SP sequence-set [store-modifiers] SP store-att-flags
550
///
551
/// store-modifiers =  SP "(" store-modifier *(SP store-modifier) ")"
552
///
553
/// store-modifier = store-modifier-name [SP store-modif-params]
554
///
555
/// store-modif-params = tagged-ext-val
556
///
557
/// store-modifier-name = tagged-ext-label
558
/// ```
559
///
560
/// From RFC 7162 (CONDSTORE/QRESYNC):
561
///
562
/// ```abnf
563
/// store-modifier =/ "UNCHANGEDSINCE" SP mod-sequence-valzer
564
///                ;; Only a single "UNCHANGEDSINCE" may be specified in a STORE operation.
565
/// ```
566
pub(crate) fn store(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
204✔
567
    #[cfg(not(feature = "ext_condstore_qresync"))]
204✔
568
    let mut parser = tuple((
204✔
569
        tag_no_case(b"STORE"),
204✔
570
        preceded(sp, sequence_set),
204✔
571
        preceded(sp, store_att_flags),
204✔
572
    ));
204✔
573

204✔
574
    #[cfg(feature = "ext_condstore_qresync")]
204✔
575
    let mut parser = tuple((
204✔
576
        tag_no_case(b"STORE"),
204✔
577
        preceded(sp, sequence_set),
204✔
578
        opt(delimited(
204✔
579
            tag(" ("),
204✔
580
            preceded(tag_no_case("UNCHANGEDSINCE "), mod_sequence_valzer),
204✔
581
            char(')'),
204✔
582
        )),
204✔
583
        preceded(sp, store_att_flags),
204✔
584
    ));
204✔
585

586
    #[cfg(not(feature = "ext_condstore_qresync"))]
587
    let (remaining, (_, sequence_set, (kind, response, flags))) = parser(input)?;
588

589
    #[cfg(feature = "ext_condstore_qresync")]
590
    let (remaining, (_, sequence_set, unchanged_since, (kind, response, flags))) = parser(input)?;
204✔
591

592
    Ok((
32✔
593
        remaining,
32✔
594
        CommandBody::Store {
32✔
595
            sequence_set,
32✔
596
            kind,
32✔
597
            response,
32✔
598
            flags,
32✔
599
            uid: false,
32✔
600
            #[cfg(feature = "ext_condstore_qresync")]
32✔
601
            unchanged_since,
32✔
602
        },
32✔
603
    ))
32✔
604
}
204✔
605

606
/// `store-att-flags = (["+" / "-"] "FLAGS" [".SILENT"]) SP (flag-list / (flag *(SP flag)))`
607
pub(crate) fn store_att_flags(
32✔
608
    input: &[u8],
32✔
609
) -> IMAPResult<&[u8], (StoreType, StoreResponse, Vec<Flag>)> {
32✔
610
    let mut parser = tuple((
32✔
611
        tuple((
32✔
612
            map(
32✔
613
                opt(alt((
32✔
614
                    value(StoreType::Add, tag(b"+")),
32✔
615
                    value(StoreType::Remove, tag(b"-")),
32✔
616
                ))),
32✔
617
                |type_| match type_ {
36✔
618
                    Some(type_) => type_,
32✔
619
                    None => StoreType::Replace,
×
620
                },
36✔
621
            ),
32✔
622
            tag_no_case(b"FLAGS"),
32✔
623
            map(opt(tag_no_case(b".SILENT")), |x| match x {
36✔
624
                Some(_) => StoreResponse::Silent,
×
625
                None => StoreResponse::Answer,
32✔
626
            }),
36✔
627
        )),
32✔
628
        sp,
32✔
629
        alt((flag_list, separated_list1(sp, flag))),
32✔
630
    ));
32✔
631

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

634
    Ok((remaining, (store_type, store_response, flag_list)))
32✔
635
}
32✔
636

637
/// `uid = "UID" SP (copy / fetch / search / store)`
638
///
639
/// Note: Unique identifiers used instead of message sequence numbers
640
pub(crate) fn uid(input: &[u8]) -> IMAPResult<&[u8], CommandBody> {
168✔
641
    let mut parser = tuple((
168✔
642
        tag_no_case(b"UID"),
168✔
643
        sp,
168✔
644
        alt((copy, fetch, search, store, r#move)),
168✔
645
    ));
168✔
646

647
    let (remaining, (_, _, mut cmd)) = parser(input)?;
168✔
648

649
    match cmd {
20✔
650
        CommandBody::Copy { ref mut uid, .. }
×
651
        | CommandBody::Fetch { ref mut uid, .. }
16✔
652
        | CommandBody::Search { ref mut uid, .. }
×
653
        | CommandBody::Store { ref mut uid, .. }
×
654
        | CommandBody::Move { ref mut uid, .. } => *uid = true,
20✔
655
        _ => unreachable!(),
×
656
    }
657

658
    Ok((remaining, cmd))
20✔
659
}
168✔
660

661
#[cfg(test)]
662
mod tests {
663
    use std::num::NonZeroU32;
664

665
    use imap_types::{
666
        core::Tag,
667
        fetch::{MessageDataItemName, Section},
668
    };
669

670
    use super::*;
671
    use crate::{encode::Encoder, CommandCodec};
672

673
    #[test]
674
    fn test_parse_fetch() {
2✔
675
        println!("{:#?}", fetch(b"fetch 1:1 (flags)???"));
2✔
676
    }
2✔
677

678
    #[test]
679
    fn test_parse_fetch_att() {
2✔
680
        let tests = [
2✔
681
            (MessageDataItemName::Envelope, "ENVELOPE???"),
2✔
682
            (MessageDataItemName::Flags, "FLAGS???"),
2✔
683
            (MessageDataItemName::InternalDate, "INTERNALDATE???"),
2✔
684
            (MessageDataItemName::Rfc822, "RFC822???"),
2✔
685
            (MessageDataItemName::Rfc822Header, "RFC822.HEADER???"),
2✔
686
            (MessageDataItemName::Rfc822Size, "RFC822.SIZE???"),
2✔
687
            (MessageDataItemName::Rfc822Text, "RFC822.TEXT???"),
2✔
688
            (MessageDataItemName::Body, "BODY???"),
2✔
689
            (MessageDataItemName::BodyStructure, "BODYSTRUCTURE???"),
2✔
690
            (MessageDataItemName::Uid, "UID???"),
2✔
691
            (
2✔
692
                MessageDataItemName::BodyExt {
2✔
693
                    partial: None,
2✔
694
                    peek: false,
2✔
695
                    section: None,
2✔
696
                },
2✔
697
                "BODY[]???",
2✔
698
            ),
2✔
699
            (
2✔
700
                MessageDataItemName::BodyExt {
2✔
701
                    partial: None,
2✔
702
                    peek: true,
2✔
703
                    section: None,
2✔
704
                },
2✔
705
                "BODY.PEEK[]???",
2✔
706
            ),
2✔
707
            (
2✔
708
                MessageDataItemName::BodyExt {
2✔
709
                    partial: None,
2✔
710
                    peek: true,
2✔
711
                    section: Some(Section::Text(None)),
2✔
712
                },
2✔
713
                "BODY.PEEK[TEXT]???",
2✔
714
            ),
2✔
715
            (
2✔
716
                MessageDataItemName::BodyExt {
2✔
717
                    partial: Some((42, NonZeroU32::try_from(1337).unwrap())),
2✔
718
                    peek: true,
2✔
719
                    section: Some(Section::Text(None)),
2✔
720
                },
2✔
721
                "BODY.PEEK[TEXT]<42.1337>???",
2✔
722
            ),
2✔
723
        ];
2✔
724

2✔
725
        let expected_remainder = "???".as_bytes();
2✔
726

727
        for (expected, test) in tests {
30✔
728
            let (got_remainder, got) = fetch_att(test.as_bytes()).unwrap();
28✔
729

28✔
730
            assert_eq!(expected, got);
28✔
731
            assert_eq!(expected_remainder, got_remainder);
28✔
732
        }
733
    }
2✔
734

735
    #[test]
736
    fn test_that_empty_ir_is_encoded_correctly() {
2✔
737
        let command = Command::new(
2✔
738
            Tag::try_from("A").unwrap(),
2✔
739
            CommandBody::Authenticate {
2✔
740
                mechanism: AuthMechanism::Plain,
2✔
741
                initial_response: Some(Secret::new(Cow::Borrowed(&b""[..]))),
2✔
742
            },
2✔
743
        )
2✔
744
        .unwrap();
2✔
745

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

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