File Coverage

blib/lib/Mail/Box/Thread/Manager.pm
Criterion Covered Total %
statement 148 179 82.6
branch 35 64 54.6
condition 14 31 45.1
subroutine 30 34 88.2
pod 18 19 94.7
total 245 327 74.9


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Mail-Box version 4.01.
2             # The POD got stripped from this file by OODoc version 3.05.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2001-2025 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11              
12             package Mail::Box::Thread::Manager;{
13             our $VERSION = '4.01';
14             }
15              
16 4     4   1595 use parent 'Mail::Reporter';
  4         10  
  4         47  
17              
18 4     4   362 use strict;
  4         9  
  4         123  
19 4     4   18 use warnings;
  4         24  
  4         333  
20              
21 4     4   24 use Log::Report 'mail-box', import => [ qw/__x error/ ];
  4         8  
  4         40  
22              
23 4     4   3639 use Mail::Box::Thread::Node ();
  4         16  
  4         112  
24 4     4   2552 use Mail::Message::Dummy ();
  4         15  
  4         161  
25 4     4   27 use Scalar::Util qw/blessed/;
  4         12  
  4         11938  
26              
27             #--------------------
28              
29             sub init($)
30 3     3 0 36 { my ($self, $args) = @_;
31             $self->{MBTM_manager} = $args->{manager}
32 3 50       26 or error __x"thread manager needs a folder manager to work with.";
33              
34 3   50     52 $self->{MBTM_thread_body}= $args->{thread_body} // 0;
35 3   50     18 $self->{MBTM_thread_type}= $args->{thread_type} // 'Mail::Box::Thread::Node';
36 3   50     18 $self->{MBTM_dummy_type} = $args->{dummy_type} // 'Mail::Message::Dummy';
37 3   50     16 $self->{MBTM_window} = $args->{window} // 10;
38 3         10 $self->{MBTM_ids} = +{ };
39 3         16 $_[0]->{MBTM_folders} = +{ };
40              
41 3   50     19 my $ts = $args->{timespan} || '3 days';
42 3 50       34 $self->{MBTM_timespan} = $ts eq 'EVER' ? 'EVER' : Mail::Box->timespan2seconds($ts);
43 3         14 $self;
44             }
45              
46             #--------------------
47              
48 53     53 1 262 sub folderIndex() { $_[0]->{MBTM_folders} }
49 2     2 1 6 sub folders() { values %{$_[0]->folderIndex} }
  2         8  
50 45     45 1 135 sub folder($) { $_[0]->folderIndex->{$_[1]} }
51              
52              
53 565     565 1 1236 sub byId { $_[0]->{MBTM_ids} }
54              
55              
56 547 100   547 1 1119 sub msgById($) { my $ids = $_[0]->byId; @_ > 2 ? $ids->{$_[1]} = $_[2] : $ids->{$_[1]} }
  547         2181  
