use URI::Escape qw/uri_escape_utf8/;
use Date::Parse qw/str2time/;
use Encode::MIME::Header;
+use Plack::Util;
use PublicInbox::Hval qw/ascii_html/;
use PublicInbox::Linkify;
-use PublicInbox::MID qw/mid_clean id_compress mid2path mid_mime/;
+use PublicInbox::MID qw/mid_clean id_compress mid_mime/;
use PublicInbox::MsgIter;
use PublicInbox::Address;
+use PublicInbox::WwwStream;
require POSIX;
use constant INDENT => ' ';
sub th_pfx ($) { $_[0] == 0 ? '' : TCHILD };
# public functions: (unstable)
-# TODO: stream this, since threading is expensive but also oh-so-important
sub msg_html {
my ($ctx, $mime, $footer) = @_;
- $footer = defined($footer) ? "\n$footer" : '';
my $hdr = $mime->header_obj;
- headers_to_html_header($hdr, $ctx) .
- multipart_text_as_html($mime, '') .
- '</pre><hr /><pre>' .
- html_footer($hdr, 1, $ctx) .
- '</pre>' . msg_reply($ctx, $hdr) .
- '<hr /><pre>'. $footer . '</pre></body></html>';
+ my $tip = _msg_html_prepare($hdr, $ctx);
+ PublicInbox::WwwStream->new($ctx, sub {
+ my ($nr, undef) = @_;
+ if ($nr == 1) {
+ $tip . multipart_text_as_html($mime, '') .
+ '</pre><hr />'
+ } elsif ($nr == 2) {
+ # fake an EOF if generating the footer fails;
+ # we want to at least show the message if something
+ # here crashes:
+ eval {
+ '<pre>' . html_footer($hdr, 1, $ctx) .
+ '</pre>' . msg_reply($ctx, $hdr)
+ };
+ } else {
+ undef
+ }
+ });
}
# /$INBOX/$MESSAGE_ID/#R
sub msg_reply {
my ($ctx, $hdr) = @_;
- my $se_url =
- 'https://kernel.org/pub/software/scm/git/docs/git-send-email.html';
+ my $se_url = 'https://git-htmldocs.bogomips.org/git-send-email.html';
my ($arg, $link) = mailto_arg_link($hdr);
push @$arg, '/path/to/YOUR_REPLY';
- "<hr /><pre\nid=R>".
- "You may reply publically to <a\nhref=#t>this message</a> via email\n".
- "using any one of the following methods:\n\n" .
+ "<pre\nid=R>".
+ "You may reply publically to <a\nhref=#t>this message</a> via\n".
+ "plain-text email using any one of the following methods:\n\n" .
"* Save the following mbox file, import it into your mail client,\n" .
" and reply-to-all from there: <a\nhref=raw>mbox</a>\n\n" .
"* Reply to all the recipients using the <b>--to</b>, <b>--cc</b>,\n" .
" and <b>--in-reply-to</b> switches of git-send-email(1):\n\n" .
- "\tgit send-email \\\n\t\t" .
- join(" \\ \n\t\t", @$arg ). "\n\n" .
+ " git send-email \\\n " .
+ join(" \\\n ", @$arg ). "\n\n" .
qq( <a\nhref="$se_url">$se_url</a>\n\n) .
"* If your mail client supports setting the <b>In-Reply-To</b>" .
" header\n via mailto: links, try the " .
undef;
}
+sub _hdr_names ($$) {
+ my ($hdr, $field) = @_;
+ my $val = $hdr->header($field) or return '';
+ ascii_html(join(', ', PublicInbox::Address::names($val)));
+}
+
+sub nr_to_s ($$$) {
+ my ($nr, $singular, $plural) = @_;
+ return "0 $plural" if $nr == 0;
+ $nr == 1 ? "$nr $singular" : "$nr $plural";
+}
+
# this is already inside a <pre>
sub index_entry {
my ($mime, $level, $state) = @_;
- my $midx = $state->{anchor_idx}++;
+ $state->{anchor_idx}++;
my $ctx = $state->{ctx};
my $srch = $ctx->{srch};
my $hdr = $mime->header_obj;
my $subj = $hdr->header('Subject');
my $mid_raw = mid_clean(mid_mime($mime));
- my $id = anchor_for($mid_raw);
- my $seen = $state->{seen};
- $seen->{$id} = "#$id"; # save the anchor for children, later
-
+ my $id = id_compress($mid_raw);
+ my $id_m = 'm'.$id;
my $mid = PublicInbox::Hval->new_msgid($mid_raw);
- my $from = PublicInbox::Address::from_name($hdr->header('From'));
my $root_anchor = $state->{root_anchor} || '';
my $path = $root_anchor ? '../../' : '';
my $href = $mid->as_href;
my $irt = in_reply_to($hdr);
- my $parent_anchor = $seen->{anchor_for($irt)} if defined $irt;
- $from = ascii_html($from);
- $subj = ascii_html($subj);
- $subj = "<a\nhref=\"${path}$href/\">$subj</a>";
- $subj = "<u\nid=u>$subj</u>" if $root_anchor eq $id;
+ $subj = '<b>'.ascii_html($subj).'</b>';
+ $subj = "<u\nid=u>$subj</u>" if $root_anchor eq $id_m;
my $ts = _msg_date($hdr);
- my $rv = "<pre\nid=s$midx>";
- $rv .= "<b\nid=$id>$subj</b>\n";
- my $txt = "${path}$href/raw";
- my $fh = $state->{fh};
- $fh->write($rv .= "- $from @ $ts UTC (<a\nhref=\"$txt\">raw</a>)\n\n");
-
- my $mhref = "${path}$href/";
+ my $rv = "<pre><a\nhref=#e$id\nid=$id_m>#</a> ";
+ $rv .= $subj;
+ my $mhref = $path.$href.'/';
+ my $from = _hdr_names($hdr, 'From');
+ $rv .= "\n- $from @ $ts UTC\n";
+ my @tocc;
+ foreach my $f (qw(To Cc)) {
+ my $dst = _hdr_names($hdr, $f);
+ push @tocc, "$f: $dst" if $dst ne '';
+ }
+ $rv .= ' '.join('; +', @tocc) . "\n" if @tocc;
+ $rv .= "\n";
# scan through all parts, looking for displayable text
- msg_iter($mime, sub { index_walk($fh, $mhref, $_[0]) });
- $rv = "\n" . html_footer($hdr, 0, $ctx, "$path$href/#R");
-
+ msg_iter($mime, sub { $rv .= add_text_body($mhref, $_[0]) });
+ $rv .= "\n<a\nhref=\"$mhref\"\n>permalink</a>" .
+ " / <a\nhref=\"${mhref}raw\">raw</a> / ";
+ my $mapping = $state->{mapping};
+ my $nr_c = $mapping->{$mid_raw} || 0;
+ my $nr_s = 0;
if (defined $irt) {
- unless (defined $parent_anchor) {
- my $v = PublicInbox::Hval->new_msgid($irt, 1);
- $v = $v->as_href;
- $parent_anchor = "${path}$v/";
- }
- $rv .= " <a\nhref=\"$parent_anchor\">parent</a>";
+ $nr_s = ($mapping->{$irt} || 0) - 1;
+ $nr_s = 0 if $nr_s < 0;
+ $irt = anchor_for($irt);
+ $rv .= "<a\nhref=#$irt>#parent</a>,";
+ } else {
+ $rv .= 'root message:';
}
+ $nr_s = nr_to_s($nr_s, 'sibling', 'siblings');
+ $nr_c = nr_to_s($nr_c, 'reply', 'replies');
+ $rv .= " <a\nhref=#r$id\nid=e$id>$nr_s, $nr_c</a>";
+ $rv .= " / <a\nhref=\"${mhref}#R\">reply</a>";
+
if (my $pct = $state->{pct}) { # used by SearchView.pm
$rv .= " [relevance $pct->{$mid_raw}%]";
- } elsif ($srch) {
- my $threaded = 'threaded';
- my $flat = 'flat';
- my $end = '';
- if ($ctx->{flat}) {
- $flat = "<b>$flat</b>";
- $end = "\n"; # for lynx
- } else {
- $threaded = "<b>$threaded</b>";
- }
- $rv .= " [<a\nhref=\"${path}$href/t/#u\">$threaded</a>";
- $rv .= "|<a\nhref=\"${path}$href/T/#u\">$flat</a>]$end";
}
- $fh->write($rv .= '</pre>');
+ $state->{fh}->write($rv .= "\n</pre>"); # '\n' for lynx
}
sub thread_html {
sub { emit_thread_html($_[0], $ctx, $foot, $srch) }
}
+sub walk_thread {
+ my ($th, $state, $cb) = @_;
+ my @q = map { (0, $_) } $th->rootset;
+ while (@q) {
+ my $level = shift @q;
+ my $node = shift @q or next;
+ $cb->($state, $level, $node);
+ unshift @q, $level+1, $node->child, $level, $node->next;
+ }
+}
+
+sub pre_thread {
+ my ($state, $level, $node) = @_;
+ my $parent = $node->parent;
+ if ($parent) {
+ my $mid = $parent->messageid;
+ my $m = $state->{mapping};
+ $m->{$mid} ||= 0;
+ $m->{$mid}++;
+ }
+ skel_dump($state, $level, $node);
+}
+
# only private functions below.
sub emit_thread_html {
my ($res, $ctx, $foot, $srch) = @_;
my $mid = $ctx->{mid};
- my $flat = $ctx->{flat};
- my $msgs = load_results($srch->get_thread($mid, { asc => $flat }));
- my $nr = scalar @$msgs;
+ my $sres = $srch->get_thread($mid, { asc => 1 });
+ my $msgs = load_results($sres);
+ my $nr = $sres->{total};
return missing_thread($res, $ctx) if $nr == 0;
- my $seen = {};
+ my $skel = '';
my $state = {
- res => $res,
- ctx => $ctx,
- seen => $seen,
- root_anchor => anchor_for($mid),
anchor_idx => 0,
+ ctx => $ctx,
cur_level => 0,
+ dst => \$skel,
+ mapping => {}, # mid -> reply count
+ prev_attr => '',
+ prev_level => 0,
+ res => $res,
+ root_anchor => anchor_for($mid),
+ seen => {},
+ srch => $ctx->{srch},
+ upfx => '../../',
};
- require PublicInbox::Git;
- $ctx->{git} ||= PublicInbox::Git->new($ctx->{git_dir});
- if ($flat) {
- pre_anchor_entry($seen, $_) for (@$msgs);
- __thread_entry($state, $_, 0) for (@$msgs);
- } else {
- my $th = thread_results($msgs);
- thread_entry($state, $_, 0) for $th->rootset;
- if (my $max = $state->{cur_level}) {
- $state->{fh}->write(
- ('</ul></li>' x ($max - 1)) . '</ul>');
- }
- }
+ walk_thread(thread_results($msgs), $state, *pre_thread);
+
+ thread_entry($state, $_, 0) for @$msgs;
# there could be a race due to a message being deleted in git
# but still being in the Xapian index:
my $fh = delete $state->{fh} or return missing_thread($res, $ctx);
- my $final_anchor = $state->{anchor_idx};
- my $next = "<a\nid=s$final_anchor>";
- $next .= $final_anchor == 1 ? 'only message in' : 'end of';
- $next .= " thread</a>, back to <a\nhref=\"../../\">index</a>";
- $next .= "\ndownload thread: ";
+ my $next = @$msgs == 1 ? 'only message in thread' : 'end of thread';
+ $next .= ", back to <a\nhref=\"../../\">index</a>";
+ $next .= "\n<a\nid=t>$nr+ messages in thread:</a> (download: ";
$next .= "<a\nhref=\"../t.mbox.gz\">mbox.gz</a>";
- $next .= " / follow: <a\nhref=\"../t.atom\">Atom feed</a>";
+ $next .= " / follow: <a\nhref=\"../t.atom\">Atom feed</a>)\n";
+ $next .= $skel;
$fh->write('<hr /><pre>' . $next . "\n\n".
$foot . '</pre></body></html>');
$fh->close;
}
-sub index_walk {
- my ($fh, $upfx, $p) = @_;
- my $s = add_text_body($upfx, $p);
-
- return if $s eq '';
-
- $s .= "\n"; # ensure there's a trailing newline
-
- $fh->write($s);
-}
-
sub multipart_text_as_html {
my ($mime, $upfx) = @_;
my $rv = "";
# scan through all parts, looking for displayable text
msg_iter($mime, sub {
my ($p) = @_;
- $p = add_text_body($upfx, $p);
- $rv .= $p;
- $rv .= "\n" if $p ne '';
+ $rv .= add_text_body($upfx, $p);
});
$rv;
}
my $rv = $l->linkify_1(join('', @$quot));
@$quot = ();
- # we use a <div> here to allow users to specify their own
+ # we use a <span> here to allow users to specify their own
# color for quoted text
$rv = $l->linkify_2(ascii_html($rv));
$$s .= qq(<span\nclass="q">) . $rv . '</span>'
my @ret = qq($nl<a\nhref="$upfx$idx-$sfn">[-- Attachment #$idx: );
my $ts = "Type: $ct, Size: $size bytes";
push(@ret, ($desc eq '') ? "$ts --]" : "$desc --]\n[-- $ts --]");
- join('', @ret, '</a>');
+ join('', @ret, "</a>\n");
}
sub add_text_body {
$s = '';
if (defined($fn) || $depth > 0) {
$s .= attach_link($upfx, $ct, $p, $fn);
- $s .= "\n\n";
+ $s .= "\n";
}
my @quot;
my $l = PublicInbox::Linkify->new;
}
}
- flush_quote(\$s, $l, \@quot) if @quot;
+ my $end = "\n";
+ if (@quot) {
+ $end = '';
+ flush_quote(\$s, $l, \@quot);
+ }
$s =~ s/[ \t]+$//sgm; # kill per-line trailing whitespace
$s =~ s/\A\n+//s; # kill leading blank lines
- $s =~ s/\s+\z//s; # kill all trailing spaces (final "\n" added if ne '')
- $s;
+ $s =~ s/\s+\z//s; # kill all trailing spaces
+ $s .= $end;
}
-sub headers_to_html_header {
+sub _msg_html_prepare {
my ($hdr, $ctx) = @_;
my $srch = $ctx->{srch} if $ctx;
my $atom = '';
- my $rv = '';
- my $upfx = '';
+ my $rv = "<pre\nid=b>"; # anchor for body start
if ($srch) {
- $atom = qq{<link\nrel=alternate\ntitle="Atom feed"\n} .
- qq!href="${upfx}t.atom"\ntype="application/atom+xml"/>!;
+ $ctx->{-upfx} = '../';
}
-
my @title;
my $mid = $hdr->header_raw('Message-ID');
$mid = PublicInbox::Hval->new_msgid($mid);
$v = PublicInbox::Hval->new($v);
if ($h eq 'From') {
- my $n = PublicInbox::Address::from_name($v->raw);
- $title[1] = ascii_html($n);
+ my @n = PublicInbox::Address::names($v->raw);
+ $title[1] = ascii_html(join(', ', @n));
} elsif ($h eq 'Subject') {
$title[0] = $v->as_html;
if ($srch) {
next;
}
}
- $rv .= "$h: " . $v->as_html . "\n";
+ $v = $v->as_html;
+ $v =~ s/(\@[^,]+,) /$1\n\t/g if ($h eq 'Cc' || $h eq 'To');
+ $rv .= "$h: $v\n";
}
+ $title[0] ||= '(no subject)';
+ $ctx->{-title_html} = join(' - ', @title);
$rv .= 'Message-ID: <' . $mid->as_html . '> ';
- $rv .= "(<a\nhref=\"${upfx}raw\">raw</a>)\n";
+ $rv .= "(<a\nhref=\"raw\">raw</a>)\n";
$rv .= _parent_headers($hdr, $srch);
$rv .= "\n";
-
- ("<html><head><title>". join(' - ', @title) . "</title>$atom".
- PublicInbox::Hval::STYLE .
- "</head><body><pre\nid=b>" . # anchor for body start
- $rv);
}
sub thread_skel {
cur => $mid,
prev_attr => '',
prev_level => 0,
+ upfx => "$tpfx../",
+ dst => $dst,
};
- for (thread_results(load_results($sres))->rootset) {
- skel_dump($dst, $state, $tpfx, $_, 0);
- }
+ walk_thread(thread_results(load_results($sres)), $state, *skel_dump);
$ctx->{next_msg} = $state->{next_msg};
$ctx->{parent_msg} = $parent;
}
}
if (@refs) {
- $rv .= 'References: '. join(' ', @refs) . "\n";
+ $rv .= 'References: '. join("\n\t", @refs) . "\n";
}
}
$rv;
}
+sub squote_maybe ($) {
+ my ($val) = @_;
+ if ($val =~ m{([^\w@\./,\%\+\-])}) {
+ $val =~ s/(['!])/'\\$1'/g; # '!' for csh
+ return "'$val'";
+ }
+ $val;
+}
+
sub mailto_arg_link {
my ($hdr) = @_;
my %cc; # everyone else
my $subj = $hdr->header('Subject') || '';
$subj = "Re: $subj" unless $subj =~ /\bRe:/i;
my $mid = $hdr->header_raw('Message-ID');
- push @arg, "--in-reply-to='" . ascii_html($mid) . "'";
+ push @arg, '--in-reply-to='.ascii_html(squote_maybe(mid_clean($mid)));
my $irt = uri_escape_utf8($mid);
delete $cc{$to};
push @arg, '--to=' . ascii_html($to);
my $href = "mailto:$to?In-Reply-To=$irt&Cc=${cc}&Subject=$subj";
$href =~ s/%20/+/g;
- (\@arg, $href);
+ (\@arg, ascii_html($href));
}
sub html_footer {
if ($p) {
$p = PublicInbox::Hval->new_msgid($p);
$p = $p->as_href;
- $irt = "<a\nhref=\"$upfx$p/\"rel=prev>parent</a> ";
+ $irt = "<a\nhref=\"$upfx$p/\"\nrel=prev>parent</a> ";
} else {
$irt = ' ' x length('parent ');
}
sub anchor_for {
my ($msgid) = @_;
- my $id = $msgid;
- if ($id !~ /\A[a-f0-9]{40}\z/) {
- $id = id_compress(mid_clean($id), 1);
- }
- 'm' . $id;
+ 'm' . id_compress($msgid, 1);
}
sub thread_html_head {
"</head><body>");
}
-sub pre_anchor_entry {
- my ($seen, $mime) = @_;
- my $id = anchor_for(mid_mime($mime));
- $seen->{$id} = "#$id"; # save the anchor for children, later
-}
-
sub ghost_parent {
my ($upfx, $mid) = @_;
# 'subject dummy' is used internally by Mail::Thread
qq{[parent not found: <<a\nhref="$upfx$href/">$html</a>>]};
}
-sub thread_adj_level {
- my ($state, $level) = @_;
-
- my $max = $state->{cur_level};
- if ($level <= 0) {
- return '' if $max == 0; # flat output
-
- # reset existing lists
- my $x = $max > 1 ? ('</ul></li>' x ($max - 1)) : '';
- $state->{fh}->write($x . '</ul>');
- $state->{cur_level} = 0;
- return '';
- }
- if ($level == $max) { # continue existing list
- $state->{fh}->write('<li>');
- } elsif ($level < $max) {
- my $x = $max > 1 ? ('</ul></li>' x ($max - $level)) : '';
- $state->{fh}->write($x .= '<li>');
- $state->{cur_level} = $level;
- } else { # ($level > $max) # start a new level
- $state->{cur_level} = $level;
- $state->{fh}->write(($max ? '<li>' : '') . '<ul><li>');
- }
- '</li>';
-}
-
-sub ghost_flush {
- my ($state, $upfx, $mid, $level) = @_;
- my $end = '<pre>'. ghost_parent($upfx, $mid) . '</pre>';
- $state->{fh}->write($end .= thread_adj_level($state, $level));
-}
-
-sub __thread_entry {
+sub thread_entry {
my ($state, $mime, $level) = @_;
# lazy load the full message from mini_mime:
$mime = eval {
- my $path = mid2path(mid_clean(mid_mime($mime)));
- Email::MIME->new($state->{ctx}->{git}->cat_file('HEAD:'.$path));
+ my $mid = mid_clean(mid_mime($mime));
+ $state->{ctx}->{-inbox}->msg_by_mid($mid);
} or return;
+ $mime = Email::MIME->new($mime);
thread_html_head($mime, $state) if $state->{anchor_idx} == 0;
- if (my $ghost = delete $state->{ghost}) {
- # n.b. ghost messages may only be parents, not children
- foreach my $g (@$ghost) {
- ghost_flush($state, '../../', @$g);
- }
- }
- my $end = thread_adj_level($state, $level);
index_entry($mime, $level, $state);
- $state->{fh}->write($end) if $end;
-
1;
}
INDENT x ($level - 1);
}
-sub __ghost_prepare {
- my ($state, $node, $level) = @_;
- my $ghost = $state->{ghost} ||= [];
- push @$ghost, [ $node->messageid, $level ];
-}
-
-sub thread_entry {
- my ($state, $node, $level) = @_;
- return unless $node;
- if (my $mime = $node->message) {
- unless (__thread_entry($state, $mime, $level)) {
- __ghost_prepare($state, $node, $level);
- }
- } else {
- __ghost_prepare($state, $node, $level);
- }
-
- thread_entry($state, $node->child, $level + 1);
- thread_entry($state, $node->next, $level);
-}
-
sub load_results {
my ($sres) = @_;
sub fmt_ts { POSIX::strftime('%Y-%m-%d %k:%M', gmtime($_[0])) }
sub _skel_header {
- my ($dst, $state, $upfx, $hdr, $level) = @_;
+ my ($state, $hdr, $level) = @_;
+ my $dst = $state->{dst};
my $cur = $state->{cur};
my $mid = mid_clean($hdr->header_raw('Message-ID'));
my $f = ascii_html($hdr->header('X-PI-From'));
$s = $s->as_html;
}
my $m = PublicInbox::Hval->new_msgid($mid);
- $m = $upfx . '../' . $m->as_href . '/';
- $$dst .= "$pfx<a\nhref=\"$m\">";
+ my $id = '';
+ if ($state->{mapping}) {
+ $id = id_compress($mid, 1);
+ $m = '#m'.$id;
+ $id = "\nid=r".$id;
+ } else {
+ $m = $state->{upfx}.$m->as_href.'/';
+ }
+ $$dst .= "$pfx<a\nhref=\"$m\"$id>";
$$dst .= defined($s) ? "$s</a> $f\n" : "$f</a>\n";
}
sub skel_dump {
- my ($dst, $state, $upfx, $node, $level) = @_;
- return unless $node;
+ my ($state, $level, $node) = @_;
if (my $mime = $node->message) {
- my $hdr = $mime->header_obj;
- my $mid = mid_clean($hdr->header_raw('Message-ID'));
- _skel_header($dst, $state, $upfx, $hdr, $level);
+ _skel_header($state, $mime->header_obj, $level);
} else {
my $mid = $node->messageid;
+ my $dst = $state->{dst};
if ($mid eq 'subject dummy') {
$$dst .= "\t[no common parent]\n";
+ return;
+ }
+ if ($state->{pct}) { # search result
+ $$dst .= ' [irrelevant] ';
} else {
$$dst .= ' [not found] ';
- $$dst .= indent_for($level) . th_pfx($level);
- $mid = PublicInbox::Hval->new_msgid($mid);
- my $href = "$upfx../" . $mid->as_href . '/';
- my $html = $mid->as_html;
- $$dst .= qq{<<a\nhref="$href">$html</a>>\n};
}
+ $$dst .= indent_for($level) . th_pfx($level);
+ my $upfx = $state->{upfx};
+ my $id = '';
+ if ($state->{mapping}) { # thread index view
+ $id = "\nid=".anchor_for($mid);
+ }
+ $mid = PublicInbox::Hval->new_msgid($mid);
+ my $href = $upfx . $mid->as_href . '/';
+ my $html = $mid->as_html;
+ $$dst .= qq{<<a\nhref="$href"$id>$html</a>>\n};
}
- skel_dump($dst, $state, $upfx, $node->child, $level+1);
- skel_dump($dst, $state, $upfx, $node->next, $level);
}
sub sort_ts {
} @_;
}
+sub _tryload_ghost ($$) {
+ my ($srch, $mid) = @_;
+ my $smsg = $srch->lookup_mail($mid) or return;
+ $smsg->mini_mime;
+}
+
# accumulate recent topics if search is supported
# returns 1 if done, undef if not
sub add_topic {
- my ($state, $node, $level) = @_;
- return unless $node;
- my $child_adjust = 1;
-
- if (my $x = $node->message) {
+ my ($state, $level, $node) = @_;
+ my $srch = $state->{srch};
+ my $mid = $node->messageid;
+ my $x = $node->message || _tryload_ghost($srch, $mid);
+ my ($subj, $ts);
+ if ($x) {
$x = $x->header_obj;
- my $subj;
-
$subj = $x->header('Subject');
- $subj = $state->{srch}->subject_normalized($subj);
-
- if (++$state->{subjs}->{$subj} == 1) {
- push @{$state->{order}}, [ $level, $subj ];
- }
-
- my $mid = mid_clean($x->header_raw('Message-ID'));
-
- my $ts = $x->header('X-PI-TS');
- my $exist = $state->{latest}->{$subj};
- if (!$exist || $exist->[1] < $ts) {
- $state->{latest}->{$subj} = [ $mid, $ts ];
- }
- } else {
- # ghost message, do not bump level
- $child_adjust = 0;
+ $subj = $srch->subject_normalized($subj);
+ $ts = $x->header('X-PI-TS');
+ } else { # ghost message, do not bump level
+ $ts = -666;
+ $subj = "<$mid>";
+ }
+ if (++$state->{subjs}->{$subj} == 1) {
+ push @{$state->{order}}, [ $level, $subj ];
+ }
+ my $exist = $state->{latest}->{$subj};
+ if (!$exist || $exist->[1] < $ts) {
+ $state->{latest}->{$subj} = [ $mid, $ts ];
}
-
- add_topic($state, $node->child, $level + $child_adjust);
- add_topic($state, $node->next, $level);
}
sub emit_topics {
my $n = delete $subjs->{$subj};
my ($mid, $ts) = @{delete $latest->{$subj}};
$mid = PublicInbox::Hval->new_msgid($mid)->as_href;
- $subj = PublicInbox::Hval->new($subj)->as_html;
$pfx = indent_for($level);
my $nl = $level == $prev ? "\n" : '';
if ($nl && $cur) {
}
$cur ||= [ $ts, '' ];
$cur->[0] = $ts if $ts > $cur->[0];
- $cur->[1] .= $nl . $pfx . th_pfx($level) .
- "<a\nhref=\"$mid/t/#u\"><b>" .
- $subj . "</b></a>\n";
+ $cur->[1] .= $nl . $pfx . th_pfx($level);
+ if ($ts == -666) { # ghost
+ $cur->[1] .= ghost_parent('', $mid) . "\n";
+ next; # child will have mbox / atom link
+ }
+ $subj = PublicInbox::Hval->new($subj)->as_html;
+ $cur->[1] .= "<a\nhref=\"$mid/t/#u\"><b>$subj</b></a>\n";
$ts = fmt_ts($ts);
my $attr = " $ts UTC";
while (scalar @{$state->{order}} < $max) {
my $sres = $state->{srch}->query('', \%opts);
my $nr = scalar @{$sres->{msgs}} or last;
-
- for (thread_results(load_results($sres))->rootset) {
- add_topic($state, $_, 0);
- }
+ $sres = load_results($sres);
+ walk_thread(thread_results($sres), $state, *add_topic);
$opts{offset} += $nr;
}