package PublicInbox::View;
use strict;
use warnings;
+use bytes (); # only for bytes::length
use PublicInbox::MsgTime qw(msg_datestamp);
use PublicInbox::Hval qw/ascii_html obfuscate_addrs/;
use PublicInbox::Linkify;
use PublicInbox::Address;
use PublicInbox::WwwStream;
use PublicInbox::Reply;
+use PublicInbox::ViewDiff qw(flush_diff);
require POSIX;
use Time::Local qw(timegm);
my ($ctx, $mime, $more, $smsg) = @_;
my $hdr = $mime->header_obj;
my $ibx = $ctx->{-inbox};
- my $obfs_ibx = $ctx->{-obfs_ibx} = $ibx->{obfuscate} ? $ibx : undef;
+ $ctx->{-obfs_ibx} = $ibx->{obfuscate} ? $ibx : undef;
my $tip = _msg_html_prepare($hdr, $ctx, $more, 0);
my $end = 2;
PublicInbox::WwwStream->response($ctx, 200, sub {
if ($nr == 1) {
# $more cannot be true w/o $smsg being defined:
my $upfx = $more ? '../'.mid_escape($smsg->mid).'/' : '';
- $tip . multipart_text_as_html($mime, $upfx, $obfs_ibx) .
+ $tip . multipart_text_as_html($mime, $upfx, $ctx) .
'</pre><hr>'
} elsif ($more && @$more) {
++$end;
my $str = eval {
my ($id, $prev, $smsg) = @$more;
my $mid = $ctx->{mid};
- $smsg = $ctx->{-inbox}->smsg_mime($smsg);
+ my $ibx = $ctx->{-inbox};
+ $smsg = $ibx->smsg_mime($smsg);
my $next = $ctx->{srch}->next_by_mid($mid, \$id, \$prev);
@$more = $next ? ($id, $prev, $next) : ();
if ($smsg) {
my $mime = $smsg->{mime};
my $upfx = '../' . mid_escape($smsg->mid) . '/';
_msg_html_prepare($mime->header_obj, $ctx, $more, $nr) .
- multipart_text_as_html($mime, $upfx,
- $ctx->{-obfs_ibx}) .
+ multipart_text_as_html($mime, $upfx, $ctx) .
'</pre><hr>'
} else {
'';
my $irt;
my $obfs_ibx = $ctx->{-obfs_ibx};
+ $subj = '(no subject)' if $subj eq '';
my $rv = "<a\nhref=#e$id\nid=m$id>*</a> ";
$subj = '<b>'.ascii_html($subj).'</b>';
obfuscate_addrs($obfs_ibx, $subj) if $obfs_ibx;
$rv .= $subj . "\n";
$rv .= _th_index_lite($mid_raw, \$irt, $id, $ctx);
my @tocc;
- my $mime = delete $smsg->{mime}; # critical to memory use
+ my $ds = $smsg->ds; # for v1 non-Xapian/SQLite users
+ # deleting {mime} is critical to memory use,
+ # the rest of the fields saves about 400K as we iterate across 1K msgs
+ my ($mime) = delete @$smsg{qw(mime ds ts blob subject)};
+
my $hdr = $mime->header_obj;
my $from = _hdr_names_html($hdr, 'From');
obfuscate_addrs($obfs_ibx, $from) if $obfs_ibx;
- $rv .= "From: $from @ ".fmt_ts($smsg->ds)." UTC";
+ $rv .= "From: $from @ ".fmt_ts($ds)." UTC";
my $upfx = $ctx->{-upfx};
my $mhref = $upfx . mid_escape($mid_raw) . '/';
$rv .= qq{ (<a\nhref="$mhref">permalink</a> / };
$rv .= "\n";
# scan through all parts, looking for displayable text
- msg_iter($mime, sub { $rv .= add_text_body($mhref, $obfs_ibx, $_[0]) });
+ my $ibx = $ctx->{-inbox};
+ msg_iter($mime, sub { $rv .= add_text_body($mhref, $ctx, $_[0]) });
# add the footer
$rv .= "\n<a\nhref=#$id_m\nid=e$id>^</a> ".
my $nr_s = 0;
my $siblings;
if (my $smsg = $node->{smsg}) {
- ($$irt) = (($smsg->{references} || '') =~ m/<([^>]+)>\z/);
+ # delete saves about 200KB on a 1K message thread
+ if (my $refs = delete $smsg->{references}) {
+ ($$irt) = ($refs =~ m/<([^>]+)>\z/);
+ }
}
my $irt_map = $mapping->{$$irt} if defined $$irt;
if (defined $irt_map) {
sub stream_thread ($$) {
my ($rootset, $ctx) = @_;
- my $inbox = $ctx->{-inbox};
+ my $ibx = $ctx->{-inbox};
my @q = map { (0, $_) } @$rootset;
my $level;
my $smsg;
my $node = shift @q or next;
my $cl = $level + 1;
unshift @q, map { ($cl, $_) } @{$node->{children}};
- $smsg = $inbox->smsg_mime($node->{smsg}) and last;
+ $smsg = $ibx->smsg_mime($node->{smsg}) and last;
}
return missing_thread($ctx) unless $smsg;
- $ctx->{-obfs_ibx} = $inbox->{obfuscate} ? $inbox : undef;
+ $ctx->{-obfs_ibx} = $ibx->{obfuscate} ? $ibx : undef;
$ctx->{-title_html} = ascii_html($smsg->subject);
$ctx->{-html_tip} = thread_index_entry($ctx, $level, $smsg);
$smsg = undef;
my $node = shift @q or next;
my $cl = $level + 1;
unshift @q, map { ($cl, $_) } @{$node->{children}};
- if ($smsg = $inbox->smsg_mime($node->{smsg})) {
+ if ($smsg = $ibx->smsg_mime($node->{smsg})) {
return thread_index_entry($ctx, $level, $smsg);
} else {
return ghost_index_entry($ctx, $level, $node);
$ctx->{prev_level} = 0;
$ctx->{root_anchor} = anchor_for($mid);
$ctx->{mapping} = {};
- $ctx->{s_nr} = "$nr+ messages in thread";
+ $ctx->{s_nr} = ($nr > 1 ? "$nr+ messages" : 'only message')
+ .' in thread';
my $rootset = thread_results($ctx, $msgs);
# reduce hash lookups in pre_thread->skel_dump
- my $inbox = $ctx->{-inbox};
- $ctx->{-obfs_ibx} = $inbox->{obfuscate} ? $inbox : undef;
+ my $ibx = $ctx->{-inbox};
+ $ctx->{-obfs_ibx} = $ibx->{obfuscate} ? $ibx : undef;
walk_thread($rootset, $ctx, *pre_thread);
$skel .= '</pre>';
# flat display: lazy load the full message from smsg
my $smsg;
while (my $m = shift @$msgs) {
- $smsg = $inbox->smsg_mime($m) and last;
+ $smsg = $ibx->smsg_mime($m) and last;
}
return missing_thread($ctx) unless $smsg;
$ctx->{-title_html} = ascii_html($smsg->subject);
return unless $msgs;
$smsg = undef;
while (my $m = shift @$msgs) {
- $smsg = $inbox->smsg_mime($m) and last;
+ $smsg = $ibx->smsg_mime($m) and last;
}
return index_entry($smsg, $ctx, scalar @$msgs) if $smsg;
$msgs = undef;
}
sub multipart_text_as_html {
- my ($mime, $upfx, $obfs_ibx) = @_;
+ my ($mime, $upfx, $ctx) = @_;
my $rv = "";
# scan through all parts, looking for displayable text
- msg_iter($mime, sub { $rv .= add_text_body($upfx, $obfs_ibx, $_[0]) });
+ msg_iter($mime, sub { $rv .= add_text_body($upfx, $ctx, $_[0]) });
$rv;
}
# show everything in the full version with anchor from
# short version (see above)
- my $rv = $l->linkify_1(join('', @$quot));
- @$quot = ();
+ my $rv = $l->linkify_1($$quot);
# we use a <span> here to allow users to specify their own
# color for quoted text
$rv = $l->linkify_2(ascii_html($rv));
+ $$quot = undef;
$$s .= qq(<span\nclass="q">) . $rv . '</span>'
}
}
sub add_text_body {
- my ($upfx, $obfs_ibx, $p) = @_;
+ my ($upfx, $ctx, $p) = @_;
+ my $ibx = $ctx->{-inbox};
+ my $obfs_ibx = $ibx->{obfuscate} ? $ibx : undef;
# $p - from msg_iter: [ Email::MIME, depth, @idx ]
- my ($part, $depth) = @$p; # attachment @idx is unused
+ my ($part, $depth, @idx) = @$p;
my $ct = $part->content_type || 'text/plain';
my $fn = $part->filename;
my ($s, $err) = msg_part_text($part, $ct);
return attach_link($upfx, $ct, $p, $fn) unless defined $s;
- my @lines = split(/^/m, $s);
+ # makes no difference to browsers, and don't screw up filename
+ # link generation in diffs with the extra '%0D'
+ $s =~ s/\r\n/\n/sg;
+
+ # always support diff-highlighting, but we can't linkify hunk
+ # headers for solver unless some coderepo are configured:
+ my $diff;
+ if ($s =~ /^(?:diff|---|\+{3}) /ms) {
+ # diffstat anchors do not link across attachments or messages:
+ $idx[0] = $upfx . $idx[0] if $upfx ne '';
+ $ctx->{-apfx} = join('/', @idx);
+ $ctx->{-anchors} = {}; # attr => filename
+ $ctx->{-diff} = $diff = [];
+ delete $ctx->{-long_path};
+ my $spfx;
+ if ($ibx->{-repo_objs}) {
+ if (index($upfx, '//') >= 0) { # absolute URL (Atom feeds)
+ $spfx = $upfx;
+ $spfx =~ s!/([^/]*)/\z!/!;
+ } else {
+ my $n_slash = $upfx =~ tr!/!/!;
+ if ($n_slash == 0) {
+ $spfx = '../';
+ } elsif ($n_slash == 1) {
+ $spfx = '';
+ } else { # nslash == 2
+ $spfx = '../../';
+ }
+ }
+ }
+ $ctx->{-spfx} = $spfx;
+ };
+
+ # some editors don't put trailing newlines at the end:
+ $s .= "\n" unless $s =~ /\n\z/s;
+
+ # split off quoted and unquoted blocks:
+ my @sections = split(/((?:^>[^\n]*\n)+)/sm, $s);
$s = '';
if (defined($fn) || $depth > 0 || $err) {
# badly-encoded message with $err? tell the world about it!
$s .= attach_link($upfx, $ct, $p, $fn, $err);
$s .= "\n";
}
- my @quot;
my $l = PublicInbox::Linkify->new;
- foreach my $cur (@lines) {
- if ($cur !~ /^>/) {
- # show the previously buffered quote inline
- flush_quote(\$s, $l, \@quot) if @quot;
-
- # regular line, OK
+ foreach my $cur (@sections) {
+ if ($cur =~ /\A>/) {
+ flush_quote(\$s, $l, \$cur);
+ } elsif ($diff) {
+ @$diff = split(/^/m, $cur);
+ $cur = undef;
+ flush_diff(\$s, $ctx, $l);
+ } else {
+ # regular lines, OK
$l->linkify_1($cur);
$s .= $l->linkify_2(ascii_html($cur));
- } else {
- push @quot, $cur;
+ $cur = undef;
}
}
- if (@quot) { # ugh, top posted
- flush_quote(\$s, $l, \@quot);
- obfuscate_addrs($obfs_ibx, $s) if $obfs_ibx;
- $s;
- } else {
- obfuscate_addrs($obfs_ibx, $s) if $obfs_ibx;
- if ($s =~ /\n\z/s) { # common, last line ends with a newline
- $s;
- } else { # some editors don't do newlines...
- $s .= "\n";
- }
- }
+ obfuscate_addrs($obfs_ibx, $s) if $obfs_ibx;
+ $s;
}
sub _msg_html_prepare {
my $subj = $hdr->header('Subject');
defined $subj or $subj = '';
+ $subj = '(no subject)' if $subj eq '';
$ctx->{prev_subj} = [ split(/ /, $srch->subject_normalized($subj)) ];
$ctx->{cur} = $mid;
$ctx->{prev_attr} = '';
PublicInbox::ExtMsg::ext_msg($ctx);
}
-sub _msg_date {
- my ($hdr) = @_;
- fmt_ts(msg_datestamp($hdr));
-}
-
sub fmt_ts { POSIX::strftime('%Y-%m-%d %k:%M', gmtime($_[0])) }
sub dedupe_subject {
if ($x) {
$subj = $x->subject;
$subj = $srch->subject_normalized($subj);
+ $subj = '(no subject)' if $subj eq '';
$ds = $x->ds;
if ($level == 0) {
$topic = [ $ds, 1, { $subj => $mid }, $subj ];
my $mbox = qq(<a\nhref="$href/t.mbox.gz">mbox.gz</a>);
my $atom = qq(<a\nhref="$href/t.atom">Atom</a>);
- my $s = "<a\nhref=\"$href/T/$anchor\"><b>$top</b></a>\n" .
+ my $s = "<a\nhref=\"$href/T/$anchor\">$top</a>\n" .
" $ds UTC $n - $mbox / $atom\n";
for (my $i = 0; $i < scalar(@ex); $i += 2) {
my $level = $ex[$i];