2 * ========================================================================
3 * Copyright 2013-2022 Eduardo Chappa
4 * Copyright 2006-2007 University of Washington
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * ========================================================================
15 #include "../pith/headers.h"
16 #include "../pith/sort.h"
17 #include "../pith/state.h"
18 #include "../pith/status.h"
19 #include "../pith/msgno.h"
20 #include "../pith/flag.h"
21 #include "../pith/pineelt.h"
22 #include "../pith/thread.h"
23 #include "../pith/search.h"
24 #include "../pith/pattern.h"
25 #include "../pith/util.h"
26 #include "../pith/signal.h"
27 #include "../pith/busy.h"
28 #include "../pith/icache.h"
32 * global place to store mail_sort and mail_thread results
34 struct global_sort_data g_sort
;
40 void sort_sort_callback(MAILSTREAM
*, unsigned long *, unsigned long);
41 int percent_sorted(void);
42 int pine_compare_long(const qsort_t
*, const qsort_t
*);
43 int pine_compare_long_rev(const qsort_t
*, const qsort_t
*);
44 int pine_compare_scores(const qsort_t
*, const qsort_t
*);
45 void build_score_array(MAILSTREAM
*, MSGNO_S
*);
46 void free_score_array(void);
49 /*----------------------------------------------------------------------
50 Map sort types to names
53 sort_name(SortOrder so
)
56 * Make sure the first upper case letter of any new sort name is
57 * unique. The command char and label for sort selection is
58 * derived from this name and its first upper case character.
59 * See mailcmd.c:select_sort().
61 return((so
== SortArrival
) ? "Arrival" :
62 (so
== SortDate
) ? "Date" :
63 (so
== SortSubject
) ? "Subject" :
64 (so
== SortCc
) ? "Cc" :
65 (so
== SortFrom
) ? "From" :
66 (so
== SortTo
) ? "To" :
67 (so
== SortSize
) ? "siZe" :
68 (so
== SortSubject2
) ? "OrderedSubj" :
69 (so
== SortScore
) ? "scorE" :
70 (so
== SortThread
) ? "tHread" :
75 /*----------------------------------------------------------------------
76 Sort the current folder into the order set in the msgmap
82 The idea of the deferred sort is to let the user interrupt a long sort
83 and have a chance to do a different command, such as a sort by arrival
84 or a Goto. The next newmail call will increment the deferred variable,
85 then the user may do a command, then the newmail call after that
86 causes the sort to happen if it is still needed.
89 sort_folder(MAILSTREAM
*stream
, MSGNO_S
*msgmap
, SortOrder new_sort
,
90 int new_rev
, unsigned int flags
)
92 long raw_current
, i
, j
;
93 unsigned long *sort
= NULL
;
95 char sort_msg
[MAX_SCREEN_COLS
+1];
96 SortOrder current_sort
;
100 dprint((2, "Sorting by %s%s\n",
101 sort_name(new_sort
), new_rev
? "/reverse" : ""));
106 current_sort
= mn_get_sort(msgmap
);
107 current_rev
= mn_get_revsort(msgmap
);
109 * If we were previously threading (spare == 1) and now we're switching
110 * sorts (other than just a rev switch) then erase the information
111 * about the threaded state (collapsed and so forth).
113 if(stream
&& stream
->spare
&& (current_sort
!= new_sort
))
114 erase_threading_info(stream
, msgmap
);
116 if(mn_get_total(msgmap
) <= 1L
117 && !(mn_get_total(msgmap
) == 1L
118 && (new_sort
== SortThread
|| new_sort
== SortSubject2
))){
119 mn_set_sort(msgmap
, new_sort
);
120 mn_set_revsort(msgmap
, new_rev
);
121 if(!mn_get_mansort(msgmap
))
122 mn_set_mansort(msgmap
, (flags
& SRT_MAN
) ? 1 : 0);
127 raw_current
= mn_m2raw(msgmap
, mn_get_cur(msgmap
));
129 if(new_sort
== SortArrival
){
131 * NOTE: RE c-client sorting, our idea of arrival is really
132 * just the natural sequence order. C-client, and probably
133 * rightly so, considers "arrival" the order based on the
134 * message's internal date. This is more costly to compute
135 * since it means touching the message store (say "nntp"?),
136 * so we just preempt it here.
138 * Someday c-client will support "unsorted" and do what
139 * we're doing here. That day this gets scrapped.
142 mn_set_sort(msgmap
, new_sort
);
143 mn_set_revsort(msgmap
, new_rev
);
145 if(current_sort
!= new_sort
|| current_rev
!= new_rev
||
146 any_lflagged(msgmap
, MN_EXLD
))
147 clear_index_cache(stream
, 0);
149 if(any_lflagged(msgmap
, MN_EXLD
)){
151 * BEWARE: "exclusion" may leave holes in the unsorted sort order
152 * so we have to do a real sort if that is the case.
154 qsort(msgmap
->sort
+1, (size_t) mn_get_total(msgmap
),
156 new_rev
? pine_compare_long_rev
: pine_compare_long
);
158 else if(mn_get_total(msgmap
) > 0L){
160 clear_index_cache(stream
, 0);
161 for(i
= 1L, j
= mn_get_total(msgmap
); j
>= 1; i
++, j
--)
165 for(i
= 1L; i
<= mn_get_total(msgmap
); i
++)
169 /* reset the inverse array */
170 msgno_reset_isort(msgmap
);
172 else if(new_sort
== SortScore
){
175 * We have to build a temporary array which maps raw msgno to
176 * score. We use the index cache machinery to build the array.
179 mn_set_sort(msgmap
, new_sort
);
180 mn_set_revsort(msgmap
, new_rev
);
183 /* TRANSLATORS: tell user they are waiting for Sorting of %s, the foldername */
184 snprintf(sort_msg
, sizeof(sort_msg
), _("Sorting \"%s\""),
185 strsquish(tmp_20k_buf
+ 500, 500, ps_global
->cur_folder
,
186 ps_global
->ttyo
->screen_cols
- 20));
187 we_cancel
= busy_cue(sort_msg
, NULL
, 0);
191 * We do this so that we don't have to lookup the scores with function
192 * calls for each qsort compare.
194 build_score_array(stream
, msgmap
);
196 qsort(msgmap
->sort
+1, (size_t) mn_get_total(msgmap
),
197 sizeof(long), pine_compare_scores
);
199 clear_index_cache(stream
, 0);
205 * Flip the sort if necessary (cheaper to do it once than for
206 * every comparison in pine_compare_scores.
208 if(new_rev
&& mn_get_total(msgmap
) > 1L){
209 long *ep
= &msgmap
->sort
[mn_get_total(msgmap
)],
210 *sp
= &msgmap
->sort
[1], tmp
;
220 /* reset the inverse array */
221 msgno_reset_isort(msgmap
);
225 mn_set_sort(msgmap
, new_sort
);
226 mn_set_revsort(msgmap
, new_rev
);
227 clear_index_cache(stream
, 0);
230 int (*sort_func
)() = NULL
;
233 * IMAP sort doesn't give us any way to get progress,
234 * so just spin the bar rather than show zero percent
235 * forever while a slow sort's going on...
237 if(!(stream
&& stream
->dtb
&& stream
->dtb
->name
238 && !strucmp(stream
->dtb
->name
, "imap")
239 && LEVELSORT(stream
)))
240 sort_func
= percent_sorted
;
242 snprintf(sort_msg
, sizeof(sort_msg
), _("Sorting \"%s\""),
243 strsquish(tmp_20k_buf
+ 500, 500, ps_global
->cur_folder
,
244 ps_global
->ttyo
->screen_cols
- 20));
245 we_cancel
= busy_cue(sort_msg
, sort_func
, 0);
249 * Limit the sort/thread if messages are hidden from view
250 * by lighting searched bit of every interesting msg in
251 * the folder and call c-client thread/sort to do the dirty work.
253 * Unfortunately it isn't that easy. IMAP servers are not able to
254 * handle medium to large sized sequence sets (more than 1000
255 * characters in the command line breaks some) so we have to try
256 * to handle it locally. By lighting the searched bits and
257 * providing a NULL search program we get a special c-client
258 * interface. This means that c-client will attempt to send the
259 * sequence set with the SORT or THREAD but it may get back
260 * a BAD response because of long command lines. In that case,
261 * if it is a SORT call, c-client will issue the full SORT
262 * without the sequence sets and will then filter the results
263 * locally. So sort_sort_callback will see the correctly
264 * filtered results. If it is a mail_thread call, a similar thing
265 * will be done. If a BAD is received, then there is no way to
266 * easily filter the results. C-client (in this special case where
267 * we provide a NULL search program) will set tree->num to zero
268 * for nodes of the thread tree which were supposed to be
269 * filtered out of the thread. Then pine, in sort_thread_callback,
270 * will treat those as dummy nodes (nodes which are part of the
271 * tree logically but where we don't have access to the messages).
272 * This will give us a different answer than we would have gotten
273 * if the restricted thread would have worked, but it's something.
275 * It isn't in general possible to give some shorter search set
276 * in place of the long sequence set because the long sequence set
277 * may be the result of several filter rules or of excluded
278 * messages in news (in this 2nd case maybe we could give
279 * a shorter search set).
281 * We note also that the too-long commands in imap is a general
282 * imap deficiency. It comes up in particular also in SEARCH
283 * commands. Pine likes to exclude the hidden messages from the
284 * SEARCH. SEARCH will be handled transparently by the local
285 * c-client by first issuing the full SEARCH command, if that
286 * comes back with a BAD and there is a pgm->msgno at the top
287 * level of pgm, then c-client will re-issue the SEARCH command
288 * but without the msgno sequence set in hopes that the resulting
289 * command will now be short enough, and then it will filter out
290 * the sequence set locally. If that doesn't work, it will
291 * download the messages and do the SEARCH locally. That is
292 * controllable by a flag bit.
294 for(i
= 1L; i
<= stream
->nmsgs
; i
++)
295 if((mc
= mail_elt(stream
, i
)) != NULL
)
296 mc
->searched
= !get_lflag(stream
, NULL
, i
, MN_EXLD
);
298 g_sort
.msgmap
= msgmap
;
299 if(new_sort
== SortThread
|| new_sort
== SortSubject2
){
303 * Install callback to collect thread results
304 * and update sort mapping. Problem this solves
305 * is that of receiving exists/expunged events
306 * within sort/thread response. Since we update
307 * the sorted table within those handlers, we
308 * can get out of sync when we replace possibly
309 * stale sort/thread results once the function
310 * call's returned. Make sense? Thought so.
312 mail_parameters(NULL
, SET_THREADRESULTS
,
313 (void *) sort_thread_callback
);
315 thread
= mail_thread(stream
,
316 (new_sort
== SortThread
)
317 ? "REFERENCES" : "ORDEREDSUBJECT",
320 mail_parameters(NULL
, SET_THREADRESULTS
, (void *) NULL
);
323 new_sort
= current_sort
;
324 new_rev
= current_rev
;
325 q_status_message1(SM_ORDER
, 3, 3,
326 "Sort Failed! Restored %.200s sort.",
327 sort_name(new_sort
));
331 mail_free_threadnode(&thread
);
335 * Set up the sort program.
336 * NOTE: we deal with reverse bit below.
338 g_sort
.prog
= mail_newsortpgm();
339 g_sort
.prog
->function
= (new_sort
== SortSubject
)
341 : (new_sort
== SortFrom
)
343 : (new_sort
== SortTo
)
345 : (new_sort
== SortCc
)
347 : (new_sort
== SortDate
)
349 : (new_sort
== SortSize
)
353 mail_parameters(NULL
, SET_SORTRESULTS
,
354 (void *) sort_sort_callback
);
356 /* Where the rubber meets the road. */
357 sort
= mail_sort(stream
, NULL
, NULL
, g_sort
.prog
, 0L);
359 mail_parameters(NULL
, SET_SORTRESULTS
, (void *) NULL
);
362 new_sort
= current_sort
;
363 new_rev
= current_rev
;
364 q_status_message1(SM_ORDER
, 3, 3,
365 "Sort Failed! Restored %s sort.",
366 sort_name(new_sort
));
370 fs_give((void **) &sort
);
372 mail_free_sortpgm(&g_sort
.prog
);
379 * Flip the sort if necessary (cheaper to do it once than for
380 * every comparison underneath mail_sort()
382 if(new_rev
&& mn_get_total(msgmap
) > 1L){
383 long *ep
= &msgmap
->sort
[mn_get_total(msgmap
)],
384 *sp
= &msgmap
->sort
[1], tmp
;
393 /* reset the inverse array */
394 msgno_reset_isort(msgmap
);
397 * Flip the thread numbers around.
398 * This puts us in a weird state that requires keeping track
399 * of. The direction of the thread list hasn't changed, but the
400 * thrdnos have and the display direction has.
402 * For Sort thrdno 1 thread list head
404 * thrdno . v nextthd this direction
406 * thrdno n thread list tail
408 * Rev Sort thrdno 1 thread list tail
410 * thrdno . ^ nextthd this direction
412 * thrdno n thread list head
414 if(new_sort
== SortThread
|| new_sort
== SortSubject2
){
417 thrd
= fetch_head_thread(stream
);
418 for(j
= msgmap
->max_thrdno
; thrd
&& j
>= 1L; j
--){
422 thrd
= fetch_thread(stream
,
431 /* Fix up sort structure */
432 mn_set_sort(msgmap
, new_sort
);
433 mn_set_revsort(msgmap
, new_rev
);
435 * Once a folder has been sorted manually, we continue treating it
436 * as manually sorted until it is closed.
438 if(!mn_get_mansort(msgmap
))
439 mn_set_mansort(msgmap
, (flags
& SRT_MAN
) ? 1 : 0);
441 if(!msgmap
->hilited
){
443 * If current is hidden, change current to visible parent.
444 * It can only be hidden if we are threading.
446 * Don't do this if hilited is set, because it means we're in the
447 * middle of an aggregate op, and this will mess up our selection.
448 * "hilited" means we've done a pseudo_selected, which we'll later
449 * fix with restore_selected.
452 mn_reset_cur(msgmap
, first_sorted_flagged(new_rev
? F_NONE
: F_SRCHBACK
,
454 mn_raw2m(msgmap
, raw_current
),
457 mn_reset_cur(msgmap
, mn_raw2m(msgmap
, raw_current
));
462 if(!sp_mail_box_changed(stream
))
463 sp_set_unsorted_newmail(stream
, 0);
466 * Turn off the MN_USOR flag. Don't bother going through the
467 * function call and the message number mappings.
472 for(i
= 1L; i
<= stream
->nmsgs
; i
++)
473 if((mc
= mail_elt(stream
, i
)) != NULL
&& (pelt
= mc
->sparep
) != NULL
)
480 sort_sort_callback(MAILSTREAM
*stream
, long unsigned int *list
, long unsigned int nmsgs
)
484 dprint((2, "sort_sort_callback\n"));
486 if(mn_get_total(g_sort
.msgmap
) < nmsgs
)
487 alpine_panic("Message count shrank after sort!");
489 /* copy ulongs to array of longs */
490 for(i
= nmsgs
; i
> 0; i
--)
491 g_sort
.msgmap
->sort
[i
] = list
? (long) list
[i
-1] : i
-1;
493 /* reset the inverse array */
494 msgno_reset_isort(g_sort
.msgmap
);
496 dprint((2, "sort_sort_callback done\n"));
501 * Return value for use by progress bar.
507 * C-client's sort routine exports two types of status
508 * indicators. One's the progress thru loading the cache (typically
509 * the elephantine bulk of the delay, and the progress thru the
510 * actual sort (typically qsort). Our job is to balance the two
513 return(g_sort
.prog
&& g_sort
.prog
->nmsgs
514 ? (((((g_sort
.prog
->progress
.cached
* 100)
515 / g_sort
.prog
->nmsgs
) * 9) / 10)
516 + (((g_sort
.prog
->progress
.sorted
) * 10)
517 / g_sort
.prog
->nmsgs
))
523 * This is only used at startup time. It sets ps->def_sort and
524 * ps->def_sort_rev. The syntax of the sort_spec is type[/reverse].
525 * A reverse without a type is the same as arrival/reverse. A blank
526 * argument also means arrival/reverse.
529 decode_sort(char *sort_spec
, SortOrder
*def_sort
, int *def_sort_rev
)
532 char *fix_this
= NULL
;
535 if(!sort_spec
|| !*sort_spec
){
536 *def_sort
= SortArrival
;
541 if(struncmp(sort_spec
, "reverse", strlen(sort_spec
)) == 0){
542 *def_sort
= SortArrival
;
548 if((sep
= strindex(sort_spec
, '/')) != NULL
){
552 if(struncmp(sep
, "reverse", strlen(sep
)) == 0)
560 for(x
= 0; ps_global
->sort_types
[x
] != EndofList
; x
++)
561 if(struncmp(sort_name(ps_global
->sort_types
[x
]),
562 sort_spec
, strlen(sort_spec
)) == 0)
568 if(ps_global
->sort_types
[x
] == EndofList
)
571 *def_sort
= ps_global
->sort_types
[x
];
572 *def_sort_rev
= reverse
;
577 /*----------------------------------------------------------------------
578 Compare raw message numbers
581 pine_compare_long(const qsort_t
*a
, const qsort_t
*b
)
583 long *mess_a
= (long *)a
, *mess_b
= (long *)b
, mdiff
;
585 return((mdiff
= *mess_a
- *mess_b
) ? ((mdiff
> 0L) ? 1 : -1) : 0);
589 * reverse version of pine_compare_long
592 pine_compare_long_rev(const qsort_t
*a
, const qsort_t
*b
)
594 long *mess_a
= (long *)a
, *mess_b
= (long *)b
, mdiff
;
596 return((mdiff
= *mess_a
- *mess_b
) ? ((mdiff
< 0L) ? 1 : -1) : 0);
603 * This calculate all of the scores and also puts them into a temporary array
604 * for speed when sorting.
607 build_score_array(MAILSTREAM
*stream
, MSGNO_S
*msgmap
)
609 SEARCHSET
*searchset
;
610 long msgno
, cnt
, nmsgs
, rawmsgno
;
614 nmsgs
= mn_get_total(msgmap
);
615 g_score_arr
= (long *) fs_get((nmsgs
+1) * sizeof(score
));
616 memset(g_score_arr
, 0, (nmsgs
+1) * sizeof(score
));
619 * Build a searchset that contains everything except those that have
620 * already been looked up.
623 for(msgno
=1L; msgno
<= stream
->nmsgs
; msgno
++)
624 if((mc
= mail_elt(stream
, msgno
)) != NULL
)
627 for(cnt
=0L, msgno
=1L; msgno
<= nmsgs
; msgno
++){
628 rawmsgno
= mn_m2raw(msgmap
, msgno
);
629 if(get_msg_score(stream
, rawmsgno
) == SCORE_UNDEF
630 && rawmsgno
> 0L && stream
&& rawmsgno
<= stream
->nmsgs
631 && (mc
= mail_elt(stream
, rawmsgno
))){
638 searchset
= build_searchset(stream
);
639 (void)calculate_some_scores(stream
, searchset
, 0);
640 mail_free_searchset(&searchset
);
644 * Copy scores to g_score_arr. They should all be defined now but if
645 * they aren't assign score zero.
647 for(rawmsgno
= 1L; rawmsgno
<= nmsgs
; rawmsgno
++){
648 score
= get_msg_score(stream
, rawmsgno
);
649 g_score_arr
[rawmsgno
] = (score
== SCORE_UNDEF
) ? 0L : score
;
655 free_score_array(void)
658 fs_give((void **) &g_score_arr
);
662 /*----------------------------------------------------------------------
666 pine_compare_scores(const qsort_t
*a
, const qsort_t
*b
)
668 long *mess_a
= (long *)a
, *mess_b
= (long *)b
, mdiff
;
671 return((sdiff
= g_score_arr
[*mess_a
] - g_score_arr
[*mess_b
])
672 ? ((sdiff
> 0L) ? 1 : -1)
673 : ((mdiff
= *mess_a
- *mess_b
) ? ((mdiff
> 0) ? 1 : -1) : 0));
678 reset_sort_order(unsigned int flags
)
680 long rflags
= ROLE_DO_OTHER
;
683 SortOrder the_sort_order
;
686 /* set default order */
687 the_sort_order
= ps_global
->def_sort
;
688 sort_is_rev
= ps_global
->def_sort_rev
;
690 if(ps_global
->mail_stream
&& nonempty_patterns(rflags
, &pstate
)){
691 for(pat
= first_pattern(&pstate
); pat
; pat
= next_pattern(&pstate
)){
692 if(match_pattern(pat
->patgrp
, ps_global
->mail_stream
, NULL
,
693 NULL
, NULL
, SE_NOSERVER
|SE_NOPREFETCH
))
697 if(pat
&& pat
->action
&& !pat
->action
->bogus
698 && pat
->action
->sort_is_set
){
699 the_sort_order
= pat
->action
->sortorder
;
700 sort_is_rev
= pat
->action
->revsort
;
704 sort_folder(ps_global
->mail_stream
, ps_global
->msgmap
,
705 the_sort_order
, sort_is_rev
, flags
);