# Copyright (C) 2014-2015 all contributors # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt) # # Used for displaying the HTML web interface. # See Documentation/design_www.txt for this. package PublicInbox::View; use strict; use warnings; 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 mid_mime/; use PublicInbox::MsgIter; use PublicInbox::Address; use PublicInbox::WwwStream; require POSIX; use constant INDENT => ' '; use constant TCHILD => '` '; sub th_pfx ($) { $_[0] == 0 ? '' : TCHILD }; # public functions: (unstable) sub msg_html { my ($ctx, $mime, $footer) = @_; my $hdr = $mime->header_obj; my $tip = _msg_html_prepare($hdr, $ctx); PublicInbox::WwwStream->new($ctx, sub { my ($nr, undef) = @_; if ($nr == 1) { $tip . multipart_text_as_html($mime, '') . '
' . html_footer($hdr, 1, $ctx) . '' . 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 ($arg, $link) = mailto_arg_link($hdr); push @$arg, '/path/to/YOUR_REPLY'; "
". "You may reply publically to this message 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: mbox\n\n" . "* Reply to all the recipients using the --to, --cc,\n" . " and --in-reply-to switches of git-send-email(1):\n\n" . " git send-email \\\n " . join(" \\\n ", @$arg ). "\n\n" . qq( $se_url\n\n) . "* If your mail client supports setting the In-Reply-To" . " header\n via mailto: links, try the " . qq(mailto: link\n) . ''; } sub in_reply_to { my ($hdr) = @_; my $irt = $hdr->header_raw('In-Reply-To'); return mid_clean($irt) if (defined $irt); my $refs = $hdr->header_raw('References'); if ($refs && $refs =~ /<([^>]+)>\s*\z/s) { return $1; } undef; } # this is already inside a
sub index_entry { my ($mime, $level, $state) = @_; my $midx = $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 $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 = "$subj"; $subj = "$subj" if $root_anchor eq $id; my $ts = _msg_date($hdr); my $rv = ""; $rv .= "$subj\n"; my $txt = "${path}$href/raw"; my $fh = $state->{fh}; $fh->write($rv .= "- $from @ $ts UTC (raw)\n\n"); my $mhref = "${path}$href/"; # 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"); if (defined $irt) { unless (defined $parent_anchor) { my $v = PublicInbox::Hval->new_msgid($irt, 1); $v = $v->as_href; $parent_anchor = "${path}$v/"; } $rv .= " parent"; } 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 = "$flat"; $end = "\n"; # for lynx } else { $threaded = "$threaded"; } $rv .= " [$threaded"; $rv .= "|$flat]$end"; } $fh->write($rv .= ''); } sub thread_html { my ($ctx, $foot, $srch) = @_; # $_[0] in sub is the Plack callback sub { emit_thread_html($_[0], $ctx, $foot, $srch) } } # 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; return missing_thread($res, $ctx) if $nr == 0; my $seen = {}; my $state = { res => $res, ctx => $ctx, seen => $seen, root_anchor => anchor_for($mid), anchor_idx => 0, cur_level => 0, }; 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 @q = map { (0, $_) } thread_results($msgs)->rootset; while (@q) { my $level = shift @q; my $node = shift @q or next; thread_entry($state, $level, $node); unshift @q, $level+1, $node->child, $level, $node->next; } if (my $max = $state->{cur_level}) { $state->{fh}->write( ('' x ($max - 1)) . ''); } } # 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 = ""; $next .= $final_anchor == 1 ? 'only message in' : 'end of'; $next .= " thread, back to index"; $next .= "\ndownload thread: "; $next .= "mbox.gz"; $next .= " / follow: Atom feed"; $fh->write('' . $next . "\n\n". $foot . '