mail-auth 0.15.1

DKIM, ARC, SPF and DMARC library for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 */

use std::net::{Ipv4Addr, Ipv6Addr};

use crate::{
    parse::TxtRecordParser,
    spf::{
        Directive, Macro, Mechanism, Qualifier, RR_FAIL, RR_NEUTRAL_NONE, RR_SOFTFAIL,
        RR_TEMP_PERM_ERROR, SpfRecord, Variable,
    },
};

use super::SPFParser;

#[test]
fn parse_spf() {
    for (record, expected_result) in [
        (
            "v=spf1 +mx a:colo.example.com/28 -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::A {
                            macro_string: Macro::Literal(b"colo.example.com".as_slice().into()),
                            ip4_mask: u32::MAX << (32 - 28),
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 a:A.EXAMPLE.COM -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::A {
                            macro_string: Macro::Literal(b"A.EXAMPLE.COM".as_slice().into()),
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 +mx -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 +mx redirect=_spf.example.com",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                redirect: Macro::Literal(b"_spf.example.com".as_slice().into()).into(),
                exp: None,
                directives: Box::new([Directive::new(
                    Qualifier::Pass,
                    Mechanism::Mx {
                        macro_string: Macro::None,
                        ip4_mask: u32::MAX,
                        ip6_mask: u128::MAX,
                    },
                )]),
            },
        ),
        (
            "v=spf1 a mx -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::A {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 include:example.com include:example.org -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Include {
                            macro_string: Macro::Literal(b"example.com".as_slice().into()),
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Include {
                            macro_string: Macro::Literal(b"example.org".as_slice().into()),
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 exists:%{ir}.%{l1r+-}._spf.%{d} -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Exists {
                            macro_string: Macro::List(Box::new([
                                Macro::Variable {
                                    letter: Variable::Ip,
                                    num_parts: 0,
                                    reverse: true,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                                Macro::Literal(b".".as_slice().into()),
                                Macro::Variable {
                                    letter: Variable::SenderLocalPart,
                                    num_parts: 1,
                                    reverse: true,
                                    escape: false,
                                    delimiters: (1u64 << (b'+' - b'+')) | (1u64 << (b'-' - b'+')),
                                },
                                Macro::Literal(b"._spf.".as_slice().into()),
                                Macro::Variable {
                                    letter: Variable::Domain,
                                    num_parts: 0,
                                    reverse: false,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                            ])),
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 mx -all exp=explain._spf.%{d}",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: Macro::List(Box::new([
                    Macro::Literal(b"explain._spf.".as_slice().into()),
                    Macro::Variable {
                        letter: Variable::Domain,
                        num_parts: 0,
                        reverse: false,
                        escape: false,
                        delimiters: 1u64 << (b'.' - b'+'),
                    },
                ]))
                .into(),
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 ip4:192.0.2.1 ip4:192.0.2.129 -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Ip4 {
                            addr: "192.0.2.1".parse().unwrap(),
                            mask: u32::MAX,
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Ip4 {
                            addr: "192.0.2.129".parse().unwrap(),
                            mask: u32::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 ip4:192.0.2.0/24 mx -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Ip4 {
                            addr: "192.0.2.0".parse().unwrap(),
                            mask: u32::MAX << (32 - 24),
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 mx/30 mx:example.org/30 -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX << (32 - 30),
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::Literal(b"example.org".as_slice().into()),
                            ip4_mask: u32::MAX << (32 - 30),
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 ptr -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Ptr {
                            macro_string: Macro::None,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 exists:%{l1r+}.%{d}",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([Directive::new(
                    Qualifier::Pass,
                    Mechanism::Exists {
                        macro_string: Macro::List(Box::new([
                            Macro::Variable {
                                letter: Variable::SenderLocalPart,
                                num_parts: 1,
                                reverse: true,
                                escape: false,
                                delimiters: 1u64 << (b'+' - b'+'),
                            },
                            Macro::Literal(b".".as_slice().into()),
                            Macro::Variable {
                                letter: Variable::Domain,
                                num_parts: 0,
                                reverse: false,
                                escape: false,
                                delimiters: 1u64 << (b'.' - b'+'),
                            },
                        ])),
                    },
                )]),
            },
        ),
        (
            "v=spf1 exists:%{ir}.%{l1r+}.%{d}",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([Directive::new(
                    Qualifier::Pass,
                    Mechanism::Exists {
                        macro_string: Macro::List(Box::new([
                            Macro::Variable {
                                letter: Variable::Ip,
                                num_parts: 0,
                                reverse: true,
                                escape: false,
                                delimiters: 1u64 << (b'.' - b'+'),
                            },
                            Macro::Literal(b".".as_slice().into()),
                            Macro::Variable {
                                letter: Variable::SenderLocalPart,
                                num_parts: 1,
                                reverse: true,
                                escape: false,
                                delimiters: 1u64 << (b'+' - b'+'),
                            },
                            Macro::Literal(b".".as_slice().into()),
                            Macro::Variable {
                                letter: Variable::Domain,
                                num_parts: 0,
                                reverse: false,
                                escape: false,
                                delimiters: 1u64 << (b'.' - b'+'),
                            },
                        ])),
                    },
                )]),
            },
        ),
        (
            "v=spf1 exists:_h.%{h}._l.%{l}._o.%{o}._i.%{i}._spf.%{d} ?all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Exists {
                            macro_string: Macro::List(Box::new([
                                Macro::Literal(b"_h.".as_slice().into()),
                                Macro::Variable {
                                    letter: Variable::HeloDomain,
                                    num_parts: 0,
                                    reverse: false,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                                Macro::Literal(b"._l.".as_slice().into()),
                                Macro::Variable {
                                    letter: Variable::SenderLocalPart,
                                    num_parts: 0,
                                    reverse: false,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                                Macro::Literal(b"._o.".as_slice().into()),
                                Macro::Variable {
                                    letter: Variable::SenderDomainPart,
                                    num_parts: 0,
                                    reverse: false,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                                Macro::Literal(b"._i.".as_slice().into()),
                                Macro::Variable {
                                    letter: Variable::Ip,
                                    num_parts: 0,
                                    reverse: false,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                                Macro::Literal(b"._spf.".as_slice().into()),
                                Macro::Variable {
                                    letter: Variable::Domain,
                                    num_parts: 0,
                                    reverse: false,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                            ])),
                        },
                    ),
                    Directive::new(Qualifier::Neutral, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 mx ?exists:%{ir}.whitelist.example.org -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(
                        Qualifier::Neutral,
                        Mechanism::Exists {
                            macro_string: Macro::List(Box::new([
                                Macro::Variable {
                                    letter: Variable::Ip,
                                    num_parts: 0,
                                    reverse: true,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                                Macro::Literal(b".whitelist.example.org".as_slice().into()),
                            ])),
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 mx exists:%{l}._%-spf_%_verify%%.%{d} -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Exists {
                            macro_string: Macro::List(Box::new([
                                Macro::Variable {
                                    letter: Variable::SenderLocalPart,
                                    num_parts: 0,
                                    reverse: false,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                                Macro::Literal(b"._%20spf_ verify%.".as_slice().into()),
                                Macro::Variable {
                                    letter: Variable::Domain,
                                    num_parts: 0,
                                    reverse: false,
                                    escape: false,
                                    delimiters: 1u64 << (b'.' - b'+'),
                                },
                            ])),
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 mx redirect=%{l1r+}._at_.%{o,=_/}._spf.%{d}",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: Macro::List(Box::new([
                    Macro::Variable {
                        letter: Variable::SenderLocalPart,
                        num_parts: 1,
                        reverse: true,
                        escape: false,
                        delimiters: 1u64 << (b'+' - b'+'),
                    },
                    Macro::Literal(b"._at_.".as_slice().into()),
                    Macro::Variable {
                        letter: Variable::SenderDomainPart,
                        num_parts: 0,
                        reverse: false,
                        escape: false,
                        delimiters: (1u64 << (b',' - b'+'))
                            | (1u64 << (b'=' - b'+'))
                            | (1u64 << (b'_' - b'+'))
                            | (1u64 << (b'/' - b'+')),
                    },
                    Macro::Literal(b"._spf.".as_slice().into()),
                    Macro::Variable {
                        letter: Variable::Domain,
                        num_parts: 0,
                        reverse: false,
                        escape: false,
                        delimiters: 1u64 << (b'.' - b'+'),
                    },
                ]))
                .into(),
                directives: Box::new([Directive::new(
                    Qualifier::Pass,
                    Mechanism::Mx {
                        macro_string: Macro::None,
                        ip4_mask: u32::MAX,
                        ip6_mask: u128::MAX,
                    },
                )]),
            },
        ),
        (
            "v=spf1 -ip4:192.0.2.0/24 a//96 +all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Fail,
                        Mechanism::Ip4 {
                            addr: "192.0.2.0".parse().unwrap(),
                            mask: u32::MAX << (32 - 24),
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::A {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX << (128 - 96),
                        },
                    ),
                    Directive::new(Qualifier::Pass, Mechanism::All),
                ]),
            },
        ),
        (
            concat!(
                "v=spf1 +mx/11//100 ~a:domain.com/12/123 ?ip6:::1 ",
                "-ip6:a::b/111 ip6:1080::8:800:68.0.3.1/96 "
            ),
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::None,
                            ip4_mask: u32::MAX << (32 - 11),
                            ip6_mask: u128::MAX << (128 - 100),
                        },
                    ),
                    Directive::new(
                        Qualifier::SoftFail,
                        Mechanism::A {
                            macro_string: Macro::Literal(b"domain.com".as_slice().into()),
                            ip4_mask: u32::MAX << (32 - 12),
                            ip6_mask: u128::MAX << (128 - 123),
                        },
                    ),
                    Directive::new(
                        Qualifier::Neutral,
                        Mechanism::Ip6 {
                            addr: "::1".parse().unwrap(),
                            mask: u128::MAX,
                        },
                    ),
                    Directive::new(
                        Qualifier::Fail,
                        Mechanism::Ip6 {
                            addr: "a::b".parse().unwrap(),
                            mask: u128::MAX << (128 - 111),
                        },
                    ),
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Ip6 {
                            addr: "1080::8:800:68.0.3.1".parse().unwrap(),
                            mask: u128::MAX << (128 - 96),
                        },
                    ),
                ]),
            },
        ),
        (
            "v=spf1 mx:example.org -all ra=postmaster rp=15 rr=e:f:s:n",
            SpfRecord {
                ra: Some(b"postmaster".as_slice().into()),
                rp: 15,
                rr: RR_FAIL | RR_NEUTRAL_NONE | RR_SOFTFAIL | RR_TEMP_PERM_ERROR,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Mx {
                            macro_string: Macro::Literal(b"example.org".as_slice().into()),
                            ip4_mask: u32::MAX,
                            ip6_mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
        (
            "v=spf1 ip6:fe80:0000:0000::0000:0000:0000:1 -all",
            SpfRecord {
                ra: None,
                rp: 100,
                rr: u8::MAX,
                exp: None,
                redirect: None,
                directives: Box::new([
                    Directive::new(
                        Qualifier::Pass,
                        Mechanism::Ip6 {
                            addr: "fe80:0000:0000::0000:0000:0000:1".parse().unwrap(),
                            mask: u128::MAX,
                        },
                    ),
                    Directive::new(Qualifier::Fail, Mechanism::All),
                ]),
            },
        ),
    ] {
        assert_eq!(
            SpfRecord::parse(record.as_bytes())
                .unwrap_or_else(|err| panic!("{record:?} : {err:?}")),
            expected_result,
            "{record}"
        );
    }
}

#[test]
fn parse_ip6() {
    for test in [
        "ABCD:EF01:2345:6789:ABCD:EF01:2345:6789",
        "2001:DB8:0:0:8:800:200C:417A",
        "FF01:0:0:0:0:0:0:101",
        "0:0:0:0:0:0:0:1",
        "0:0:0:0:0:0:0:0",
        "2001:DB8::8:800:200C:417A",
        "2001:DB8:0:0:8:800:200C::",
        "FF01::101",
        "1234::",
        "::1",
        "::",
        "a:b::c:d",
        "a::c:d",
        "a:b:c::d",
        "::c:d",
        "0:0:0:0:0:0:13.1.68.3",
        "0:0:0:0:0:FFFF:129.144.52.38",
        "::13.1.68.3",
        "::FFFF:129.144.52.38",
        "fe80::1",
        "fe80::0000:1",
        "fe80:0000::0000:1",
        "fe80:0000:0000:0000::1",
        "fe80:0000:0000:0000::0000:1",
        "fe80:0000:0000::0000:0000:0000:1",
        "fe80::0000:0000:0000:0000:0000:1",
        "fe80:0000:0000:0000:0000:0000:0000:1",
    ] {
        for test in [test.to_string(), format!("{test} ")] {
            let (ip, stop_char) = test
                .as_bytes()
                .iter()
                .ip6()
                .unwrap_or_else(|err| panic!("{test:?} : {err:?}"));
            assert_eq!(stop_char, b' ', "{test}");
            assert_eq!(ip, test.trim_end().parse::<Ipv6Addr>().unwrap())
        }
    }

    for invalid_test in [
        "0:0:0:0:0:0:0:1:1",
        "0:0:0:0:0:0:13.1.68.3.4",
        "::0:0:0:0:0:0:0:0",
        "0:0:0:0::0:0:0:0",
        " ",
        "",
    ] {
        assert!(
            invalid_test.as_bytes().iter().ip6().is_err(),
            "{}",
            invalid_test
        );
    }
}

#[test]
fn parse_ip4() {
    for test in ["0.0.0.0", "255.255.255.255", "13.1.68.3", "129.144.52.38"] {
        for test in [test.to_string(), format!("{test} ")] {
            let (ip, stop_char) = test
                .as_bytes()
                .iter()
                .ip4()
                .unwrap_or_else(|err| panic!("{test:?} : {err:?}"));
            assert_eq!(stop_char, b' ', "{test}");
            assert_eq!(ip, test.trim_end().parse::<Ipv4Addr>().unwrap());
        }
    }
}