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

duesee / imap-codec / 9964908275

16 Jul 2024 10:20PM UTC coverage: 92.967% (-0.01%) from 92.981%
9964908275

push

github

duesee
Update imap-codec/Cargo.toml

Co-authored-by: Jakob Schikowski <jakob.schikowski@gmx.de>

11156 of 12000 relevant lines covered (92.97%)

1090.05 hits per line

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

93.52
/imap-codec/src/fetch.rs
1
use std::num::NonZeroU32;
2

3
use abnf_core::streaming::sp;
4
use imap_types::{
5
    core::{AString, NString8, Vec1},
6
    fetch::{MessageDataItem, MessageDataItemName, Part, PartSpecifier, Section},
7
};
8
use nom::{
9
    branch::alt,
10
    bytes::streaming::{tag, tag_no_case},
11
    combinator::{map, opt, value},
12
    multi::separated_list1,
13
    sequence::{delimited, preceded, tuple},
14
};
15

16
use crate::{
17
    body::body,
18
    core::{astring, nstring, number, nz_number},
19
    datetime::date_time,
20
    decode::IMAPResult,
21
    envelope::envelope,
22
    extensions::binary::{literal8, partial, section_binary},
23
    flag::flag_fetch,
24
};
25

26
/// ```abnf
27
/// fetch-att = "ENVELOPE" /
28
///             "FLAGS" /
29
///             "INTERNALDATE" /
30
///             "RFC822" [".HEADER" / ".SIZE" / ".TEXT"] /
31
///             "BODY" ["STRUCTURE"] /
32
///             "UID" /
33
///             "BODY"      section ["<" number "." nz-number ">"] /
34
///             "BODY.PEEK" section ["<" number "." nz-number ">"] /
35
///             "BINARY"      section-binary [partial] / ; RFC 3516
36
///             "BINARY.PEEK" section-binary [partial] / ; RFC 3516
37
///             "BINARY.SIZE" section-binary             ; RFC 3516
38
/// ```
39
pub(crate) fn fetch_att(input: &[u8]) -> IMAPResult<&[u8], MessageDataItemName> {
172✔
40
    alt((
172✔
41
        value(MessageDataItemName::Envelope, tag_no_case(b"ENVELOPE")),
172✔
42
        value(MessageDataItemName::Flags, tag_no_case(b"FLAGS")),
172✔
43
        value(
172✔
44
            MessageDataItemName::InternalDate,
172✔
45
            tag_no_case(b"INTERNALDATE"),
172✔
46
        ),
172✔
47
        value(
172✔
48
            MessageDataItemName::BodyStructure,
172✔
49
            tag_no_case(b"BODYSTRUCTURE"),
172✔
50
        ),
172✔
51
        map(
172✔
52
            tuple((
172✔
53
                tag_no_case(b"BODY.PEEK"),
172✔
54
                section,
172✔
55
                opt(delimited(
172✔
56
                    tag(b"<"),
172✔
57
                    tuple((number, tag(b"."), nz_number)),
172✔
58
                    tag(b">"),
172✔
59
                )),
172✔
60
            )),
172✔
61
            |(_, section, byterange)| MessageDataItemName::BodyExt {
175✔
62
                section,
6✔
63
                partial: byterange.map(|(start, _, end)| (start, end)),
6✔
64
                peek: true,
6✔
65
            },
175✔
66
        ),
172✔
67
        map(
172✔
68
            tuple((
172✔
69
                tag_no_case(b"BODY"),
172✔
70
                section,
172✔
71
                opt(delimited(
172✔
72
                    tag(b"<"),
172✔
73
                    tuple((number, tag(b"."), nz_number)),
172✔
74
                    tag(b">"),
172✔
75
                )),
172✔
76
            )),
172✔
77
            |(_, section, byterange)| MessageDataItemName::BodyExt {
177✔
78
                section,
58✔
79
                partial: byterange.map(|(start, _, end)| (start, end)),
58✔
80
                peek: false,
58✔
81
            },
177✔
82
        ),
172✔
83
        map(
172✔
84
            tuple((tag_no_case("BINARY.PEEK"), section_binary, opt(partial))),
172✔
85
            |(_, section, partial)| MessageDataItemName::Binary {
172✔
86
                section,
×
87
                partial,
×
88
                peek: true,
×
89
            },
172✔
90
        ),
172✔
91
        map(
172✔
92
            tuple((tag_no_case("BINARY"), section_binary, opt(partial))),
172✔
93
            |(_, section, partial)| MessageDataItemName::Binary {
172✔
94
                section,
×
95
                partial,
×
96
                peek: false,
×
97
            },
172✔
98
        ),
172✔
99
        map(
172✔
100
            preceded(tag_no_case("BINARY.SIZE"), section_binary),
172✔
101
            |section| MessageDataItemName::BinarySize { section },
172✔
102
        ),
172✔
103
        value(MessageDataItemName::Body, tag_no_case(b"BODY")),
172✔
104
        value(MessageDataItemName::Uid, tag_no_case(b"UID")),
172✔
105
        value(
172✔
106
            MessageDataItemName::Rfc822Header,
172✔
107
            tag_no_case(b"RFC822.HEADER"),
172✔
108
        ),
172✔
109
        value(MessageDataItemName::Rfc822Size, tag_no_case(b"RFC822.SIZE")),
172✔
110
        value(MessageDataItemName::Rfc822Text, tag_no_case(b"RFC822.TEXT")),
172✔
111
        value(MessageDataItemName::Rfc822, tag_no_case(b"RFC822")),
172✔
112
    ))(input)
172✔
113
}
172✔
114

