t/linkify.t
t/main-bin/spamc
t/mda.t
+t/mid.t
t/msg_iter.t
t/msgmap.t
t/nntp.t
my $code = 404;
my $h = PublicInbox::Hval->new_msgid($mid);
- my $href = $h->as_href;
+ my $href = $h->{href};
my $html = $h->as_html;
my $title = "<$html> not found";
my $s = "<pre>Message-ID <$html>\nnot found\n";
my $u = $ibx->base_url($env) or next;
foreach my $m (@$res) {
my $p = PublicInbox::Hval->new_msgid($m);
- my $r = $p->as_href;
+ my $r = $p->{href};
my $t = $p->as_html;
$s .= qq{<a\nhref="$u$r/">$u$t/</a>\n};
}
sub exact {
my ($ctx, $found, $mid) = @_;
my $h = PublicInbox::Hval->new_msgid($mid);
- my $href = $h->as_href;
+ my $href = $h->{href};
my $html = $h->as_html;
my $title = "<$html> found in ";
my $end = @$found == 1 ? 'another inbox' : 'other inboxes';
my $fh = $cb->([200, ['Content-Type' => 'application/atom+xml']]);
my $ibx = $ctx->{-inbox};
my $html_url = $ibx->base_url($ctx->{env});
- $html_url .= PublicInbox::Hval->new_msgid($mid)->as_href;
+ $html_url .= PublicInbox::Hval->new_msgid($mid)->{href};
$feed_opts->{url} = $html_url;
$feed_opts->{emit_header} = 1;
my $mid = $header_obj->header_raw('Message-ID');
defined $mid or return;
$mid = PublicInbox::Hval->new_msgid($mid);
- my $href = $midurl . $mid->as_href . '/';
+ my $href = $midurl . $mid->{href}. '/';
my $date = $header_obj->header('Date');
my $updated = feed_updated($date);
use strict;
use warnings;
use Encode qw(find_encoding);
-use URI::Escape qw(uri_escape_utf8);
-use PublicInbox::MID qw/mid_clean/;
+use PublicInbox::MID qw/mid_clean mid_escape/;
use base qw/Exporter/;
our @EXPORT_OK = qw/ascii_html/;
sub new_msgid {
my ($class, $msgid) = @_;
$msgid = mid_clean($msgid);
- $class->new($msgid, $msgid);
+ $class->new($msgid, mid_escape($msgid));
}
sub new_oneline {
}
sub as_html { ascii_html($_[0]->{raw}) }
-sub as_href { ascii_html(uri_escape_utf8($_[0]->{href})) }
sub raw {
if (defined $_[1]) {
use strict;
use warnings;
use base qw/Exporter/;
-our @EXPORT_OK = qw/mid_clean id_compress mid2path mid_mime/;
+our @EXPORT_OK = qw/mid_clean id_compress mid2path mid_mime mid_escape/;
+use URI::Escape qw(uri_escape_utf8);
use Digest::SHA qw/sha1_hex/;
use constant MID_MAX => 40; # SHA-1 hex length
sub mid_mime ($) { $_[0]->header_obj->header_raw('Message-ID') }
+# RFC3986, section 3.3:
+sub MID_ESC () { '^A-Za-z0-9\-\._~!\$\&\';\(\)\*\+,;=:@' }
+sub mid_escape ($) { uri_escape_utf8($_[0], MID_ESC) }
+
1;
package PublicInbox::Mbox;
use strict;
use warnings;
-use PublicInbox::MID qw/mid_clean/;
-use URI::Escape qw/uri_escape_utf8/;
+use PublicInbox::MID qw/mid_clean mid_escape/;
require Email::Simple;
sub emit1 {
my $ibx = $ctx->{-inbox};
my $base = $ibx->base_url($ctx->{env});
my $mid = mid_clean($header_obj->header('Message-ID'));
- $mid = uri_escape_utf8($mid);
+ $mid = mid_escape($mid);
my @append = (
'Archived-At', "<$base$mid/>",
'List-Archive', "<$base>",
use fields qw(nntpd article rbuf ng long_res);
use PublicInbox::Search;
use PublicInbox::Msgmap;
+use PublicInbox::MID qw(mid_escape);
use PublicInbox::Git;
require PublicInbox::EvCleanup;
use Email::Simple;
use POSIX qw(strftime);
use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
-use URI::Escape qw(uri_escape_utf8);
use constant {
r501 => '501 command syntax error',
r221 => '221 Header follows',
$hdr->header_set('Xref', xref($ng, $n));
header_append($hdr, 'List-Post', "<mailto:$ng->{-primary_address}>");
if (my $url = $ng->base_url) {
- $mid = uri_escape_utf8($mid);
+ $mid = mid_escape($mid);
header_append($hdr, 'Archived-At', "<$url$mid/>");
header_append($hdr, 'List-Archive', "<$url>");
}
use strict;
use warnings;
use PublicInbox::Config;
-use URI::Escape qw(uri_escape_utf8);
+use PublicInbox::MID qw(mid_escape);
sub new {
my ($class, $pi_config) = @_;
# article IDs are not stable across clones,
# do not encourage caching/bookmarking them
$code = 302;
- $url .= uri_escape_utf8($mid) . '/';
+ $url .= mid_escape($mid) . '/';
}
}
use PublicInbox::SearchMsg;
use PublicInbox::Hval qw/ascii_html/;
use PublicInbox::View;
-use PublicInbox::MID qw(mid2path mid_mime mid_clean);
+use PublicInbox::MID qw(mid2path mid_mime mid_clean mid_escape);
use Email::MIME;
require PublicInbox::Git;
require PublicInbox::Thread;
my $s = ascii_html($smsg->subject);
my $f = ascii_html($smsg->from_name);
my $ts = PublicInbox::View::fmt_ts($smsg->ts);
- my $mid = PublicInbox::Hval->new_msgid($smsg->mid)->as_href;
+ my $mid = PublicInbox::Hval->new_msgid($smsg->mid)->{href};
$$res .= qq{$rank. <b><a\nhref="$mid/">}.
$s . "</a></b>\n";
$$res .= "$pfx - by $f @ $ts UTC [$pct%]\n\n";
use strict;
use warnings;
use PublicInbox::Hval;
+use PublicInbox::MID qw(mid_escape);
sub new {
my ($class, $qp) = @_;
$self = $tmp;
}
- my $q = PublicInbox::Hval->new($self->{'q'})->as_href;
+ my $q = mid_escape($self->{'q'});
$q =~ s/%20/+/g; # improve URL readability
my $qs = "q=$q";
use Date::Parse qw/str2time/;
use PublicInbox::Hval qw/ascii_html/;
use PublicInbox::Linkify;
-use PublicInbox::MID qw/mid_clean id_compress mid_mime/;
+use PublicInbox::MID qw/mid_clean id_compress mid_mime mid_escape/;
use PublicInbox::MsgIter;
use PublicInbox::Address;
use PublicInbox::WwwStream;
my $mid_raw = mid_clean(mid_mime($mime));
my $id = id_compress($mid_raw, 1);
my $id_m = 'm'.$id;
- my $mid = PublicInbox::Hval->new_msgid($mid_raw);
my $root_anchor = $ctx->{root_anchor} || '';
my $irt = in_reply_to($hdr);
}
$rv .= "From: "._hdr_names($hdr, 'From').' @ '._msg_date($hdr)." UTC";
my $upfx = $ctx->{-upfx};
- my $mhref = $upfx . $mid->as_href . '/';
+ my $mhref = $upfx . mid_escape($mid_raw) . '/';
$rv .= qq{ (<a\nhref="$mhref">permalink</a> / };
$rv .= qq{<a\nhref="${mhref}raw">raw</a>)\n};
$rv .= ' '.join('; +', @tocc) . "\n" if @tocc;
my $mapping = $ctx->{mapping};
if (!$mapping && $irt) {
my $mirt = PublicInbox::Hval->new_msgid($irt);
- my $href = $upfx . $mirt->as_href . '/';
+ my $href = $upfx . $mirt->{href}. '/';
my $html = $mirt->as_html;
$rv .= qq(In-Reply-To: <<a\nhref="$href">$html</a>>\n)
}
if (defined $irt) {
my $v = PublicInbox::Hval->new_msgid($irt);
my $html = $v->as_html;
- my $href = $v->as_href;
+ my $href = $v->{href};
$rv .= "In-Reply-To: <";
$rv .= "<a\nhref=\"../$href/\">$html</a>>\n";
}
$subj = "Re: $subj" unless $subj =~ /\bRe:/i;
my $mid = $hdr->header_raw('Message-ID');
push @arg, '--in-reply-to='.squote_maybe(mid_clean($mid));
- my $irt = uri_escape_utf8($mid);
+ my $irt = mid_escape($mid);
delete $cc{$to};
push @arg, "--to=$to";
$to = uri_escape_utf8($to);
$next = $prev = ' ';
if (my $n = $ctx->{next_msg}) {
- $n = PublicInbox::Hval->new_msgid($n)->as_href;
+ $n = PublicInbox::Hval->new_msgid($n)->{href};
$next = "<a\nhref=\"$upfx$n/\"\nrel=next>next</a>";
}
my $u;
my $par = $ctx->{parent_msg};
if ($par) {
- $u = PublicInbox::Hval->new_msgid($par)->as_href;
+ $u = PublicInbox::Hval->new_msgid($par)->{href};
$u = "$upfx$u/";
}
if (my $p = $ctx->{prev_msg}) {
- $prev = PublicInbox::Hval->new_msgid($p)->as_href;
+ $prev = PublicInbox::Hval->new_msgid($p)->{href};
if ($p && $par && $p eq $par) {
$prev = "<a\nhref=\"$upfx$prev/\"\n" .
'rel=prev>prev parent</a>';
sub linkify_ref_nosrch {
my $v = PublicInbox::Hval->new_msgid($_[0]);
my $html = $v->as_html;
- my $href = $v->as_href;
+ my $href = $v->{href};
"<<a\nhref=\"../$href/\">$html</a>>";
}
return '[no common parent]' if ($mid eq 'subject dummy');
$mid = PublicInbox::Hval->new_msgid($mid);
- my $href = $mid->as_href;
+ my $href = $mid->{href};
my $html = $mid->as_html;
qq{[parent not found: <<a\nhref="$upfx$href/">$html</a>>]};
}
$s = PublicInbox::Hval->new($s);
$s = $s->as_html;
}
- my $m = PublicInbox::Hval->new_msgid($mid);
+ my $m;
my $id = '';
my $mapping = $ctx->{mapping};
my $end = defined($s) ? "$s</a> $f\n" : "$f</a>\n";
$map->[1] = "$d<a\nhref=\"$m\">$end";
$id = "\nid=r".$id;
} else {
- $m = $ctx->{-upfx}.$m->as_href.'/';
+ $m = $ctx->{-upfx}.mid_escape($mid).'/';
}
$$dst .= $d . "<a\nhref=\"$m\"$id>" . $end;
}
$d .= indent_for($level) . th_pfx($level);
my $upfx = $ctx->{-upfx};
my $m = PublicInbox::Hval->new_msgid($mid);
- my $href = $upfx . $m->as_href . '/';
+ my $href = $upfx . $m->{href} . '/';
my $html = $m->as_html;
if ($map) {
@$topic = ();
next unless defined $top; # ghost topic
my $mid = delete $seen->{$top};
- my $href = PublicInbox::Hval->new_msgid($mid)->as_href;
+ my $href = mid_escape($mid);
$top = PublicInbox::Hval->new($top)->as_html;
$ts = fmt_ts($ts);
my $sub = $ex[$i + 1];
$mid = delete $seen->{$sub};
$sub = PublicInbox::Hval->new($sub)->as_html;
- $href = PublicInbox::Hval->new_msgid($mid)->as_href;
+ $href = mid_escape($mid);
$s .= indent_for($level) . TCHILD;
$s .= "<a\nhref=\"$href/T/#u\">$sub</a>\n";
}
use warnings;
use PublicInbox::Config;
use PublicInbox::Hval;
-use URI::Escape qw(uri_escape_utf8 uri_unescape);
+use URI::Escape qw(uri_unescape);
+use PublicInbox::MID qw(mid_escape);
require PublicInbox::Git;
use PublicInbox::GitHTTPBackend;
our $INBOX_RE = qr!\A/([\w\.\-]+)!;
}
my $url = $obj->base_url($ctx->{env});
my $qs = $ctx->{env}->{QUERY_STRING};
- $url .= (uri_escape_utf8($mid) . '/') if (defined $mid);
+ $url .= (mid_escape($mid) . '/') if (defined $mid);
$url .= $suffix if (defined $suffix);
$url .= "?$qs" if $qs ne '';
if ($mid =~ /\A<(.+)>\z/) {
$mid = $1;
}
- $mid = uri_escape_utf8($mid);
+ $mid = uri_escape_utf8($mid,
+ '^A-Za-z0-9\-\._~!\$\&\';\(\)\*\+,;=:@');
$header_obj->header_set('List-Archive', "<$archive_url>");
foreach my $h (qw(Help Unsubscribe Subscribe Owner)) {
like($res->{head}, qr/Status:\s*206/i, "info/refs partial past end OK");
is($res->{body}, substr($orig, 5), 'partial body OK past end');
}
-
+use Data::Dumper;
# atom feeds
{
local $ENV{HOME} = $home;
like($res->{body}, qr/<title>test for public-inbox/,
"set title in XML feed");
like($res->{body},
- qr!http://test\.example\.com/test/blah%40example\.com/!,
+ qr!http://test\.example\.com/test/blah\@example\.com/!,
"link id set");
like($res->{body}, qr/what\?/, "reply included");
}
$im->add($reply);
$im->done;
- my $res = cgi_run("/test/slashy%2fasdf%40example.com/raw");
+ my $res = cgi_run("/test/slashy%2fasdf\@example.com/raw");
like($res->{body}, qr/Message-Id: <\Q$slashy_mid\E>/,
"slashy mid raw hit");
$res = cgi_run("/test/blahblah\@example.com/f/");
like($res->{head}, qr/Status: 301 Moved/, "301 response");
like($res->{head},
- qr!^Location: http://[^/]+/test/blahblah%40example\.com/\r\n!ms,
+ qr!^Location: http://[^/]+/test/blahblah\@example\.com/\r\n!ms,
'301 redirect location');
$res = cgi_run("/test/blahblah\@example.con/");
like($res->{head}, qr/Status: 300 Multiple Choices/, "mid html miss");
$res = cgi_run("/test/new.html");
- like($res->{body}, qr/slashy%2Fasdf%40example\.com/,
+ like($res->{body}, qr/slashy%2Fasdf\@example\.com/,
"slashy URL generated correctly");
}
# retrieve thread as an mbox
{
local $ENV{HOME} = $home;
- my $path = "/test/blahblah%40example.com/t.mbox.gz";
+ my $path = "/test/blahblah\@example.com/t.mbox.gz";
my $res = cgi_run($path);
like($res->{head}, qr/^Status: 501 /, "search not-yet-enabled");
my $indexed = system($index, $maindir) == 0;
my $have_xml_feed = eval { require XML::Feed; 1 } if $indexed;
if ($have_xml_feed) {
- $path = "/test/blahblah%40example.com/t.atom";
+ $path = "/test/blahblah\@example.com/t.atom";
$res = cgi_run($path);
like($res->{head}, qr/^Status: 200 /, "atom returned 200");
like($res->{head}, qr!^Content-Type: application/atom\+xml!m,
warn "W: ".$r->code . " $u\n"
}
- # check bad links
- my @at = grep(/@/, @links);
- print "BAD: $u ", join("\n", @at), "\n" if @at;
-
my $s;
# blocking
foreach my $l (@links, "DONE\t$u") {
--- /dev/null
+# Copyright (C) 2016 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use Test::More;
+use PublicInbox::MID qw(mid_escape);
+
+is(mid_escape('foo!@(bar)'), 'foo!@(bar)');
+is(mid_escape('foo%!@(bar)'), 'foo%25!@(bar)');
+is(mid_escape('foo%!@(bar)'), 'foo%25!@(bar)');
+
+done_testing();
+1;
PublicInbox::NNTP::set_nntp_headers($mime->header_obj, $ng, 1, $mid);
is_deeply([ $mime->header('Message-ID') ], [ "<$mid>" ],
'Message-ID unchanged');
- is_deeply([ $mime->header('Archived-At') ], [ "<${u}a%40b/>" ],
+ is_deeply([ $mime->header('Archived-At') ], [ "<${u}a\@b/>" ],
'Archived-At: set');
is_deeply([ $mime->header('List-Archive') ], [ "<$u>" ],
'List-Archive: set');
is_deeply([ $mime->header('Message-ID') ], [ "<$mid>" ],
'Message-ID unchanged');
is_deeply([ $mime->header('Archived-At') ],
- [ "<${u}a%40b/>", '<http://mirror.example.com/m/a%40b/>' ],
+ [ "<${u}a\@b/>", '<http://mirror.example.com/m/a@b/>' ],
'Archived-At: appended');
is_deeply([ $mime->header('Xref') ], [ 'example.com test:2' ],
'Old Xref: clobbered');
foreach my $t (qw(t T)) {
test_psgi($app, sub {
my ($cb) = @_;
- my $u = $pfx . "/blah%40example.com/$t";
+ my $u = $pfx . "/blah\@example.com/$t";
my $res = $cb->(GET($u));
is(301, $res->code, "redirect for missing /");
my $location = $res->header('Location');
foreach my $t (qw(f)) {
test_psgi($app, sub {
my ($cb) = @_;
- my $u = $pfx . "/blah%40example.com/$t";
+ my $u = $pfx . "/blah\@example.com/$t";
my $res = $cb->(GET($u));
is(301, $res->code, "redirect for legacy /f");
my $location = $res->header('Location');
- like($location, qr!/blah%40example\.com/\z!,
+ like($location, qr!/blah\@example\.com/\z!,
'redirected with missing /');
});
}
is(200, $res->code, 'success response received');
like($res->content, qr!href="new\.atom"!,
'atom URL generated');
- like($res->content, qr!href="blah%40example\.com/"!,
+ like($res->content, qr!href="blah\@example\.com/"!,
'index generated');
});
my $res = $cb->(GET($pfx . '/atom.xml'));
is(200, $res->code, 'success response received for atom');
like($res->content,
- qr!link\s+href="\Q$pfx\E/blah%40example\.com/"!s,
+ qr!link\s+href="\Q$pfx\E/blah\@example\.com/"!s,
'atom feed generated correct URL');
});
test_psgi($app, sub {
my ($cb) = @_;
- my $path = '/blah%40example.com/';
+ my $path = '/blah@example.com/';
my $res = $cb->(GET($pfx . $path));
is(200, $res->code, "success for $path");
like($res->content, qr!<title>hihi - Me</title>!,
$res = $cb->(GET($pfx . $path));
is(301, $res->code, "redirect for $path");
my $location = $res->header('Location');
- like($location, qr!/blah%40example\.com/\z!,
+ like($location, qr!/blah\@example\.com/\z!,
'/$MESSAGE_ID/f/ redirected to /$MESSAGE_ID/');
});
test_psgi($app, sub {
my ($cb) = @_;
- my $res = $cb->(GET($pfx . '/blah%40example.com/raw'));
+ my $res = $cb->(GET($pfx . '/blah@example.com/raw'));
is(200, $res->code, 'success response received for /*/raw');
like($res->content, qr!^From !sm, "mbox returned");
});
foreach my $t (qw(m f)) {
test_psgi($app, sub {
my ($cb) = @_;
- my $res = $cb->(GET($pfx . "/$t/blah%40example.com.txt"));
+ my $res = $cb->(GET($pfx . "/$t/blah\@example.com.txt"));
is(301, $res->code, "redirect for old $t .txt link");
my $location = $res->header('Location');
- like($location, qr!/blah%40example\.com/raw\z!,
+ like($location, qr!/blah\@example\.com/raw\z!,
".txt redirected to /raw");
});
}
while (my ($t, $e) = each %umap) {
test_psgi($app, sub {
my ($cb) = @_;
- my $res = $cb->(GET($pfx . "/$t/blah%40example.com.html"));
+ my $res = $cb->(GET($pfx . "/$t/blah\@example.com.html"));
is(301, $res->code, "redirect for old $t .html link");
my $location = $res->header('Location');
like($location,
- qr!/blah%40example\.com/$e(?:#u)?\z!,
+ qr!/blah\@example\.com/$e(?:#u)?\z!,
".html redirected to new location");
});
}
foreach my $sfx (qw(mbox mbox.gz)) {
test_psgi($app, sub {
my ($cb) = @_;
- my $res = $cb->(GET($pfx . "/t/blah%40example.com.$sfx"));
+ my $res = $cb->(GET($pfx . "/t/blah\@example.com.$sfx"));
is(301, $res->code, 'redirect for old thread link');
my $location = $res->header('Location');
like($location,
- qr!/blah%40example\.com/t\.mbox(?:\.gz)?\z!,
+ qr!/blah\@example\.com/t\.mbox(?:\.gz)?\z!,
"$sfx redirected to /mbox.gz");
});
}
is($res->code, 200, 'OK with URLMap mount');
$res = $cb->(GET('/a/test/m/blah%40example.com.html'));
is($res->header('Location'),
- 'http://localhost/a/test/blah%40example.com/',
+ 'http://localhost/a/test/blah@example.com/',
'redirect functions properly under mount');
$res = $cb->(GET('/test/blah%40example.com/'));