1 # This library is free software; you can redistribute it and/or modify
2 # it under the same terms as Perl itself.
4 # This license differs from the rest of public-inbox
6 # Our own jwz-style threading class based on Mail::Thread from CPAN.
7 # Mail::Thread is unmaintained and available on some distros.
8 # We also do not want pruning or subject grouping, since we want
9 # to encourage strict threading and hopefully encourage people
10 # to use proper In-Reply-To.
12 # This includes fixes from several open bugs for Mail::Thread
14 # Avoid circular references
15 # - https://rt.cpan.org/Public/Bug/Display.html?id=22817
17 # And avoid recursion in recurse_down:
18 # - https://rt.cpan.org/Ticket/Display.html?id=116727
19 # - http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=833479
20 package PublicInbox::SearchThread;
32 sub rootset { @{$_[0]{rootset}} }
38 grep { !$_->{parent} } values %{$self->{id_table}} ];
44 delete $self->{id_table};
48 sub _get_cont_for_id ($$) {
49 my ($self, $mid) = @_;
50 $self->{id_table}{$mid} ||= PublicInbox::SearchThread::Msg->new($mid);
56 _add_message($self, $_) foreach @{$self->{messages}};
59 sub _add_message ($$) {
60 my ($self, $smsg) = @_;
63 my $this = _get_cont_for_id($self, $smsg->{mid});
64 $this->{smsg} = $smsg;
66 # B. For each element in the message's References field:
68 if (defined(my $refs = $smsg->{references})) {
69 foreach my $ref ($refs =~ m/<([^>]+)>/g) {
70 # Find a Container object for the given Message-ID
71 my $cont = _get_cont_for_id($self, $ref);
73 # Link the References field's Containers together in
74 # the order implied by the References header
76 # * If they are already linked don't change the
78 # * Do not add a link if adding that link would
81 !$cont->{parent} && # already linked
82 !$cont->has_descendent($prev) # would loop
84 $prev->add_child($cont);
90 # C. Set the parent of this message to be the last element in
92 if ($prev && !$this->has_descendent($prev)) { # would loop
93 $prev->add_child($this)
102 my $root = _get_cont_for_id($self, 'fakeroot');
103 $root->add_child( $_ ) for @{ $self->{rootset} };
106 $root->order_children( $ordersub );
109 my $kids = $root->children;
110 $self->{rootset} = $kids;
111 $root->remove_child($_) for @$kids;
114 package PublicInbox::SearchThread::Msg;
116 use Scalar::Util qw(weaken);
118 sub new { my $self = shift; bless { id => shift }, $self; }
121 my ($self, $child) = @_;
122 croak "Cowardly refusing to become my own parent: $self"
125 if (grep { $_ == $child } @{$self->children}) {
126 # All is potentially correct with the world
127 weaken($child->{parent} = $self);
131 my $parent = $child->{parent};
132 remove_child($parent, $child) if $parent;
134 $child->{next} = $self->{child};
135 $self->{child} = $child;
136 weaken($child->{parent} = $self);
140 my ($self, $child) = @_;
142 my $x = $self->{child} or return;
143 if ($x == $child) { # First one's easy.
144 $self->{child} = $child->{next};
145 $child->{parent} = $child->{next} = undef;
150 while ($x = $x->{next}) {
152 $prev->{next} = $x->{next}; # Unlink x
153 $x->{next} = $x->{parent} = undef; # Deparent it
158 # oddly, we can get here
159 $child->{next} = $child->{parent} = undef;
165 die "Assertion failed: $child" unless eval {$child};
167 $self->recurse_down(sub { $there = 1 if $_[0] == $child });
175 my $visitor = $self->{child};
177 push @children, $visitor;
178 $visitor = $visitor->{next};
184 my ($self, $children) = @_;
185 my $walk = $self->{child} = shift @$children;
187 $walk = $walk->{next} = shift @$children;
193 my $ordersub = shift;
195 return unless $ordersub;
199 my $children = $cont->children;
200 return if @$children < 2;
201 $cont->set_children( $ordersub->( $children ) );
203 $self->iterate_down( undef, $sub );
207 # non-recursive version of recurse_down to avoid stack depth warnings
209 my ($self, $callback) = @_;
212 while (my $cont = shift @q) {
216 if (my $next = $cont->{next}) {
218 $cont->{next} = undef;
223 if (my $child = $cont->{child}) {
225 $cont->{child} = undef;
235 my ($before, $after) = @_;
242 push @visited, [ $walk, $depth ];
243 $before->($walk, $depth) if $before;
248 my $child = $walk->{child};
249 if ($child && $seen{$child}) {
250 $walk->{child} = $child = undef;
253 my $next = $walk->{next};
254 if ($next && $seen{$next}) {
255 $walk->{next} = $next = undef;
267 while ($up && !$next) {
270 $next = $up->{next} if $up;
275 return unless $after;
276 while (@visited) { $after->(@{ pop @visited }) }