115
/// `msg-att = "("
116
///            (msg-att-dynamic / msg-att-static) *(SP (msg-att-dynamic / msg-att-static))
117
///            ")"`
118
pub(crate) fn msg_att(input: &[u8]) -> IMAPResult<&[u8], Vec1<MessageDataItem>> {
328✔
119
    delimited(
328✔
120
        tag(b"("),
328✔
121
        map(
328✔
122
            separated_list1(sp, alt((msg_att_dynamic, msg_att_static))),
328✔
123
            Vec1::unvalidated,
328✔
124
        ),
328✔
125
        tag(b")"),
328✔
126
    )(input)
328✔
127
}
328✔
128

129
/// `msg-att-dynamic = "FLAGS" SP "(" [flag-fetch *(SP flag-fetch)] ")"`
130
///
131
/// Note: MAY change for a message
132
pub(crate) fn msg_att_dynamic(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem> {
552✔
133
    let mut parser = tuple((
552✔
134
        tag_no_case(b"FLAGS"),
552✔
135
        sp,
552✔
136
        delimited(tag(b"("), opt(separated_list1(sp, flag_fetch)), tag(b")")),
552✔
137
    ));
552✔
138

139
    let (remaining, (_, _, flags)) = parser(input)?;
552✔
140

141
    Ok((remaining, MessageDataItem::Flags(flags.unwrap_or_default())))
280✔
142
}
552✔
143

144
/// ```abnf
145
/// msg-att-static = "ENVELOPE" SP envelope /
146
///                  "INTERNALDATE" SP date-time /
147
///                  "RFC822" [".HEADER" / ".TEXT"] SP nstring /
148
///                  "RFC822.SIZE" SP number /
149
///                  "BODY" ["STRUCTURE"] SP body /
150
///                  "BODY" section ["<" number ">"] SP nstring /
151
///                  "UID" SP uniqueid /
152
///                  "BINARY" section-binary SP (nstring / literal8) / ; RFC 3516
153
///                  "BINARY.SIZE" section-binary SP number            ; RFC 3516
154
/// ```
155
///
156
/// Note: MUST NOT change for a message
157
pub(crate) fn msg_att_static(input: &[u8]) -> IMAPResult<&[u8], MessageDataItem> {
272✔
158
    alt((
272✔
159
        map(
272✔
160
            tuple((tag_no_case(b"ENVELOPE"), sp, envelope)),
272✔
161
            |(_, _, envelope)| MessageDataItem::Envelope(envelope),
274✔
162
        ),
272✔
163
        map(
272✔
164
            tuple((tag_no_case(b"INTERNALDATE"), sp, date_time)),
272✔
165
            |(_, _, date_time)| MessageDataItem::InternalDate(date_time),
274✔
166
        ),
272✔
167
        map(
272✔
168
            tuple((tag_no_case(b"RFC822.HEADER"), sp, nstring)),
272✔
169
            |(_, _, nstring)| MessageDataItem::Rfc822Header(nstring),
272✔
170
        ),
272✔
171
        map(
272✔
172
            tuple((tag_no_case(b"RFC822.TEXT"), sp, nstring)),
272✔
173
            |(_, _, nstring)| MessageDataItem::Rfc822Text(nstring),
272✔
174
        ),
272✔
175
        map(
272✔
176
            tuple((tag_no_case(b"RFC822.SIZE"), sp, number)),
272✔
177
            |(_, _, num)| MessageDataItem::Rfc822Size(num),
276✔
178
        ),
272✔
179
        map(
272✔
180
            tuple((tag_no_case(b"RFC822"), sp, nstring)),
272✔
181
            |(_, _, nstring)| MessageDataItem::Rfc822(nstring),
276✔
182
        ),
272✔
183
        map(
272✔
184
            tuple((tag_no_case(b"BODYSTRUCTURE"), sp, body(8))),
272✔
185
            |(_, _, body)| MessageDataItem::BodyStructure(body),
274✔
186
        ),
272✔
187
        map(
272✔
188
            tuple((tag_no_case(b"BODY"), sp, body(8))),
272✔
189
            |(_, _, body)| MessageDataItem::Body(body),
274✔
190
        ),
272✔
191
        map(
272✔
192
            tuple((
272✔
193
                tag_no_case(b"BODY"),
272✔
194
                section,
272✔
195
                opt(delimited(tag(b"<"), number, tag(b">"))),
272✔
196
                sp,
272✔
197
                nstring,
272✔
198
            )),
272✔
199
            |(_, section, origin, _, data)| MessageDataItem::BodyExt {
274✔
200
                section,
28✔
201
                origin,
28✔
202
                data,
28✔
203
            },
274✔
204
        ),
272✔
205
        map(tuple((tag_no_case(b"UID"), sp, uniqueid)), |(_, _, uid)| {
278✔
206
            MessageDataItem::Uid(uid)
84✔
207
        }),
278✔
208
        map(
272✔
209
            tuple((
272✔
210
                tag_no_case(b"BINARY"),
272✔
211
                section_binary,
272✔
212
                sp,
272✔
213
                alt((
272✔
214
                    map(nstring, NString8::NString),
272✔
215
                    map(literal8, NString8::Literal8),
272✔
216
                )),
272✔
217
            )),
272✔
218
            |(_, section, _, value)| MessageDataItem::Binary { section, value },
272✔
219
        ),
272✔
220
        map(
272✔
221
            tuple((tag_no_case(b"BINARY.SIZE"), section_binary, sp, number)),
272✔
222
            |(_, section, _, size)| MessageDataItem::BinarySize { section, size },
272✔
223
        ),
272✔
224
    ))(input)
272✔
225
}
272✔
226

227
#[inline]
228
/// `uniqueid = nz-number`
229
///
230
/// Note: Strictly ascending
231
pub(crate) fn uniqueid(input: &[u8]) -> IMAPResult<&[u8], NonZeroU32> {
84✔
232
    nz_number(input)
84✔
233
}
84✔
234

235
/// `section = "[" [section-spec] "]"`
236
pub(crate) fn section(input: &[u8]) -> IMAPResult<&[u8], Option<Section>> {
94✔
237
    delimited(tag(b"["), opt(section_spec), tag(b"]"))(input)
94✔
238
}
94✔
239

240
/// `section-spec = section-msgtext / (section-part ["." section-text])`
241
pub(crate) fn section_spec(input: &[u8]) -> IMAPResult<&[u8], Section> {
92✔
242
    alt((
92✔
243
        map(section_msgtext, |part_specifier| match part_specifier {
100✔
244
            PartSpecifier::PartNumber(_) => unreachable!(),
×
245
            PartSpecifier::Header => Section::Header(None),
56✔
246
            PartSpecifier::HeaderFields(fields) => Section::HeaderFields(None, fields),
28✔
247
            PartSpecifier::HeaderFieldsNot(fields) => Section::HeaderFieldsNot(None, fields),
×
248
            PartSpecifier::Text => Section::Text(None),
4✔
249
            PartSpecifier::Mime => unreachable!(),
×
250
        }),
100✔
251
        map(
92✔
252
            tuple((section_part, opt(tuple((tag(b"."), section_text))))),
92✔
253
            |(part_number, maybe_part_specifier)| {
92✔
254
                if let Some((_, part_specifier)) = maybe_part_specifier {
×
255
                    match part_specifier {
×
256
                        PartSpecifier::PartNumber(_) => unreachable!(),
×
257
                        PartSpecifier::Header => Section::Header(Some(Part(part_number))),
×
258
                        PartSpecifier::HeaderFields(fields) => {
×
259
                            Section::HeaderFields(Some(Part(part_number)), fields)
×
260
                        }
261
                        PartSpecifier::HeaderFieldsNot(fields) => {
×
262
                            Section::HeaderFieldsNot(Some(Part(part_number)), fields)
×
263
                        }
264
                        PartSpecifier::Text => Section::Text(Some(Part(part_number))),
×
265
                        PartSpecifier::Mime => Section::Mime(Part(part_number)),
×
266
                    }
267
                } else {
268
                    Section::Part(Part(part_number))
×
269
                }
270
            },
92✔
271
        ),
92✔
272
    ))(input)
92✔
273
}
92✔
274

275
/// `section-msgtext = "HEADER" / "HEADER.FIELDS" [".NOT"] SP header-list / "TEXT"`
276
///
277
/// Top-level or MESSAGE/RFC822 part
278
pub(crate) fn section_msgtext(input: &[u8]) -> IMAPResult<&[u8], PartSpecifier> {
92✔
279
    alt((
92✔
280
        map(
92✔
281
            tuple((tag_no_case(b"HEADER.FIELDS.NOT"), sp, header_list)),
92✔
282
            |(_, _, header_list)| PartSpecifier::HeaderFieldsNot(header_list),
92✔
283
        ),
92✔
284
        map(
92✔
285
            tuple((tag_no_case(b"HEADER.FIELDS"), sp, header_list)),
92✔
286
            |(_, _, header_list)| PartSpecifier::HeaderFields(header_list),
94✔
287
        ),
92✔
288
        value(PartSpecifier::Header, tag_no_case(b"HEADER")),
92✔
289
        value(PartSpecifier::Text, tag_no_case(b"TEXT")),
92✔
290
    ))(input)
92✔
291
}
92✔
292

293
#[inline]
294
/// `section-part = nz-number *("." nz-number)`
295
///
296
/// Body part nesting
297
pub(crate) fn section_part(input: &[u8]) -> IMAPResult<&[u8], Vec1<NonZeroU32>> {
4✔
298
    map(separated_list1(tag(b"."), nz_number), Vec1::unvalidated)(input)
4✔
299
}
4✔
300

301
/// `section-text = section-msgtext / "MIME"`
302
///
303
/// Text other than actual body part (headers, etc.)
304
pub(crate) fn section_text(input: &[u8]) -> IMAPResult<&[u8], PartSpecifier> {
×
305
    alt((
×
306
        section_msgtext,
×
307
        value(PartSpecifier::Mime, tag_no_case(b"MIME")),
×
308
    ))(input)
×
309
}
×
310

311
/// `header-list = "(" header-fld-name *(SP header-fld-name) ")"`
312
pub(crate) fn header_list(input: &[u8]) -> IMAPResult<&[u8], Vec1<AString>> {
28✔
313
    map(
28✔
314
        delimited(tag(b"("), separated_list1(sp, header_fld_name), tag(b")")),
28✔
315
        Vec1::unvalidated,
28✔
316
    )(input)
28✔
317
}
28✔
318

319
#[inline]
320
/// `header-fld-name = astring`
321
pub(crate) fn header_fld_name(input: &[u8]) -> IMAPResult<&[u8], AString> {
56✔
322
    astring(input)
56✔
323
}
56✔
324

325
#[cfg(test)]
326
mod tests {
327
    use imap_types::{
328
        body::{BasicFields, Body, BodyStructure, SpecificFields},
329
        core::{IString, NString},
330
        datetime::DateTime,
331
        envelope::Envelope,
332
    };
333

334
    use super::*;
335
    use crate::testing::known_answer_test_encode;
336

337
    #[test]
338
    fn test_encode_message_data_item_name() {
2✔
339
        let tests = [
2✔
340
            (MessageDataItemName::Body, b"BODY".as_ref()),
2✔
341
            (
2✔
342
                MessageDataItemName::BodyExt {
2✔
343
                    section: None,
2✔
344
                    partial: None,
2✔
345
                    peek: false,
2✔
346
                },
2✔
347
                b"BODY[]",
2✔
348
            ),
2✔
349
            (MessageDataItemName::BodyStructure, b"BODYSTRUCTURE"),
2✔
350
            (MessageDataItemName::Envelope, b"ENVELOPE"),
2✔
351
            (MessageDataItemName::Flags, b"FLAGS"),
2✔
352
            (MessageDataItemName::InternalDate, b"INTERNALDATE"),
2✔
353
            (MessageDataItemName::Rfc822, b"RFC822"),
2✔
354
            (MessageDataItemName::Rfc822Header, b"RFC822.HEADER"),
2✔
355
            (MessageDataItemName::Rfc822Size, b"RFC822.SIZE"),
2✔
356
            (MessageDataItemName::Rfc822Text, b"RFC822.TEXT"),
2✔
357
            (MessageDataItemName::Uid, b"UID"),
2✔
358
        ];
2✔
359

360
        for test in tests {
24✔
361
            known_answer_test_encode(test);
22✔
362
        }
22✔
363
    }
2✔
364

365
    #[test]
366
    fn test_encode_message_data_item() {
2✔
367
        let tests = [
2✔
368
            (
2✔
369
                MessageDataItem::Body(BodyStructure::Single {
2✔
370
                    body: Body {
2✔
371
                        basic: BasicFields {
2✔
372
                            parameter_list: vec![],
2✔
373
                            id: NString(None),
2✔
374
                            description: NString(None),
2✔
375
                            content_transfer_encoding: IString::try_from("base64").unwrap(),
2✔
376
                            size: 42,
2✔
377
                        },
2✔
378
                        specific: SpecificFields::Text {
2✔
379
                            subtype: IString::try_from("foo").unwrap(),
2✔
380
                            number_of_lines: 1337,
2✔
381
                        },
2✔
382
                    },
2✔
383
                    extension_data: None,
2✔
384
                }),
2✔
385
                b"BODY (\"TEXT\" \"foo\" NIL NIL NIL \"base64\" 42 1337)".as_ref(),
2✔
386
            ),
2✔
387
            (
2✔
388
                MessageDataItem::BodyExt {
2✔
389
                    section: None,
2✔
390
                    origin: None,
2✔
391
                    data: NString(None),
2✔
392
                },
2✔
393
                b"BODY[] NIL",
2✔
394
            ),
2✔
395
            (
2✔
396
                MessageDataItem::BodyExt {
2✔
397
                    section: None,
2✔
398
                    origin: Some(123),
2✔
399
                    data: NString(None),
2✔
400
                },
2✔
401
                b"BODY[]<123> NIL",
2✔
402
            ),
2✔
403
            (
2✔
404
                MessageDataItem::BodyStructure(BodyStructure::Single {
2✔
405
                    body: Body {
2✔
406
                        basic: BasicFields {
2✔
407
                            parameter_list: vec![],
2✔
408
                            id: NString(None),
2✔
409
                            description: NString(None),
2✔
410
                            content_transfer_encoding: IString::try_from("base64").unwrap(),
2✔
411
                            size: 213,
2✔
412
                        },
2✔
413
                        specific: SpecificFields::Text {
2✔
414
                            subtype: IString::try_from("").unwrap(),
2✔
415
                            number_of_lines: 224,
2✔
416
                        },
2✔
417
                    },
2✔
418
                    extension_data: None,
2✔
419
                }),
2✔
420
                b"BODYSTRUCTURE (\"TEXT\" \"\" NIL NIL NIL \"base64\" 213 224)",
2✔
421
            ),
2✔
422
            (
2✔
423
                MessageDataItem::Envelope(Envelope {
2✔
424
                    date: NString(None),
2✔
425
                    subject: NString(None),
2✔
426
                    from: vec![],
2✔
427
                    sender: vec![],
2✔
428
                    reply_to: vec![],
2✔
429
                    to: vec![],
2✔
430
                    cc: vec![],
2✔
431
                    bcc: vec![],
2✔
432
                    in_reply_to: NString(None),
2✔
433
                    message_id: NString(None),
2✔
434
                }),
2✔
435
                b"ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)",
2✔
436
            ),
2✔
437
            (MessageDataItem::Flags(vec![]), b"FLAGS ()"),
2✔
438
            (
2✔
439
                MessageDataItem::InternalDate(
2✔
440
                    DateTime::try_from(
2✔
441
                        chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200")
2✔
442
                            .unwrap(),
2✔
443
                    )
2✔
444
                    .unwrap(),
2✔
445
                ),
2✔
446
                b"INTERNALDATE \"01-Jul-2003 10:52:37 +0200\"",
2✔
447
            ),
2✔
448
            (MessageDataItem::Rfc822(NString(None)), b"RFC822 NIL"),
2✔
449
            (
2✔
450
                MessageDataItem::Rfc822Header(NString(None)),
2✔
451
                b"RFC822.HEADER NIL",
2✔
452
            ),
2✔
453
            (MessageDataItem::Rfc822Size(3456), b"RFC822.SIZE 3456"),
2✔
454
            (
2✔
455
                MessageDataItem::Rfc822Text(NString(None)),
2✔
456
                b"RFC822.TEXT NIL",
2✔
457
            ),
2✔
458
            (
2✔
459
                MessageDataItem::Uid(NonZeroU32::try_from(u32::MAX).unwrap()),
2✔
460
                b"UID 4294967295",
2✔
461
            ),
2✔
462
        ];
2✔
463

464
        for test in tests {
26✔
465
            known_answer_test_encode(test);
24✔
466
        }
24✔
467
    }
2✔
468

469
    #[test]
470
    fn test_encode_section() {
2✔
471
        let tests = [
2✔
472
            (
2✔
473
                Section::Part(Part(Vec1::from(NonZeroU32::try_from(1).unwrap()))),
2✔
474
                b"1".as_ref(),
2✔
475
            ),
2✔
476
            (Section::Header(None), b"HEADER"),
2✔
477
            (
2✔
478
                Section::Header(Some(Part(Vec1::from(NonZeroU32::try_from(1).unwrap())))),
2✔
479
                b"1.HEADER",
2✔
480
            ),
2✔
481
            (
2✔
482
                Section::HeaderFields(None, Vec1::from(AString::try_from("").unwrap())),
2✔
483
                b"HEADER.FIELDS (\"\")",
2✔
484
            ),
2✔
485
            (
2✔
486
                Section::HeaderFields(
2✔
487
                    Some(Part(Vec1::from(NonZeroU32::try_from(1).unwrap()))),
2✔
488
                    Vec1::from(AString::try_from("").unwrap()),
2✔
489
                ),
2✔
490
                b"1.HEADER.FIELDS (\"\")",
2✔
491
            ),
2✔
492
            (
2✔
493
                Section::HeaderFieldsNot(None, Vec1::from(AString::try_from("").unwrap())),
2✔
494
                b"HEADER.FIELDS.NOT (\"\")",
2✔
495
            ),
2✔
496
            (
2✔
497
                Section::HeaderFieldsNot(
2✔
498
                    Some(Part(Vec1::from(NonZeroU32::try_from(1).unwrap()))),
2✔
499
                    Vec1::from(AString::try_from("").unwrap()),
2✔
500
                ),
2✔
501
                b"1.HEADER.FIELDS.NOT (\"\")",
2✔
502
            ),
2✔
503
            (Section::Text(None), b"TEXT"),
2✔
504
            (
2✔
505
                Section::Text(Some(Part(Vec1::from(NonZeroU32::try_from(1).unwrap())))),
2✔
506
                b"1.TEXT",
2✔
507
            ),
2✔
508
            (
2✔
509
                Section::Mime(Part(Vec1::from(NonZeroU32::try_from(1).unwrap()))),
2✔
510
                b"1.MIME",
2✔
511
            ),
2✔
512
        ];
2✔
513

514
        for test in tests {
22✔
515
            known_answer_test_encode(test)
20✔
516
        }
517
    }
2✔
518
}
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