use Encode::MIME::Header;
use Email::MIME::ContentType qw/parse_content_type/;
use PublicInbox::Hval;
-use PublicInbox::MID qw/mid_clean mid_compressed mid2path/;
+use PublicInbox::MID qw/mid_clean mid_compress mid2path/;
use Digest::SHA;
require POSIX;
# public functions:
sub msg_html {
- my ($class, $mime, $full_pfx, $footer, $srch) = @_;
+ my ($ctx, $mime, $full_pfx, $footer) = @_;
if (defined $footer) {
$footer = "\n" . $footer;
} else {
$footer = '';
}
- headers_to_html_header($mime, $full_pfx, $srch) .
+ headers_to_html_header($mime, $full_pfx, $ctx) .
multipart_text_as_html($mime, $full_pfx) .
- '</pre><hr /><pre>' .
- html_footer($mime, 1, $full_pfx, $srch) .
+ '</pre><hr />' . PRE_WRAP .
+ html_footer($mime, 1, $full_pfx, $ctx) .
$footer .
'</pre></body></html>';
}
}
# this is already inside a <pre>
-# state = [ time, seen = {}, first_commit, page_nr = 0 ]
sub index_entry {
my ($fh, $mime, $level, $state) = @_;
- my ($srch, $seen, $first_commit) = @$state;
- my $midx = $state->[3]++;
+ my $midx = $state->{anchor_idx}++;
+ my $ctx = $state->{ctx};
+ my $srch = $ctx->{srch};
my ($prev, $next) = ($midx - 1, $midx + 1);
my $part_nr = 0;
my $enc = enc_for($mime->header("Content-Type"));
my $mid_raw = $header_obj->header('Message-ID');
my $id = anchor_for($mid_raw);
+ my $seen = $state->{seen};
$seen->{$id} = "#$id"; # save the anchor for later
my $mid = PublicInbox::Hval->new_msgid($mid_raw);
my $from = PublicInbox::Hval->new_oneline($mime->header('From'))->raw;
my @from = Email::Address->parse($from);
$from = $from[0]->name;
- (defined($from) && length($from)) or $from = $from[0]->address;
$from = PublicInbox::Hval->new_oneline($from)->as_html;
$subj = PublicInbox::Hval->new_oneline($subj)->as_html;
- my $root_anchor = $seen->{root_anchor};
my $more = 'permalink';
- my $path = $root_anchor ? '../' : '';
+ my $root_anchor = $state->{root_anchor};
+ my $path = $root_anchor ? '../../' : '';
my $href = $mid->as_href;
my $irt = $header_obj->header('In-Reply-To');
- my ($anchor_idx, $anchor, $t_anchor);
+ my ($anchor_idx, $anchor);
if (defined $irt) {
$anchor_idx = anchor_for($irt);
$anchor = $seen->{$anchor_idx};
- $t_anchor = T_ANCHOR;
- } else {
- $t_anchor = '';
}
- if (defined $srch) {
- $subj = "<a\nhref=\"${path}t/$href.html#u\">$subj</a>";
+ if ($srch) {
+ $subj = "<a\nhref=\"${path}t/$href/#u\">$subj</a>";
}
if ($root_anchor && $root_anchor eq $id) {
$subj = "<u\nid=\"u\">$subj</u>";
unless (defined $ts) {
$ts = msg_timestamp($mime);
}
- my $fmt = '%Y-%m-%d %H:%M';
- $ts = POSIX::strftime($fmt, gmtime($ts));
+ $ts = POSIX::strftime('%Y-%m-%d %H:%M', gmtime($ts));
my $rv = "<table\nsummary=l$level><tr>";
if ($level) {
$fh->write($rv .= "\n\n");
my ($fhref, $more_ref);
- my $mhref = "${path}m/$href.html";
+ my $mhref = "${path}m/$href/";
if ($level > 0) {
- $fhref = "${path}f/$href.html";
+ $fhref = "${path}f/$href/";
$more_ref = \$more;
}
# scan through all parts, looking for displayable text
});
$mime->body_set('');
- my $txt = "${path}m/$href.txt";
+ my $txt = "${path}m/$href/raw";
$rv = "\n<a\nhref=\"$mhref\">$more</a> <a\nhref=\"$txt\">raw</a> ";
- $rv .= html_footer($mime, 0);
+ $rv .= html_footer($mime, 0, undef, $ctx);
if (defined $irt) {
unless (defined $anchor) {
my $v = PublicInbox::Hval->new_msgid($irt);
$v = $v->as_href;
- $anchor = "${path}m/$v.html";
+ $anchor = "${path}m/$v/";
$seen->{$anchor_idx} = $anchor;
}
$rv .= " <a\nhref=\"$anchor\">parent</a>";
}
- if ($srch) {
- $rv .= " <a\nhref=\"${path}t/$href.html$t_anchor\">" .
- "threadlink</a>";
- }
-
$fh->write($rv .= '</pre></td></tr></table>');
}
sub emit_thread_html {
my ($cb, $ctx, $foot, $srch) = @_;
- my $mid = mid_compressed($ctx->{mid});
+ my $mid = mid_compress($ctx->{mid});
my $res = $srch->get_thread($mid);
my $msgs = load_results($res);
my $nr = scalar @$msgs;
return missing_thread($cb) if $nr == 0;
my $fh = $cb->([200,['Content-Type'=>'text/html; charset=UTF-8']]);
my $th = thread_results($msgs);
- my $state = [ $srch, { root_anchor => anchor_for($mid) }, undef, 0 ];
+ my $state = {
+ ctx => $ctx,
+ seen => {},
+ root_anchor => anchor_for($mid),
+ anchor_idx => 0,
+ };
{
require PublicInbox::GitCatFile;
my $git = PublicInbox::GitCatFile->new($ctx->{git_dir});
thread_entry($fh, $git, $state, $_, 0) for $th->rootset;
}
- my $final_anchor = $state->[3];
+ my $final_anchor = $state->{anchor_idx};
my $next = "<a\nid=\"s$final_anchor\">";
-
- if ($final_anchor == 1) {
- $next .= 'only message in thread';
- } else {
- $next .= 'end of thread';
- }
- $next .= "</a>, back to <a\nhref=\"../\">index</a>\n";
-
+ $next .= $final_anchor == 1 ? 'only message in' : 'end of';
+ $next .= " thread</a>, back to <a\nhref=\"../../\">index</a>\n";
+ $next .= "download: <a\nhref=\"mbox.gz\">mbox.gz</a>\n\n";
$fh->write("<hr />" . PRE_WRAP . $next . $foot .
"</pre></body></html>");
$fh->close;
$s =~ s/^\s*$//sgm;
$s =~ s/\s+\z//s;
- if (length $s) {
+ if ($s ne '') {
# kill per-line trailing whitespace
$s =~ s/[ \t]+$//sgm;
$s .= "\n" unless $s =~ /\n\z/s;
"$pad " . ascii_html($fn) . " $pad\n";
}
-my $LINK_RE = qr!\b((?:ftp|https?|nntp)://[@\w\+\&\?\.\%\;/#=-]+)!;
+my $LINK_RE = qr!\b((?:ftp|https?|nntp)://
+ [\@:\w\.-]+/
+ ?[\@\w\+\&\?\.\%\;/#=-]*)!x;
sub linkify {
# no newlines added here since it'd break the splitting we do
if ($full_pfx) {
if (!$final && scalar(@$quot) <= MAX_INLINE_QUOTED) {
# show quote inline
- my $rv = join("\n", map { linkify($_); $_ } @$quot);
+ my $rv = join('', map { linkify($_); $_ } @$quot);
@$quot = ();
- return $rv . "\n";
+ return $rv;
}
# show a short snippet of quoted text and link to full version:
# short version (see above)
my $nr = ++$$n;
my $rv = "<a\nid=q${part_nr}_$nr></a>";
- $rv .= join("\n", map { linkify($_); $_ } @$quot) . "\n";
+ $rv .= join('', map { linkify($_); $_ } @$quot);
@$quot = ();
$rv;
}
$part->body_set('');
$s = $enc->decode($s);
$s = ascii_html($s);
- my @lines = split(/\n/, $s);
+ my @lines = split(/^/m, $s);
$s = '';
if ($$part_nr > 0) {
# regular line, OK
linkify($cur);
$s .= $cur;
- $s .= "\n";
} else {
push @quot, $cur;
}
}
sub headers_to_html_header {
- my ($mime, $full_pfx, $srch) = @_;
-
+ my ($mime, $full_pfx, $ctx) = @_;
+ my $srch = $ctx->{srch} if $ctx;
my $rv = "";
my @title;
my $header_obj = $mime->header_obj;
my $mid_href = $mid->as_href;
foreach my $h (qw(From To Cc Subject Date)) {
my $v = $mime->header($h);
- defined($v) && length($v) or next;
+ defined($v) && ($v ne '') or next;
$v = PublicInbox::Hval->new_oneline($v);
if ($h eq 'From') {
} elsif ($h eq 'Subject') {
$title[0] = $v->as_html;
if ($srch) {
- $rv .= "$h: <a\nhref=\"../t/$mid_href.html\">";
+ $rv .= "$h: <a\nid=\"t\"\n" .
+ "href=\"../../t/$mid_href/\">";
$rv .= $v->as_html . "</a>\n";
next;
}
$rv .= "$h: " . $v->as_html . "\n";
}
-
$rv .= 'Message-ID: <' . $mid->as_html . '> ';
- $mid_href = "../m/$mid_href" unless $full_pfx;
- $rv .= "(<a\nhref=\"$mid_href.txt\">raw</a>)\n";
+ my $raw_ref = $full_pfx ? 'raw' : "../../m/$mid_href/raw";
+ $rv .= "(<a\nhref=\"$raw_ref\">raw</a>)\n";
+ if ($srch) {
+ $rv .= "<a\nhref=\"#r\">References: [see below]</a>\n";
+ } else {
+ $rv .= _parent_headers_nosrch($header_obj);
+ }
+ $rv .= "\n";
+
+ ("<html><head><title>". join(' - ', @title) .
+ '</title></head><body>' . PRE_WRAP . $rv);
+}
+
+sub thread_inline {
+ my ($dst, $ctx, $cur) = @_;
+ my $srch = $ctx->{srch};
+ my $mid = mid_compress(mid_clean($cur->header('Message-ID')));
+ my $res = $srch->get_thread($mid);
+ my $nr = $res->{total};
+
+ if ($nr <= 1) {
+ $$dst .= "\n[no followups, yet]</a>\n";
+ return;
+ }
+
+ $$dst .= "\n\n~$nr messages in thread: ".
+ "(<a\nhref=\"../../t/$mid/#u\">expand</a>)\n";
+ my $subj = $srch->subject_path($cur->header('Subject'));
+ my $state = {
+ seen => { $subj => 1 },
+ srch => $srch,
+ cur => $mid,
+ };
+ for (thread_results(load_results($res))->rootset) {
+ inline_dump($dst, $state, $_, 0);
+ }
+ $state->{next_msg};
+}
+
+sub _parent_headers_nosrch {
+ my ($header_obj) = @_;
+ my $rv = '';
my $irt = $header_obj->header('In-Reply-To');
if (defined $irt) {
my $html = $v->as_html;
my $href = $v->as_href;
$rv .= "In-Reply-To: <";
- $rv .= "<a\nhref=\"$href.html\">$html</a>>\n";
+ $rv .= "<a\nhref=\"../$href/\">$html</a>>\n";
}
my $refs = $header_obj->header('References');
$rv .= 'References: '. join(' ', @refs) . "\n";
}
}
-
- $rv .= "\n";
-
- ("<html><head><title>". join(' - ', @title) .
- '</title></head><body>' . PRE_WRAP . $rv);
+ $rv;
}
sub html_footer {
- my ($mime, $standalone, $full_pfx, $srch) = @_;
+ my ($mime, $standalone, $full_pfx, $ctx) = @_;
my %cc; # everyone else
my $to; # this is the From address
foreach my $h (qw(From To Cc)) {
my $v = $mime->header($h);
- defined($v) && length($v) or next;
+ defined($v) && ($v ne '') or next;
my @addrs = Email::Address->parse($v);
foreach my $recip (@addrs) {
my $address = $recip->address;
Email::Address->purge_cache if $standalone;
my $subj = $mime->header('Subject') || '';
- $subj = "Re: $subj" unless $subj =~ /\bRe:/;
- my $mid = $mime->header_obj->header('Message-ID');
+ $subj = "Re: $subj" unless $subj =~ /\bRe:/i;
+ my $mid = $mime->header('Message-ID');
my $irt = uri_escape_utf8($mid);
delete $cc{$to};
$to = uri_escape_utf8($to);
my $cc = uri_escape_utf8(join(',', sort values %cc));
my $href = "mailto:$to?In-Reply-To=$irt&Cc=${cc}&Subject=$subj";
- my $idx = $standalone ? " <a\nhref=\"../\">index</a>" : '';
+ my $srch = $ctx->{srch} if $ctx;
+ my $idx = $standalone ? " <a\nhref=\"../../\">index</a>" : '';
if ($idx && $srch) {
- $irt = $mime->header_obj->header('In-Reply-To') || '';
- $mid = mid_compressed(mid_clean($mid));
- my $t_anchor = length $irt ? T_ANCHOR : '';
- $idx = " <a\nhref=\"../t/$mid.html$t_anchor\">".
- "threadlink</a>$idx";
- my $res = $srch->get_followups($mid);
- if (my $c = $res->{total}) {
- $c = $c == 1 ? '1 followup' : "$c followups";
- $idx .= "\n$c:\n";
- $res->{srch} = $srch;
- thread_followups(\$idx, $mime, $res);
- } else {
- $idx .= "\n(no followups, yet)\n";
- }
- if ($irt) {
+ my $next = thread_inline(\$idx, $ctx, $mime);
+ if (defined $irt) {
$irt = PublicInbox::Hval->new_msgid($irt);
$irt = $irt->as_href;
- $irt = "<a\nhref=\"$irt\">parent</a> ";
+ $irt = "<a\nhref=\"../$irt/\">parent</a> ";
} else {
$irt = ' ' x length('parent ');
}
+ if ($next) {
+ $irt .= "<a\nhref=\"../$next/\">next</a> ";
+ } else {
+ $irt .= ' ';
+ }
} else {
$irt = '';
}
my $v = PublicInbox::Hval->new_msgid($_[0]);
my $html = $v->as_html;
my $href = $v->as_href;
- "<<a\nhref=\"$href.html\">$html</a>>";
+ "<<a\nhref=\"../$href/\">$html</a>>";
}
sub anchor_for {
my ($msgid) = @_;
my $id = $msgid;
if ($id !~ /\A[a-f0-9]{40}\z/) {
- $id = mid_compressed(mid_clean($id), 1);
+ $id = mid_compress(mid_clean($id), 1);
}
'm' . $id;
}
-sub simple_dump {
- my ($dst, $root, $node, $level) = @_;
- return unless $node;
- # $root = [ Root Message-ID, \%seen, $srch ];
- if (my $x = $node->message) {
- my $mid = $x->header('Message-ID');
- if ($root->[0] ne $mid) {
- my $pfx = ' ' x $level;
- $$dst .= $pfx;
- my $s = $x->header('Subject');
- my $h = $root->[2]->subject_path($s);
- if ($root->[1]->{$h}) {
- $s = '';
- } else {
- $root->[1]->{$h} = 1;
- $s = PublicInbox::Hval->new($s);
- $s = $s->as_html;
- }
- my $m = PublicInbox::Hval->new_msgid($mid);
- my $f = PublicInbox::Hval->new($x->header('X-PI-From'));
- my $d = PublicInbox::Hval->new($x->header('X-PI-Date'));
- $m = $m->as_href . '.html';
- $f = $f->as_html;
- $d = $d->as_html . ' UTC';
- if (length($s) == 0) {
- $$dst .= "` <a\nhref=\"$m\">$f @ $d</a>\n";
- } else {
- $$dst .= "` <a\nhref=\"$m\">$s</a>\n" .
- "$pfx by $f @ $d\n";
- }
- }
- }
- simple_dump($dst, $root, $node->child, $level+1);
- simple_dump($dst, $root, $node->next, $level);
-}
-
-sub thread_followups {
- my ($dst, $root, $res) = @_;
- $root->header_set('X-PI-TS', '0');
- my $msgs = load_results($res);
- push @$msgs, $root;
- my $th = thread_results($msgs);
- my $srch = $res->{srch};
- my $subj = $srch->subject_path($root->header('Subject'));
- my %seen = ($subj => 1);
- $root = [ $root->header('Message-ID'), \%seen, $srch ];
- simple_dump($dst, $root, $_, 0) for $th->rootset;
-}
-
sub thread_html_head {
my ($mime) = @_;
my $s = PublicInbox::Hval->new_oneline($mime->header('Subject'));
sub thread_entry {
my ($fh, $git, $state, $node, $level) = @_;
return unless $node;
- # $state = [ $search_res, $seen, undef, 0 (msg_nr) ];
- # $seen is overloaded with 3 types of fields:
- # 1) "root_anchor" => anchor_for(Message-ID),
- # 2) seen subject hashes: sha1(subject) => 1
- # 3) anchors hashes: "#$sha1_hex" (same as $seen in index_entry)
if (my $mime = $node->message) {
# lazy load the full message from mini_mime:
my $path = mid2path(mid_clean($mime->header('Message-ID')));
$mime = eval { Email::MIME->new($git->cat_file("HEAD:$path")) };
if ($mime) {
- if ($state->[3] == 0) {
+ if ($state->{anchor_idx} == 0) {
$fh->write(thread_html_head($mime));
}
index_entry($fh, $mime, $level, $state);
my $title = 'Thread does not exist';
$cb->([404, ['Content-Type' => 'text/html']])->write(<<EOF);
<html><head><title>$title</title></head><body><pre>$title
-<a href="../">Return to index</a></pre></body></html>
+<a href="../../">Return to index</a></pre></body></html>
EOF
}
+sub _inline_header {
+ my ($dst, $state, $mime, $level) = @_;
+ my $pfx = ' ' x $level;
+
+ my $cur = $state->{cur};
+ my $mid = $mime->header('Message-ID');
+ my $f = $mime->header('X-PI-From');
+ my $d = $mime->header('X-PI-Date');
+ $f = PublicInbox::Hval->new($f);
+ $d = PublicInbox::Hval->new($d);
+ $f = $f->as_html;
+ $d = $d->as_html . ' UTC';
+ my $midc = mid_compress(mid_clean($mid));
+ if ($cur) {
+ if ($cur eq $midc) {
+ delete $state->{cur};
+ $$dst .= "$pfx` <b><a\nid=\"r\"\nhref=\"#t\">".
+ "[this message]</a></b> by $f @ $d\n";
+
+ return;
+ }
+ } else {
+ $state->{next_msg} ||= $midc;
+ }
+
+ # Subject is never undef, this mail was loaded from
+ # our Xapian which would've resulted in '' if it were
+ # really missing (and Filter rejects empty subjects)
+ my $s = $mime->header('Subject');
+ my $h = $state->{srch}->subject_path($s);
+ if ($state->{seen}->{$h}) {
+ $s = undef;
+ } else {
+ $state->{seen}->{$h} = 1;
+ $s = PublicInbox::Hval->new($s);
+ $s = $s->as_html;
+ }
+ my $m = PublicInbox::Hval->new_msgid($mid);
+ $m = '../' . $m->as_href . '/';
+ if (defined $s) {
+ $$dst .= "$pfx` <a\nhref=\"$m\">$s</a>\n" .
+ "$pfx $f @ $d\n";
+ } else {
+ $$dst .= "$pfx` <a\nhref=\"$m\">$f @ $d</a>\n";
+ }
+}
+
+sub inline_dump {
+ my ($dst, $state, $node, $level) = @_;
+ return unless $node;
+ return if $state->{stopped};
+ if (my $mime = $node->message) {
+ _inline_header($dst, $state, $mime, $level);
+ }
+ inline_dump($dst, $state, $node->child, $level+1);
+ inline_dump($dst, $state, $node->next, $level);
+}
+
1;