Reply instructions:
+
+You may reply publically to this message via plain-text email
+using any one of the following methods:
+
+* Save the following mbox file, import it into your mail client,
+ and $reply_to_all from there: mbox
+
+ Avoid top-posting and favor interleaved quoting:
+ $p_url
+$info
+* Reply using the --to, --cc, and --in-reply-to
+ switches of git-send-email(1):
+
+ git send-email$arg
+
+ $se_url
+$link
+EOF
}
sub in_reply_to {
my ($hdr) = @_;
- my $irt = $hdr->header_raw('In-Reply-To');
+ my $refs = references($hdr);
+ $refs->[-1];
+}
- return mid_clean($irt) if (defined $irt);
+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));
+}
- my $refs = $hdr->header_raw('References');
- if ($refs && $refs =~ /<([^>]+)>\s*\z/s) {
- return $1;
- }
- undef;
+sub _hdr_names_html ($$) {
+ my ($hdr, $field) = @_;
+ my @vals = $hdr->header($field) or return '';
+ ascii_html(join(', ', PublicInbox::Address::names(join(',', @vals))));
+}
+
+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
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 ($smsg, $ctx, $more) = @_;
+ my $subj = $smsg->subject;
+ my $mid_raw = $smsg->mid;
+ my $id = id_compress($mid_raw, 1);
+ my $id_m = 'm'.$id;
+
+ my $root_anchor = $ctx->{root_anchor} || '';
+ my $irt;
+ my $obfs_ibx = $ctx->{-obfs_ibx};
+
+ $subj = '(no subject)' if $subj eq '';
+ my $rv = "* ";
+ $subj = ''.ascii_html($subj).'';
+ obfuscate_addrs($obfs_ibx, $subj) if $obfs_ibx;
+ $subj = "$subj" if $root_anchor eq $id_m;
+ $rv .= $subj . "\n";
+ $rv .= _th_index_lite($mid_raw, \$irt, $id, $ctx);
+ my @tocc;
+ 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 $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/";
+ 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($ds)." UTC";
+ my $upfx = $ctx->{-upfx};
+ my $mhref = $upfx . mid_escape($mid_raw) . '/';
+ $rv .= qq{ (permalink / };
+ $rv .= qq{raw)\n};
+ 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 .= '; +Cc: '.$cc if $clen;
+ } else {
+ $to_cc .= ' Cc: '.$cc if $clen;
}
- $rv .= " parent";
+ $to_cc .= "\n";
}
- if (my $pct = $state->{pct}) { # used by SearchView.pm
- $rv .= " [relevance $pct->{$mid_raw}%]";
- } elsif ($srch) {
- my $threaded = 'threaded';
+ 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)))) {
+ my $mirt = PublicInbox::Hval->new_msgid($irt);
+ my $href = $upfx . $mirt->{href}. '/';
+ my $html = $mirt->as_html;
+ $rv .= qq(In-Reply-To: <$html>\n)
+ }
+ $rv .= "\n";
+
+ # scan through all parts, looking for displayable text
+ my $ibx = $ctx->{-inbox};
+ msg_iter($mime, sub { $rv .= add_text_body($mhref, $ctx, $_[0]) });
+
+ # add the footer
+ $rv .= "\n^ ".
+ "permalink" .
+ " raw" .
+ " reply";
+
+ my $hr;
+ if (my $pct = $ctx->{pct}) { # used by SearchView.pm
+ $rv .= "\t[relevance $pct->{$mid_raw}%]";
+ $hr = 1;
+ } elsif ($mapping) {
+ my $nested = 'nested';
my $flat = 'flat';
my $end = '';
if ($ctx->{flat}) {
+ $hr = 1;
$flat = "$flat";
- $end = "\n"; # for lynx
} else {
- $threaded = "$threaded";
+ $nested = "$nested";
}
- $rv .= " [$threaded";
- $rv .= "|$flat]$end";
+ $rv .= "\t[$flat";
+ $rv .= "|$nested]";
+ $rv .= " $ctx->{s_nr}";
+ } else {
+ $hr = $ctx->{-hr};
}
- $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) }
+ $rv .= $more ? '
' : '
' if $hr;
+ $rv;
}
-# 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,
- };
+sub pad_link ($$;$) {
+ my ($mid, $level, $s) = @_;
+ $s ||= '...';
+ my $id = id_compress($mid, 1);
+ (' 'x19).indent_for($level).th_pfx($level)."($s)\n";
+}
- 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(
- ('' x ($max - 1)) . '');
+sub _th_index_lite {
+ my ($mid_raw, $irt, $id, $ctx) = @_;
+ my $rv = '';
+ my $mapping = $ctx->{mapping} or return $rv;
+ my $pad = ' ';
+ my $mid_map = $mapping->{$mid_raw};
+ defined $mid_map or
+ return 'public-inbox BUG: '.ascii_html($mid_raw).' not mapped';
+ my ($attr, $node, $idx, $level) = @$mid_map;
+ my $children = $node->{children};
+ my $nr_c = scalar @$children;
+ my $nr_s = 0;
+ my $siblings;
+ if (my $smsg = $node->{smsg}) {
+ # 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) {
+ $siblings = $irt_map->[1]->{children};
+ $nr_s = scalar(@$siblings) - 1;
+ $rv .= $pad . $irt_map->[0];
+ if ($idx > 0) {
+ my $prev = $siblings->[$idx - 1];
+ my $pmid = $prev->{id};
+ if ($idx > 2) {
+ my $s = ($idx - 1). ' preceding siblings ...';
+ $rv .= pad_link($pmid, $level, $s);
+ } elsif ($idx == 2) {
+ my $ppmid = $siblings->[0]->{id};
+ $rv .= $pad . $mapping->{$ppmid}->[0];
+ }
+ $rv .= $pad . $mapping->{$pmid}->[0];
+ }
+ }
+ my $s_s = nr_to_s($nr_s, 'sibling', 'siblings');
+ my $s_c = nr_to_s($nr_c, 'reply', 'replies');
+ $attr =~ s!\n\z!\n!s;
+ $attr =~ s! !!s; # no point in duplicating subject
+ $attr =~ s!]+>([^<]+)!$1!s; # no point linking to self
+ $rv .= "@ $attr";
+ if ($nr_c) {
+ my $cmid = $children->[0]->{id};
+ $rv .= $pad . $mapping->{$cmid}->[0];
+ if ($nr_c > 2) {
+ my $s = ($nr_c - 1). ' more replies';
+ $rv .= pad_link($cmid, $level + 1, $s);
+ } elsif (my $cn = $children->[1]) {
+ $rv .= $pad . $mapping->{$cn->{id}}->[0];
}
}
- # 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 $next = $siblings->[$idx+1] if $siblings && $idx >= 0;
+ if ($next) {
+ my $nmid = $next->{id};
+ $rv .= $pad . $mapping->{$nmid}->[0];
+ my $nnext = $nr_s - $idx;
+ if ($nnext > 2) {
+ my $s = ($nnext - 1).' subsequent siblings';
+ $rv .= pad_link($nmid, $level, $s);
+ } elsif (my $nn = $siblings->[$idx + 2]) {
+ $rv .= $pad . $mapping->{$nn->{id}}->[0];
+ }
+ }
+ $rv .= $pad ."$s_s, $s_c; $ctx->{s_nr}\n";
+}
- 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 . '