]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/View.pm
handle "multipart/mixed" messages which are not multipart
[public-inbox.git] / lib / PublicInbox / View.pm
index af287b96852be66b708bd0eff86d6064ee1773c6..bb49c035d976db7a65b83924a594e4b16c407b99 100644 (file)
@@ -17,6 +17,7 @@ use PublicInbox::Reply;
 require POSIX;
 use Time::Local qw(timegm);
 
+use constant COLS => 72;
 use constant INDENT => '  ';
 use constant TCHILD => '` ';
 sub th_pfx ($) { $_[0] == 0 ? '' : TCHILD };
@@ -118,6 +119,9 @@ sub msg_reply {
 
        my ($arg, $link, $reply_to_all) =
                        PublicInbox::Reply::mailto_arg_link($ibx, $hdr);
+       if (ref($arg) eq 'SCALAR') {
+               return '<pre id=R>'.ascii_html($$arg).'</pre>';
+       }
 
        # mailto: link only works if address obfuscation is disabled
        if ($link) {
@@ -164,6 +168,24 @@ sub in_reply_to {
        $refs->[-1];
 }
 
+sub fold_addresses ($) {
+       return $_[0] if length($_[0]) <= COLS;
+       # try to fold on commas after non-word chars before $lim chars,
+       # Try to get the "," preceeded by ">" or ")", but avoid folding
+       # on the comma where somebody uses "Lastname, Firstname".
+       # We also try to keep the last and penultimate addresses in
+       # the list on the same line if possible, hence the extra \z
+       # Fall back to folding on spaces at $lim + 1 chars
+       my $lim = COLS - 8; # 8 = "\t" display width
+       my $too_long = $lim + 1;
+       $_[0] =~ s/\s*\z//s; # Email::Simple doesn't strip trailing spaces
+       $_[0] = join("\n\t",
+               ($_[0] =~ /(.{0,$lim}\W(?:,|\z)|
+                               .{1,$lim}(?:,|\z)|
+                               .{1,$lim}|
+                               .{$too_long,}?)(?:\s|\z)/xgo));
+}
+
 sub _hdr_names_html ($$) {
        my ($hdr, $field) = @_;
        my $val = $hdr->header($field) or return '';
@@ -198,13 +220,6 @@ sub index_entry {
        my @tocc;
        my $mime = $smsg->{mime};
        my $hdr = $mime->header_obj;
-       foreach my $f (qw(To Cc)) {
-               my $dst = _hdr_names_html($hdr, $f);
-               if ($dst ne '') {
-                       obfuscate_addrs($obfs_ibx, $dst) if $obfs_ibx;
-                       push @tocc, "$f: $dst";
-               }
-       }
        my $from = _hdr_names_html($hdr, 'From');
        obfuscate_addrs($obfs_ibx, $from) if $obfs_ibx;
        $rv .= "From: $from @ ".fmt_ts($smsg->ds)." UTC";
@@ -212,7 +227,24 @@ sub index_entry {
        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 $to = fold_addresses(_hdr_names_html($hdr, 'To'));
+       my $cc = fold_addresses(_hdr_names_html($hdr, 'Cc'));
+       my ($tlen, $clen) = (length($to), length($cc));
+       my $to_cc = '';
+       if (($tlen + $clen) > COLS) {
+               $to_cc .= '  To: '.$to."\n" if $tlen;
+               $to_cc .= '  Cc: '.$cc."\n" if $clen;
+       } else {
+               if ($tlen) {
+                       $to_cc .= '  To: '.$to;
+                       $to_cc .= '; <b>+Cc:</b> '.$cc if $clen;
+               } else {
+                       $to_cc .= '  Cc: '.$cc if $clen;
+               }
+               $to_cc .= "\n";
+       }
+       obfuscate_addrs($obfs_ibx, $to_cc) if $obfs_ibx;
+       $rv .= $to_cc;
 
        my $mapping = $ctx->{mapping};
        if (!$mapping && (defined($irt) || defined($irt = in_reply_to($hdr)))) {
@@ -336,7 +368,7 @@ sub walk_thread {
        while (@q) {
                my ($level, $node, $i) = splice(@q, 0, 3);
                defined $node or next;
-               $cb->($ctx, $level, $node, $i);
+               $cb->($ctx, $level, $node, $i) or return;
                ++$level;
                $i = 0;
                unshift @q, map { ($level, $_, $i++) } @{$node->{children}};
@@ -511,33 +543,14 @@ sub add_text_body {
        my ($part, $depth) = @$p; # attachment @idx is unused
        my $ct = $part->content_type || 'text/plain';
        my $fn = $part->filename;
+       my ($s, $err) = msg_part_text($part, $ct);
 
-       if ($ct =~ m!\btext/x?html\b!i) {
-               return attach_link($upfx, $ct, $p, $fn);
-       }
-
-       my $s = eval { $part->body_str };
-
-       # badly-encoded message? tell the world about it!
-       my $err = $@;
-       if ($err) {
-               if ($ct =~ m!\btext/plain\b!i) {
-                       # Try to assume UTF-8 because Alpine seems to
-                       # do wacky things and set charset=X-UNKNOWN
-                       $part->charset_set('UTF-8');
-                       $s = eval { $part->body_str };
-
-                       # If forcing charset=UTF-8 failed,
-                       # attach_link will warn further down...
-                       $s = $part->body if $@;
-               } else {
-                       return attach_link($upfx, $ct, $p, $fn);
-               }
-       }
+       return attach_link($upfx, $ct, $p, $fn) unless defined $s;
 
        my @lines = split(/^/m, $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";
        }
@@ -591,29 +604,43 @@ sub _msg_html_prepare {
                $ctx->{-upfx} = '../';
        }
        my @title;
-       foreach my $h (qw(From To Cc Subject Date)) {
-               my $v = $hdr->header($h);
-               defined($v) && ($v ne '') or next;
+       my $v;
+       if (defined($v = $hdr->header('From'))) {
                $v = PublicInbox::Hval->new($v);
-
-               if ($h eq 'From') {
-                       my @n = PublicInbox::Address::names($v->raw);
-                       $title[1] = ascii_html(join(', ', @n));
-                       obfuscate_addrs($obfs_ibx, $title[1]) if $obfs_ibx;
-               } elsif ($h eq 'Subject') {
-                       $title[0] = $v->as_html;
-                       if ($srch) {
-                               $rv .= qq($h: <a\nhref="#r"\nid=t>);
-                               $rv .= $v->as_html . "</a>\n";
-                               next;
-                       }
-               }
+               my @n = PublicInbox::Address::names($v->raw);
+               $title[1] = ascii_html(join(', ', @n));
                $v = $v->as_html;
+               if ($obfs_ibx) {
+                       obfuscate_addrs($obfs_ibx, $v);
+                       obfuscate_addrs($obfs_ibx, $title[1]);
+               }
+               $rv .= "From: $v\n" if $v ne '';
+       }
+       foreach my $h (qw(To Cc)) {
+               defined($v = $hdr->header($h)) or next;
+               fold_addresses($v);
+               $v = ascii_html($v);
                obfuscate_addrs($obfs_ibx, $v) if $obfs_ibx;
-               $rv .= "$h: $v\n";
-
+               $rv .= "$h: $v\n" if $v ne '';
+       }
+       if (defined($v = $hdr->header('Subject')) && ($v ne '')) {
+               $v = ascii_html($v);
+               obfuscate_addrs($obfs_ibx, $v) if $obfs_ibx;
+               if ($srch) {
+                       $rv .= qq(Subject: <a\nhref="#r"\nid=t>$v</a>\n);
+               } else {
+                       $rv .= "Subject: $v\n";
+               }
+               $title[0] = $v;
+       } else { # dummy anchor for thread skeleton at bottom of page
+               $rv .= qq(<a\nhref="#r"\nid=t></a>) if $srch;
+               $title[0] = '(no subject)';
+       }
+       if (defined($v = $hdr->header('Date'))) {
+               $v = ascii_html($v);
+               obfuscate_addrs($obfs_ibx, $v) if $obfs_ibx; # possible :P
+               $rv .= "Date: $v\n";
        }
-       $title[0] ||= '(no subject)';
        $ctx->{-title_html} = join(' - ', @title);
        foreach (@$mids) {
                my $mid = PublicInbox::Hval->new_msgid($_) ;
@@ -680,7 +707,8 @@ sub _parent_headers {
        my ($hdr, $srch) = @_;
        my $rv = '';
 
-       my $irt = in_reply_to($hdr);
+       my $refs = references($hdr);
+       my $irt = pop @$refs;
        if (defined $irt) {
                my $v = PublicInbox::Hval->new_msgid($irt);
                my $html = $v->as_html;
@@ -693,22 +721,9 @@ sub _parent_headers {
        # we show the thread skeleton at the bottom, instead.
        return $rv if $srch;
 
-       my $refs = $hdr->header_raw('References');
-       if ($refs) {
-               # avoid redundant URLs wasting bandwidth
-               my %seen;
-               $seen{$irt} = 1 if defined $irt;
-               my @refs;
-               my @raw_refs = ($refs =~ /<([^>]+)>/g);
-               foreach my $ref (@raw_refs) {
-                       next if $seen{$ref};
-                       $seen{$ref} = 1;
-                       push @refs, linkify_ref_nosrch($ref);
-               }
-
-               if (@refs) {
-                       $rv .= 'References: '. join("\n\t", @refs) . "\n";
-               }
+       if (@$refs) {
+               @$refs = map { linkify_ref_nosrch($_) } @$refs;
+               $rv .= 'References: '. join("\n\t", @$refs) . "\n";
        }
        $rv;
 }
@@ -787,10 +802,56 @@ sub indent_for {
        $level ? INDENT x ($level - 1) : '';
 }
 
+sub find_mid_root {
+       my ($ctx, $level, $node, $idx) = @_;
+       ++$ctx->{root_idx} if $level == 0;
+       if ($node->{id} eq $ctx->{mid}) {
+               $ctx->{found_mid_at} = $ctx->{root_idx};
+               return 0;
+       }
+       1;
+}
+
+sub strict_loose_note ($) {
+       my ($nr) = @_;
+       my $msg =
+"  -- strict thread matches above, loose matches on Subject: below --\n";
+
+       if ($nr > PublicInbox::Over::DEFAULT_LIMIT()) {
+               $msg .=
+"  -- use mbox.gz link to download all $nr messages --\n";
+       }
+       $msg;
+}
+
 sub thread_results {
        my ($ctx, $msgs) = @_;
        require PublicInbox::SearchThread;
-       PublicInbox::SearchThread::thread($msgs, *sort_ds, $ctx->{-inbox});
+       my $ibx = $ctx->{-inbox};
+       my $rootset = PublicInbox::SearchThread::thread($msgs, *sort_ds, $ibx);
+
+       # FIXME: `tid' is broken on --reindex, so that needs to be fixed
+       # and preserved in the future.  This bug is hidden by `sid' matches
+       # in get_thread, so we never noticed it until now.  And even when
+       # reindexing is fixed, we'll keep this code until a SCHEMA_VERSION
+       # bump since reindexing is expensive and users may not do it
+
+       # loose threading could've returned too many results,
+       # put the root the message we care about at the top:
+       my $mid = $ctx->{mid};
+       if (defined($mid) && scalar(@$rootset) > 1) {
+               $ctx->{root_idx} = -1;
+               my $nr = scalar @$msgs;
+               walk_thread($rootset, $ctx, *find_mid_root);
+               my $idx = $ctx->{found_mid_at};
+               if (defined($idx) && $idx != 0) {
+                       my $tip = splice(@$rootset, $idx, 1);
+                       @$rootset = reverse @$rootset;
+                       unshift @$rootset, $tip;
+                       $ctx->{sl_note} = strict_loose_note($nr);
+               }
+       }
+       $rootset
 }
 
 sub missing_thread {
@@ -833,6 +894,10 @@ sub skel_dump {
        my $cur = $ctx->{cur};
        my $mid = $smsg->{mid};
 
+       if ($level == 0 && $ctx->{skel_dump_roots}++) {
+               $$dst .= delete $ctx->{sl_note} || '';
+       }
+
        my $f = ascii_html($smsg->from_name);
        my $obfs_ibx = $ctx->{-obfs_ibx};
        obfuscate_addrs($obfs_ibx, $f) if $obfs_ibx;
@@ -851,7 +916,7 @@ sub skel_dump {
                        delete $ctx->{cur};
                        $$dst .= "<b>$d<a\nid=r\nhref=\"#t\">".
                                 "$attr [this message]</a></b>\n";
-                       return;
+                       return 1;
                } else {
                        $ctx->{prev_msg} = $mid;
                }
@@ -891,6 +956,7 @@ sub skel_dump {
                $m = $ctx->{-upfx}.mid_escape($mid).'/';
        }
        $$dst .=  $d . "<a\nhref=\"$m\"$id>" . $end;
+       1;
 }
 
 sub _skel_ghost {
@@ -916,6 +982,7 @@ sub _skel_ghost {
        }
        my $dst = $ctx->{dst};
        $$dst .= $d;
+       1;
 }
 
 sub sort_ds {
@@ -942,7 +1009,7 @@ sub acc_topic {
                        $topic = [ $ds, 1, { $subj => $mid }, $subj ];
                        $ctx->{-cur_topic} = $topic;
                        push @{$ctx->{order}}, $topic;
-                       return;
+                       return 1;
                }
 
                $topic = $ctx->{-cur_topic}; # should never be undef
@@ -956,11 +1023,12 @@ sub acc_topic {
                }
                $seen->{$subj} = $mid; # latest for subject
        } else { # ghost message
-               return if $level != 0; # ignore child ghosts
+               return if $level != 0; # ignore child ghosts
                $topic = [ -666, 0, {} ];
                $ctx->{-cur_topic} = $topic;
                push @{$ctx->{order}}, $topic;
        }
+       1;
 }
 
 sub dump_topics {