57              
58             #--------------------
59              
60             sub includeFolder(@)
61 3     3 1 8 { my $self = shift;
62 3         9 my $index = $self->folderIndex;
63              
64 3         8 foreach my $folder (@_)
65 3 50 33     46 { blessed $folder && $folder->isa('Mail::Box')
66             or error __x"attempt to include a none folder: {what UNKNOWN}.", what => $folder;
67              
68 3         18 my $name = $folder->name;
69 3 50       14 next if exists $index->{$name};
70              
71 3         10 $index->{$name} = $folder;
72 3         20 $self->inThread($_) for grep ! $_->head->isDelayed, $folder->messages;
73             }
74              
75 3         36 $self;
76             }
77              
78              
79             sub removeFolder(@)
80 3     3 1 10 { my $self = shift;
81 3         65 my $index = $self->folderIndex;
82              
83 3         12 foreach my $folder (@_)
84 3 50 33     50 { blessed $folder && $folder->isa('Mail::Box')
85             or error __x"attempt to remove a none folder: {what UNKNOWN}.", what => $folder;
86              
87 3         17 my $name = $folder->name;
88 3 50       17 delete $index->{$name} or next;
89              
90             $_->headIsRead && $self->outThread($_)
91 3   33     71 for $folder->messages;
92              
93 3         37 $self->{MBTM_cleanup_needed} = 1;
94             }
95              
96 3         14 $self;
97             }
98              
99             #--------------------
100              
101             sub thread($)
102 4     4 1 414 { my ($self, $message) = @_;
103 4         23 my $msgid = $message->messageId;
104 4         94 my $timestamp = $message->timestamp;
105              
106 4         3341 $self->_processDelayedNodes;
107 4 50       15 my $thread = $self->msgById($msgid) or return;
108              
109 4         9 my @missing;
110             $thread->recurse( sub {
111 10     10   19 my $node = shift;
112 10 50       27 push @missing, $node->messageId if $node->isDummy;
113 10         31 1;
114 4         81 });
115              
116 4 50       38 @missing or return $thread;
117              
118 0         0 foreach my $folder ($self->folders)
119             {
120             # Pull-in all messages received after this-one, from any folder.
121             # Clocks may drift a bit, so use margin.
122 0         0 my @now_missing = $folder->scanForMessages($msgid, \@missing, $timestamp - 3600, 0);
123              
124 0 0       0 if(@now_missing != @missing)
125 0         0 { $self->_processDelayedNodes;
126 0 0       0 @now_missing or last;
127 0         0 @missing = @now_missing;
128             }
129             }
130              
131 0         0 $thread;
132             }
133              
134              
135             sub threadStart($)
136 3     3 1 1378 { my ($self, $message) = @_;
137 3 50       13 my $thread = $self->thread($message) or return;
138              
139 3         14 while(my $parent = $thread->repliedTo)
140 2 100       10 { unless($parent->isDummy)
141             { # Message already found, no special action to be taken.
142 1         2 $thread = $parent;
143 1         4 next;
144             }
145              
146 1         5 foreach my $folder ($self->folders)
147 1         3 { my $message = $thread->message;
148 1 50       25 my $timespan = $message->isDummy ? 'ALL' : $message->timestamp - $self->{MBTM_timespan};
149              
150             $folder->scanForMessages($thread->messageId, $parent->messageId, $timespan, $self->{MBTM_window})
151 1 50       17 or last;
152             }
153              
154 1         8 $self->_processDelayedNodes;
155 1         7 $thread = $parent;
156             }
157              
158 3         16 $thread;
159             }
160              
161              
162             sub all()
163 0     0 1 0 { my $self = shift;
164 0         0 $_->find('not-existing') for $self->folders;
165 0         0 $self->known;
166             }
167              
168              
169             sub sortedAll(@)
170 1     1 1 3 { my $self = shift;
171 1         4 $_->find('not-existing') for $self->folders;
172 1         6 $self->sortedKnown(@_);
173             }
174              
175              
176             sub known()
177 3     3 1 18 { my $self = shift->_processDelayedNodes->_cleanup;
178 3         9 grep !defined $_->repliedTo, values %{$self->byId};
  3         11  
179             }
180              
181              
182             sub sortedKnown(;$$)
183 2     2 1 6 { my $self = shift;
184 2   33 56   31 my $prepare = shift || sub { $_[0]->startTimeEstimate || 0 };
  56         158  
185 2   33 201   17 my $compare = shift || sub { $_[0] <=> $_[1] };
  201         368  
186              
187             # Special care for double keys.
188 2         6 my %value;
189 2         11 push @{$value{$prepare->($_)}}, $_ for $self->known;
  56         6299  
190 2         43 map @{$value{$_}}, sort {$compare->($a, $b)} keys %value;
  54         129  
  201         324  
191             }
192              
193             # When a whole folder is removed, many threads can become existing
194             # only of dummies. They must be removed.
195              
196             sub _cleanup()
197 3     3   7 { my $self = shift;
198 3 50       16 $self->{MBTM_cleanup_needed} or return $self;
199              
200 0         0 foreach my $thread ($self->known)
201 0         0 { my $real = 0;
202             $thread->recurse( sub {
203 0     0   0 my $node = shift;
204 0         0 foreach my $msg ($node->messages)
205 0 0       0 { next if $msg->isDummy;
206 0         0 $real = 1;
207 0         0 return 0;
208             }
209 0         0 1;
210 0         0 });
211              
212 0 0       0 next if $real;
213              
214             $thread->recurse( sub {
215 0     0   0 my $node = shift;
216 0         0 delete $self->byId->{$node->messageId};
217 0         0 1;
218 0         0 });
219             }
220              
221 0         0 delete $self->{MBTM_cleanup_needed};
222 0         0 $self;
223             }
224              
225             #--------------------
226              
227             sub toBeThreaded($@)
228 45     45 1 114 { my ($self, $folder) = (shift, shift);
229 45 50       204 $self->folder($folder->name) or return $self;
230 45         207 $self->inThread($_) for @_;
231 45         163 $self;
232             }
233              
234              
235             sub toBeUnthreaded($@)
236 0     0 1 0 { my ($self, $folder) = (shift, shift);
237 0 0       0 $self->folder($folder->name) or return $self;
238 0         0 $self->outThread($_) for @_;
239 0         0 $self;
240             }
241              
242              
243             sub inThread($)
244 135     135 1 326 { my ($self, $message) = @_;
245 135         346 my $msgid = $message->messageId;
246 135         763 my $node = $self->msgById($msgid);
247              
248             # Already known, but might reside in many folders.
249 135 50       284 if($node) { $node->addMessage($message) }
  0         0  
250             else
251 135         714 { $node = Mail::Box::Thread::Node->new(message => $message, msgid => $msgid, dummy_type => $self->{MBTM_dummy_type});
252 135         442 $self->msgById($msgid, $node);
253             }
254              
255 135         525 $self->{MBTM_delayed}{$msgid} = $node; # removes doubles.
256             }
257              
258             # The relation between nodes is delayed, to avoid that first
259             # dummy nodes have to be made, and then immediately upgrades
260             # to real nodes. So: at first we inventory what we have, and
261             # then build thread-lists.
262              
263             sub _processDelayedNodes()
264 8     8   40 { my $self = shift;
265 8 100       46 $self->{MBTM_delayed} or return $self;
266              
267 3         9 foreach my $node (values %{$self->{MBTM_delayed}})
  3         31  
268 135         383 { $self->_processDelayedMessage($node, $_) for $node->message;
269             }
270              
271 3         67 delete $self->{MBTM_delayed};
272 3         14 $self;
273             }
274              
275             sub _processDelayedMessage($$)
276 135     135   288 { my ($self, $node, $message) = @_;
277 135         426 my $msgid = $message->messageId;
278              
279             # will force parsing of head when not done yet.
280 135 50       883 my $head = $message->head or return $self;
281              
282 135         2072 my $replies;
283 135 100       612 if(my $irt = $head->get('in-reply-to'))
284 54         1079 { for($irt =~ m/\<(\S+\@\S+)\>/)
285 54         2245 { my $msgid = $1;
286 54   66     159 $replies = $self->msgById($msgid) || $self->createDummy($msgid);
287             }
288             }
289              
290 135         1182 my @refs;
291 135 100       313 if(my $refs = $head->get('references'))
292 54         1009 { while($refs =~ s/\<(\S+\@\S+)\>//s)
293 84         3439 { my $msgid = $1;
294 84   66     208 push @refs, $self->msgById($msgid) || $self->createDummy($msgid);
295             }
296             }
297              
298             # Handle the `In-Reply-To' message header.
299             # This is the most secure relationship.
300              
301 135 100       1185 if($replies)
302 54 50       201 { $node->follows($replies, 'REPLY')
303             and $replies->followedBy($node);
304             }
305              
306             # Handle the `References' message header.
307             # The (ordered) list of message-IDs give an impression where this
308             # message resides in the thread. There is a little less certainty
309             # that the list is correctly ordered and correctly maintained.
310              
311 135 100       325 if(@refs)
312 54 50       222 { push @refs, $node unless $refs[-1] eq $node;
313 54         104 my $from = shift @refs;
314              
315 54         142 while(my $to = shift @refs)
316 84 50       192 { $to->follows($from, 'REFERENCE')
317             and $from->followedBy($to);
318 84         267 $from = $to;
319             }
320             }
321              
322 135         392 $self;
323             }
324              
325              
326             sub outThread($)
327 135     135 1 1022 { my ($self, $message) = @_;
328 135         283 my $msgid = $message->messageId;
329 135 100       723 my $node = $self->msgById($msgid) or return $message;
330              
331 134         199 $node->{MBTM_messages} = [ grep $_ ne $message, @{$node->{MBTM_messages}} ];
  134         441  
332 134         410 $self;
333             }
334              
335              
336             sub createDummy($)
337 15     15 1 43 { my ($self, $msgid) = @_;
338 15         75 $self->byId->{$msgid} = $self->{MBTM_thread_type}->new(msgid => $msgid, dummy_type => $self->{MBTM_dummy_type});
339             }
340              
341             #--------------------
342              
343             1;