Email-Address-List

 view release on metacpan or  search on metacpan

t/invisible-chars.t  view on Meta::CPAN

use strict; use warnings;
use Test::More;
use Email::Address::List;

# An addr-spec (local-part@domain) should never contain
# whitespace or invisible format characters. They sneak in via
# copy/paste and produce undeliverable mail.
#
# Test the following:
#   - ASCII whitespace (space, tab, ...)
#   - Unicode whitespace (NBSP, EM SPACE, IDEOGRAPHIC SPACE, ...)
#   - Unicode format characters \p{Cf} (ZWSP, ZWJ, ZWNJ, BOM,
#     soft hyphen, bidi marks, ...)

sub addr_of {
    my $line = shift;
    my @list = Email::Address::List->parse($line);
    return undef unless @list == 1 && $list[0]{type} eq 'mailbox';
    return $list[0]{value}->address;
}

# name => character. Mix of Unicode general categories \p{Z}
# (separators), other \p{White_Space}, and \p{Cf} (format).
my @cases = (
    [ 'SPACE'                  => " "        ],
    [ 'TAB'                    => "\t"       ],
    [ 'U+00A0 NO-BREAK SPACE'  => "\x{00A0}" ],
    [ 'U+1680 OGHAM SPACE MARK'=> "\x{1680}" ],
    [ 'U+2003 EM SPACE'        => "\x{2003}" ],
    [ 'U+202F NARROW NBSP'     => "\x{202F}" ],
    [ 'U+3000 IDEOGRAPHIC SP'  => "\x{3000}" ],
    [ 'U+00AD SOFT HYPHEN'     => "\x{00AD}" ],
    [ 'U+200B ZERO WIDTH SP'   => "\x{200B}" ],
    [ 'U+200C ZWNJ'            => "\x{200C}" ],
    [ 'U+200D ZWJ'             => "\x{200D}" ],
    [ 'U+200E LRM'             => "\x{200E}" ],
    [ 'U+200F RLM'             => "\x{200F}" ],
    [ 'U+2060 WORD JOINER'     => "\x{2060}" ],
    [ 'U+FEFF ZWNBSP / BOM'    => "\x{FEFF}" ],
);

# Each character, in every position around or inside the addr-spec.
for my $case (@cases) {
    my ($name, $char) = @$case;

    is addr_of("foo\@example.com$char"), 'foo@example.com',
        "trailing $name stripped";

    is addr_of("${char}foo\@example.com"), 'foo@example.com',
        "leading $name stripped";

    is addr_of("foo${char}\@example.com"), 'foo@example.com',
        "$name before \@ stripped";

    is addr_of("foo\@${char}example.com"), 'foo@example.com',
        "$name after \@ stripped";

    is addr_of("foo\@example${char}.com"), 'foo@example.com',
        "$name inside domain stripped";

    is addr_of("<foo\@example.com$char>"), 'foo@example.com',
        "$name inside <...> stripped";
}

# Display name + angle-addr: address cleaned, phrase preserved.
{
    my @list = Email::Address::List->parse(
        qq{"Jane Doe" <foo\@example.com\x{200B}>});
    is scalar @list, 1, 'name-addr with U+200B parsed as one mailbox';
    is $list[0]{type}, 'mailbox', '...of type mailbox';
    is $list[0]{value}->address, 'foo@example.com',
        '...with U+200B stripped from address part';
    is $list[0]{value}->phrase, 'Jane Doe',
        '...and display name unchanged';
}

# After cleanup the address is pure ASCII -- not_ascii must be 0
# so downstream skip_not_ascii / loop-prevention checks behave.
{
    my @list = Email::Address::List->parse("foo\@example.com\x{200B}");
    is $list[0]{not_ascii}, 0,
        'cleaned address reported as ASCII (not_ascii=0)';
}

# Comma-separated list: one address with invisibles, one without.
{
    my @list = Email::Address::List->parse(
        "foo\@example.com\x{200B}, bar\@example.com");
    is scalar @list, 2, 'two mailboxes in comma list';
    is $list[0]{value}->address, 'foo@example.com',
        '...first address cleaned';
    is $list[1]{value}->address, 'bar@example.com',
        '...second address unaffected';
}

# Multiple distinct invisibles in one address all stripped.
is addr_of("\x{FEFF}foo\x{200B}\@example.com\x{200D}"),
    'foo@example.com',
    'multiple distinct invisible chars all stripped';



( run in 1.255 second using v1.01-cache-2.11-cpan-9581c071862 )