commit 8566283c59a25871b42e15fa6ddbce676afb8f12
Author: Oswald Buddenhagen <o...@users.sf.net>
Date:   Thu May 5 20:31:43 2022 +0200

    make expiration target side configurable
    
    REFMAIL: 87k0fauw7q....@wavexx.thregr.org

 NEWS             |   2 +
 src/config.c     |  13 ++++++
 src/mbsync.1     |   9 +++-
 src/run-tests.pl |  95 +++++++++++++++++++++++++++++++++-----
 src/sync.c       | 118 +++++++++++++++++++++++++++--------------------
 src/sync.h       |   1 +
 src/sync_p.h     |   6 +--
 src/sync_state.c |  22 +++++++--
 8 files changed, 199 insertions(+), 67 deletions(-)

diff --git a/NEWS b/NEWS
index bbee6112..7b4d557d 100644
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,8 @@ they are flagged on the source side.
 Renamed the ReNew/--renew/-N options to Upgrade/--upgrade/-u
 and Delete/--delete/-d to Gone/--gone/-g.
 
+Made the Channel side to expire with MaxMessages configurable.
+
 MaxMessages and MaxSize can be used together now.
 
 The unfiltered list of mailboxes in each Store can be printed now.
diff --git a/src/config.c b/src/config.c
index d7b4ff6b..07201120 100644
--- a/src/config.c
+++ b/src/config.c
@@ -261,6 +261,17 @@ getopt_helper( conffile_t *cfile, int *cops, 
channel_conf_t *conf )
                conf->use_internal_date = parse_bool( cfile );
        } else if (!strcasecmp( "MaxMessages", cfile->cmd )) {
                conf->max_messages = parse_int( cfile );
+       } else if (!strcasecmp( "ExpireSide", cfile->cmd )) {
+               arg = cfile->val;
+               if (!strcasecmp( "Far", arg )) {
+                       conf->expire_side = F;
+               } else if (!strcasecmp( "Near", arg )) {
+                       conf->expire_side = N;
+               } else {
+                       error( "%s:%d: invalid ExpireSide argument '%s'\n",
+                              cfile->file, cfile->line, arg );
+                       cfile->err = 1;
+               }
        } else if (!strcasecmp( "ExpireUnread", cfile->cmd )) {
                conf->expire_unread = parse_bool( cfile );
        } else {
@@ -481,6 +492,7 @@ load_config( const char *where )
 
        gcops = 0;
        glob_ok = 1;
+       global_conf.expire_side = N;
        global_conf.expire_unread = -1;
   reloop:
        while (getcline( &cfile )) {
@@ -505,6 +517,7 @@ load_config( const char *where )
                        channel = nfzalloc( sizeof(*channel) );
                        channel->name = nfstrdup( cfile.val );
                        channel->max_messages = global_conf.max_messages;
+                       channel->expire_side = global_conf.expire_side;
                        channel->expire_unread = global_conf.expire_unread;
                        channel->use_internal_date = 
global_conf.use_internal_date;
                        cops = 0;
diff --git a/src/mbsync.1 b/src/mbsync.1
index 79a6afba..87672c66 100644
--- a/src/mbsync.1
+++ b/src/mbsync.1
@@ -579,6 +579,12 @@ case you need to enable this option.
 (Global default: \fBno\fR).
 .
 .TP
+\fBExpireSide\fR \fBFar\fR|\fBNear\fR
+Selects on which side messages should be expired when \fBMaxMessages\fR is
+configured.
+(Global default: \fBNear\fR).
+.
+.TP
 \fBSync\fR {\fBNone\fR|[\fBPull\fR] [\fBPush\fR] [\fBNew\fR] [\fBOld\fR] 
[\fBUpgrade\fR] [\fBGone\fR] [\fBFlags\fR] [\fBFull\fR]}
 Select the synchronization operation(s) to perform:
 .br
@@ -693,7 +699,8 @@ date\fR) is actually the arrival time, but it is usually 
close enough.
 .
 .P
 \fBSync\fR, \fBCreate\fR, \fBRemove\fR, \fBExpunge\fR, \fBExpungeSolo\fR,
-\fBMaxMessages\fR, \fBExpireUnread\fR, and \fBCopyArrivalDate\fR
+\fBMaxMessages\fR, \fBExpireUnread\fR, \fBExpireSide\fR,
+and \fBCopyArrivalDate\fR
 can be used before any section for a global effect.
 The global settings are overridden by Channel-specific options,
 which in turn are overridden by command line switches.
diff --git a/src/run-tests.pl b/src/run-tests.pl
index 9786e037..144ba946 100755
--- a/src/run-tests.pl
+++ b/src/run-tests.pl
@@ -266,12 +266,28 @@ sub parse_chan($;$)
        }
 
        $$ss{max_pulled} = resolv_msg($$ics[0], $cs, "far");
-       $$ss{max_expired} = resolv_msg($$ics[1], $cs, "far");
+       $$ss{max_expired_far} = resolv_msg($$ics[1], $cs, "far");
        $$ss{max_pushed} = resolv_msg($$ics[2], $cs, "near");
+       $$ss{max_expired_near} = 0;
 
        return $cs;
 }
 
+sub flip_chan($)
+{
+       my ($cs) = @_;
+
+       ($$cs{far}, $$cs{near}) = ($$cs{near}, $$cs{far});
+       ($$cs{far_trash}, $$cs{near_trash}) = ($$cs{near_trash}, 
$$cs{far_trash});
+       my $ss = $$cs{state};
+       ($$ss{max_pulled}, $$ss{max_pushed}) = ($$ss{max_pushed}, 
$$ss{max_pulled});
+       ($$ss{max_expired_far}, $$ss{max_expired_near}) = 
($$ss{max_expired_near}, $$ss{max_expired_far});
+       for my $ent (@{$$ss{entries}}) {
+               ($$ent[0], $$ent[1]) = ($$ent[1], $$ent[0]);
+               $$ent[2] =~ tr/<>/></;
+       }
+}
+
 
 sub qm($)
 {
@@ -442,8 +458,9 @@ sub readstate(;$)
        my @ents;
        my %ss = (
                max_pulled => 0,
-               max_expired => 0,
+               max_expired_far => 0,
                max_pushed => 0,
+               max_expired_near => 0,
                entries => \@ents
        );
        my ($far_val, $near_val) = (0, 0);
@@ -452,7 +469,8 @@ sub readstate(;$)
                'NearUidValidity' => \$near_val,
                'MaxPulledUid' => \$ss{max_pulled},
                'MaxPushedUid' => \$ss{max_pushed},
-               'MaxExpiredFarUid' => \$ss{max_expired}
+               'MaxExpiredFarUid' => \$ss{max_expired_far},
+               'MaxExpiredNearUid' => \$ss{max_expired_near}
        );
        OUTER: while (1) {
                while (@$ls) {
@@ -473,6 +491,7 @@ sub readstate(;$)
                return;
        }
        delete $hdr{'MaxExpiredFarUid'};  # optional field
+       delete $hdr{'MaxExpiredNearUid'};  # ditto
        my @ky = keys %hdr;
        if (@ky) {
                print STDERR "Keys missing from sync state header: @ky\n";
@@ -537,8 +556,12 @@ sub mkstate($)
        open(FILE, ">", "near/.mbsyncstate") or
                die "Cannot create sync state.\n";
        print FILE "FarUidValidity 1\nMaxPulledUid ".$$ss{max_pulled}."\n".
-                  "NearUidValidity 1\nMaxExpiredFarUid ".$$ss{max_expired}.
-                  "\nMaxPushedUid ".$$ss{max_pushed}."\n\n";
+                  "NearUidValidity 1\nMaxPushedUid ".$$ss{max_pushed}."\n";
+       print FILE "MaxExpiredFarUid ".$$ss{max_expired_far}."\n"
+               if ($$ss{max_expired_far});
+       print FILE "MaxExpiredNearUid ".$$ss{max_expired_near}."\n"
+               if ($$ss{max_expired_near});
+       print FILE "\n";
        for my $ent (@{$$ss{entries}}) {
                print FILE $$ent[0]." ".$$ent[1]." ".$$ent[2]."\n";
        }
@@ -646,8 +669,9 @@ sub cmpstate($$)
        return 0 if ($ss == $ref_ss);
        my $ret = 0;
        for my $h (['MaxPulledUid', 'max_pulled'],
-                  ['MaxExpiredFarUid', 'max_expired'],
-                  ['MaxPushedUid', 'max_pushed']) {
+                  ['MaxExpiredFarUid', 'max_expired_far'],
+                  ['MaxPushedUid', 'max_pushed'],
+                  ['MaxExpiredNearUid', 'max_expired_near']) {
                my ($hn, $sn) = @$h;
                my ($got, $want) = ($$ss{$sn}, $$ref_ss{$sn});
                if ($got != $want) {
@@ -735,7 +759,8 @@ sub printstate($)
        my ($ss) = @_;
 
        return if (!$ss);
-       print " [ ".$$ss{max_pulled}.", ".$$ss{max_expired}.", 
".$$ss{max_pushed}.",\n   ";
+       print " [ ".$$ss{max_pulled}.", ".$$ss{max_expired_far}.", ".
+                       $$ss{max_pushed}.", ".$$ss{max_expired_near}.",\n   ";
        my $frst = 1;
        for my $ent (@{$$ss{entries}}) {
                if ($frst) {
@@ -884,10 +909,10 @@ sub test_impl($$$$)
        }
 }
 
-# $title, \@source_state, \@target_state, \@channel_configs
-sub test($$$$)
+# $title, \@source_state, \@target_state, \@channel_configs, $flip_sides
+sub test($$$$;$)
 {
-       my ($ttl, $isx, $itx, $sfx) = @_;
+       my ($ttl, $isx, $itx, $sfx, $flip) = @_;
 
        if (@match) {
                if ($start) {
@@ -899,10 +924,16 @@ sub test($$$$)
        }
 
        print "Testing: ".$ttl." ...\n";
+       # We don't flip the Store configs, as inverting the Channel config
+       # would be unreasonably complex.
        writecfg($sfx);
 
        my $sx = parse_chan($isx);
        my $tx = parse_chan($itx, $sx);
+       if ($flip) {
+               flip_chan($sx);
+               flip_chan($tx);
+       }
 
        test_impl(0, $sx, $tx, $sfx);
        test_impl(1, $sx, $tx, $sfx);
@@ -1263,6 +1294,9 @@ my @X33 = (
 );
 test("max messages + expire - full", \@x33, \@X33, \@O31);
 
+my @O31a = ("", "", "MaxMessages 3\nExpireSide Far\n");
+test("max messages + expire far - full", \@x33, \@X33, \@O31a, 1);
+
 my @O34 = ("", "", "Sync New\nMaxMessages 3\n");
 my @X34 = (
   I, F, I,
@@ -1465,6 +1499,9 @@ my @X61 = (
 );
 test("maxuid topping", \@x60, \@X61, \@O61);
 
+my @O62 = ("", "", "Sync Flags\nExpireSide Far");
+test("maxuid topping (expire far)", \@x60, \@X61, \@O62, 1);
+
 # Tests for refreshing previously skipped/failed/expired messages.
 # We don't know the flags at the time of the (hypothetical) previous
 # sync, so we can't know why a particular message is missing.
@@ -1688,6 +1725,24 @@ my @X82a = (
 );
 test("weird old + expire + expunge near", \@x80a, \@X82a, \@O72a);
 
+my @O82b = ("", "", "Sync New Old\nMaxMessages 3\nExpireUnread yes\nExpireSide 
Far\nExpunge Far\n");
+my @X82b = (
+  U, L, D,
+  E, "", "/", "/",
+  F, "", ">", "",
+  G, "", "", "/",
+  L, "", "/", "",
+  N, "", "/", "/",
+  O, "", ">", "",
+  P, "", "", "/",
+  T, "", "", "/",
+  D, "", "*F", "*F",
+  H, "*", "*", "",
+  Q, "*", "*", "",
+  U, "*", "*", "",
+);
+test("weird old + expire far + expunge far", \@x80a, \@X82b, \@O82b, 1);
+
 my @X83 = (
   S, L, Q,
   E, "", "<", "",
@@ -1816,6 +1871,24 @@ my @X13 = (
 );
 test("trash new remotely", \@x10, \@X13, \@O13);
 
+my @O14 = ("Trash far_trash\n", "",
+           "Sync Flags Gone\nMaxMessages 20\nExpireUnread yes\nExpireSide 
Far\nMaxSize 1k\nExpunge Both\n");
+my @X14 = (
+  K, A, K,
+  A, "", "/", "/",
+  B, "/", "/", "",
+  C, "/", "/", "#/",
+  D, "", "/", "#/",
+  E, "/", "/", "",
+  F, "/", "/", "/",
+  G, "/", "/", "#/",
+  H, "/", "/", "/",
+  I, "/", "/", "#/",
+  L, "/", "", "",
+  M, "", "", "#/",
+);
+test("trash near (expire far)", \@x10, \@X14, \@O14, 1);
+
 # Test "mirroring" expunges.
 
 my @xa0 = (
diff --git a/src/sync.c b/src/sync.c
index f0762415..83e63385 100644
--- a/src/sync.c
+++ b/src/sync.c
@@ -625,14 +625,15 @@ box_opened2( sync_vars_t *svars, int t )
        // The latter would also apply when the expired box is the source,
        // but it's more natural to treat it as read-only in that case.
        // OP_UPGRADE makes sense only for legacy S_SKIPPED entries.
-       if ((chan->ops[N] & (OP_OLD | OP_NEW | OP_UPGRADE | OP_FLAGS)) && 
chan->max_messages)
+       int xt = chan->expire_side;
+       if ((chan->ops[xt] & (OP_OLD | OP_NEW | OP_UPGRADE | OP_FLAGS)) && 
chan->max_messages)
                svars->any_expiring = 1;
        if (svars->any_expiring) {
-               opts[N] |= OPEN_PAIRED | OPEN_FLAGS;
-               if (any_dummies[N])
-                       opts[F] |= OPEN_PAIRED | OPEN_FLAGS;
-               else if (chan->ops[N] & (OP_OLD | OP_NEW | OP_UPGRADE))
-                       opts[F] |= OPEN_FLAGS;
+               opts[xt] |= OPEN_PAIRED | OPEN_FLAGS;
+               if (any_dummies[xt])
+                       opts[xt^1] |= OPEN_PAIRED | OPEN_FLAGS;
+               else if (chan->ops[xt] & (OP_OLD | OP_NEW | OP_UPGRADE))
+                       opts[xt^1] |= OPEN_FLAGS;
        }
        for (t = 0; t < 2; t++) {
                svars->opts[t] = svars->drv[t]->prepare_load_box( ctx[t], 
opts[t] );
@@ -651,36 +652,37 @@ box_opened2( sync_vars_t *svars, int t )
        }
 
        ARRAY_INIT( &mexcs );
-       if ((svars->opts[F] & OPEN_PAIRED) && !(svars->opts[F] & OPEN_OLD) && 
chan->max_messages) {
-               /* When messages have been expired on the near side, the far 
side fetch is split into
+       if ((svars->opts[xt^1] & OPEN_PAIRED) && !(svars->opts[xt^1] & 
OPEN_OLD) && chan->max_messages) {
+               /* When messages have been expired on one side, the other 
side's fetch is split into
                 * two ranges: The bulk fetch which corresponds with the most 
recent messages, and an
                 * exception list of messages which would have been expired if 
they weren't important. */
-               debug( "preparing far side selection - max expired far uid is 
%u\n", svars->maxxfuid );
+               debug( "preparing %s selection - max expired %s uid is %u\n",
+                      str_fn[xt^1], str_fn[xt^1], svars->maxxfuid );
                /* First, find out the lower bound for the bulk fetch. */
                minwuid = svars->maxxfuid + 1;
                /* Next, calculate the exception fetch. */
                for (srec = svars->srecs; srec; srec = srec->next) {
                        if (srec->status & S_DEAD)
                                continue;
-                       if (!srec->uid[F])
+                       if (!srec->uid[xt^1])
                                continue;  // No message; other state is 
irrelevant
-                       if (srec->uid[F] >= minwuid)
+                       if (srec->uid[xt^1] >= minwuid)
                                continue;  // Message is in non-expired range
-                       if ((svars->opts[F] & OPEN_NEW) && srec->uid[F] > 
svars->maxuid[F])
+                       if ((svars->opts[xt^1] & OPEN_NEW) && srec->uid[xt^1] > 
svars->maxuid[xt^1])
                                continue;  // Message is in expired range, but 
new range overlaps that
-                       if (!srec->uid[N] && !(srec->status & S_PENDING))
+                       if (!srec->uid[xt] && !(srec->status & S_PENDING))
                                continue;  // Only actually paired up messages 
matter
                        // The pair is alive, but outside the bulk range
-                       *uint_array_append( &mexcs ) = srec->uid[F];
+                       *uint_array_append( &mexcs ) = srec->uid[xt^1];
                }
                sort_uint_array( mexcs.array );
        } else {
                minwuid = 1;
        }
        sync_ref( svars );
-       load_box( svars, F, minwuid, mexcs.array );
+       load_box( svars, xt^1, minwuid, mexcs.array );
        if (!check_cancel( svars ))
-               load_box( svars, N, 1, (uint_array_t){ NULL, 0 } );
+               load_box( svars, xt, 1, (uint_array_t){ NULL, 0 } );
        sync_deref( svars );
 }
 
@@ -747,6 +749,16 @@ cmp_srec_far( const void *a, const void *b )
        return au > bu ? 1 : -1;  // Can't subtract, the result might not fit 
into signed int.
 }
 
+static int
+cmp_srec_near( const void *a, const void *b )
+{
+       uint au = (*(const alive_srec_t *)a).srec->uid[N];
+       uint bu = (*(const alive_srec_t *)b).srec->uid[N];
+       assert( au && bu );
+       assert( au != bu );
+       return au > bu ? 1 : -1;  // Can't subtract, the result might not fit 
into signed int.
+}
+
 typedef struct {
        void *aux;
        sync_rec_t *srec;
@@ -877,6 +889,7 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
        svars->oldmaxuid[N] = svars->newmaxuid[N];
 
        info( "Synchronizing...\n" );
+       int xt = svars->chan->expire_side;
        for (t = 0; t < 2; t++)
                svars->good_flags[t] = 
(uchar)svars->drv[t]->get_supported_flags( svars->ctx[t] );
 
@@ -953,15 +966,16 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                                } else if (del[t^1]) {
                                        // The source was newly expunged, so 
possibly propagate the deletion.
                                        // The target may be in an unknown 
state (not fetched).
-                                       if ((t == F) && (srec->status & 
(S_EXPIRE|S_EXPIRED))) {
+                                       if ((t != xt) && (srec->status & 
(S_EXPIRE | S_EXPIRED))) {
                                                /* Don't propagate deletion 
resulting from expiration. */
                                                if (~srec->status & (S_EXPIRE | 
S_EXPIRED)) {
                                                        // An expiration was 
interrupted, but the message was expunged since.
                                                        srec->status |= 
S_EXPIRE | S_EXPIRED;  // Override failed unexpiration attempts.
                                                        JLOG( "~ %u %u %u", 
(srec->uid[F], srec->uid[N], srec->status), "forced expiration commit" );
                                                }
-                                               JLOG( "> %u %u 0", 
(srec->uid[F], srec->uid[N]), "near side expired, orphaning far side" );
-                                               srec->uid[N] = 0;
+                                               JLOG( "%c %u %u 0", ("<>"[xt], 
srec->uid[F], srec->uid[N]),
+                                                     "%s expired, orphaning 
%s", (str_fn[xt], str_fn[xt^1]) );
+                                               srec->uid[xt] = 0;
                                        } else {
                                                if (srec->msg[t] && 
(srec->msg[t]->status & M_FLAGS) &&
                                                    // Ignore deleted flag, as 
that's what we'll change ourselves ...
@@ -988,7 +1002,7 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                                  doflags:
                                        if (svars->chan->ops[t] & OP_FLAGS) {
                                                sflags = sanitize_flags( 
sflags, svars, t );
-                                               if ((t == F) && (srec->status & 
(S_EXPIRE|S_EXPIRED))) {
+                                               if ((t != xt) && (srec->status 
& (S_EXPIRE | S_EXPIRED))) {
                                                        /* Don't propagate 
deletion resulting from expiration. */
                                                        debug( "  near side 
expiring\n" );
                                                        sflags &= ~F_DELETED;
@@ -1054,7 +1068,7 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                                                        // debug( "not 
re-propagating orphaned message %u\n", tmsg->uid );
                                                        continue;
                                                }
-                                               if (t == F || !(srec->status & 
S_EXPIRED)) {
+                                               if (t != xt || !(srec->status & 
S_EXPIRED)) {
                                                        // Orphans are 
essentially deletion propagation transactions which
                                                        // were interrupted 
midway through by not expunging the target. We
                                                        // don't re-propagate 
these, as it would be illogical, and also
@@ -1100,7 +1114,7 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                                // The 1st unknown message which should be 
known marks the end
                                // of the synced range; more known messages may 
follow (from an
                                // unidirectional sync in the opposite 
direction).
-                               if (t == F || tmsg->uid > svars->maxxfuid)
+                               if (t != xt || tmsg->uid > svars->maxxfuid)
                                        topping = 0;
 
                                const char *what;
@@ -1163,50 +1177,50 @@ box_loaded( int sts, message_t *msgs, int total_msgs, 
int recent_msgs, void *aux
        }
 
        if (svars->any_expiring) {
-               // Note: When this branch is entered, we have loaded all near 
side messages.
                /* Expire excess messages. Important (flagged, unread, or 
unpropagated) messages
                 * older than the first not expired message are not counted 
towards the total. */
+               // Note: When this branch is entered, we have loaded all 
expire-side messages.
                debug( "preparing message expiration\n" );
                alive_srec_t *arecs = nfmalloc( sizeof(*arecs) * svars->nsrecs 
);
                int alive = 0;
                for (srec = svars->srecs; srec; srec = srec->next) {
                        if (srec->status & S_DEAD)
                                continue;
-                       // We completely ignore unpaired near-side messages, as 
we cannot expire
+                       // We completely ignore unpaired expire-side messages, 
as we cannot expire
                        // them without data loss; consequently, we also don't 
count them.
-                       // Note that we also ignore near-side messages we're 
currently propagating,
+                       // Note that we also ignore expire-side messages we're 
currently propagating,
                        // which delays expiration of some messages by one 
cycle. Otherwise, we'd
                        // have to sequence flag updating after message 
propagation to avoid a race
                        // with external expunging, and that seems unreasonably 
expensive.
-                       if (!srec->uid[F])
+                       if (!srec->uid[xt^1])
                                continue;
                        if (!(srec->status & S_PENDING)) {
                                // We ignore unpaired far-side messages, as 
there is obviously nothing
                                // to expire in the first place.
-                               if (!srec->msg[N])
+                               if (!srec->msg[xt])
                                        continue;
-                               nflags = srec->msg[N]->flags;
-                               if (srec->status & S_DUMMY(N)) {
-                                       if (!srec->msg[F])
+                               nflags = srec->msg[xt]->flags;
+                               if (srec->status & S_DUMMY(xt)) {
+                                       if (!srec->msg[xt^1])
                                                continue;
                                        // We need to pull in the real Flagged 
and Seen even if flag
                                        // propagation was not requested, as 
the placeholder's ones are
                                        // useless (except for un-seeing).
                                        // This results in the somewhat weird 
situation that messages
                                        // which are not visibly flagged remain 
unexpired.
-                                       sflags = srec->msg[F]->flags;
+                                       sflags = srec->msg[xt^1]->flags;
                                        aflags = (sflags & ~srec->flags) & 
(F_SEEN | F_FLAGGED);
                                        dflags = (~sflags & srec->flags) & 
F_SEEN;
                                        nflags = (nflags & (~(F_SEEN | 
F_FLAGGED) | (srec->flags & F_SEEN)) & ~dflags) | aflags;
                                }
-                               nflags = (nflags | srec->aflags[N]) & 
~srec->dflags[N];
+                               nflags = (nflags | srec->aflags[xt]) & 
~srec->dflags[xt];
                        } else {
                                if (srec->status & S_UPGRADE) {
                                        // The dummy's F & S flags are mostly 
masked out anyway,
                                        // but we may be pulling in the real 
ones.
-                                       nflags = (srec->pflags | 
srec->aflags[N]) & ~srec->dflags[N];
+                                       nflags = (srec->pflags | 
srec->aflags[xt]) & ~srec->dflags[xt];
                                } else {
-                                       nflags = srec->msg[F]->flags;
+                                       nflags = srec->msg[xt^1]->flags;
                                }
                        }
                        if (!(nflags & F_DELETED) || (srec->status & (S_EXPIRE 
| S_EXPIRED))) {
@@ -1216,7 +1230,7 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                }
                // Sort such that the messages which have been in the
                // complete store longest expire first.
-               qsort( arecs, alive, sizeof(*arecs), cmp_srec_far );
+               qsort( arecs, alive, sizeof(*arecs), (xt == F) ? cmp_srec_near 
: cmp_srec_far );
                int todel = alive - svars->chan->max_messages;
                debug( "%d alive messages, %d excess - expiring\n", alive, 
todel );
                int unseen = 0;
@@ -1230,7 +1244,7 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                                todel--;
                        } else if (todel > 0 ||
                                   ((srec->status & (S_EXPIRE | S_EXPIRED)) == 
(S_EXPIRE | S_EXPIRED)) ||
-                                  ((srec->status & (S_EXPIRE | S_EXPIRED)) && 
(srec->msg[N]->flags & F_DELETED))) {
+                                  ((srec->status & (S_EXPIRE | S_EXPIRED)) && 
(srec->msg[xt]->flags & F_DELETED))) {
                                /* The message is excess or was already (being) 
expired. */
                                srec->status |= S_NEXPIRE;
                                debug( "  expiring pair(%u,%u)\n", 
srec->uid[F], srec->uid[N] );
@@ -1241,7 +1255,7 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                if (svars->chan->expire_unread < 0 && unseen * 2 > 
svars->chan->max_messages) {
                        error( "%s: %d unread messages in excess of MaxMessages 
(%d).\n"
                               "Please set ExpireUnread to decide outcome. 
Skipping mailbox.\n",
-                              svars->orig_name[N], unseen, 
svars->chan->max_messages );
+                              svars->orig_name[xt], unseen, 
svars->chan->max_messages );
                        svars->ret |= SYNC_FAIL;
                        cancel_sync( svars );
                        return;
@@ -1272,9 +1286,9 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                                        // If we have so many new messages that 
some of them are instantly expired,
                                        // but some are still propagated 
because they are important, we need to
                                        // ensure explicitly that the bulk 
fetch limit is upped.
-                                       if (svars->maxxfuid < srec->uid[F])
-                                               svars->maxxfuid = srec->uid[F];
-                                       srec->msg[F]->srec = NULL;
+                                       if (svars->maxxfuid < srec->uid[xt^1])
+                                               svars->maxxfuid = 
srec->uid[xt^1];
+                                       srec->msg[xt^1]->srec = NULL;
                                }
                        }
                }
@@ -1307,7 +1321,7 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int 
recent_msgs, void *aux
                                }
                        } else {
                                /* The trigger is an expiration transaction 
being ongoing ... */
-                               if ((t == N) && ((shifted_bit(srec->status, 
S_EXPIRE, S_EXPIRED) ^ srec->status) & S_EXPIRED)) {
+                               if ((t == xt) && ((shifted_bit(srec->status, 
S_EXPIRE, S_EXPIRED) ^ srec->status) & S_EXPIRED)) {
                                        // ... but the actual action derives 
from the wanted state -
                                        // so that canceled transactions are 
rolled back as well.
                                        if (srec->status & S_NEXPIRE)
@@ -1529,14 +1543,14 @@ flags_set_p2( sync_vars_t *svars, sync_rec_t *srec, int 
t )
                              (str_hl[t], fmt_lone_flags( nflags ).str, 
fmt_lone_flags( srec->flags ).str) );
                        srec->flags = nflags;
                }
-               if (t == N) {
+               if (t == svars->chan->expire_side) {
                        uchar ex = (srec->status / S_EXPIRE) & 1;
                        uchar exd = (srec->status / S_EXPIRED) & 1;
                        if (ex != exd) {
                                uchar nex = (srec->status / S_NEXPIRE) & 1;
                                if (nex == ex) {
-                                       if (nex && svars->maxxfuid < 
srec->uid[F])
-                                               svars->maxxfuid = srec->uid[F];
+                                       if (nex && svars->maxxfuid < 
srec->uid[t^1])
+                                               svars->maxxfuid = 
srec->uid[t^1];
                                        srec->status = (srec->status & 
~S_EXPIRED) | (nex * S_EXPIRED);
                                        JLOG( "~ %u %u %d", (srec->uid[F], 
srec->uid[N], srec->status & S_LOGGED),
                                              "expired %d - commit", nex );
@@ -1582,6 +1596,7 @@ msgs_flags_set( sync_vars_t *svars, int t )
                only_solo = 0;
        else
                goto skip;
+       int xt = svars->chan->expire_side;
        int expunge_other = (svars->chan->ops[t^1] & OP_EXPUNGE);
        // Driver-wise, this makes sense only if (svars->opts[t] & 
OPEN_UID_EXPUNGE),
        // but the trashing loop uses the result as well.
@@ -1603,7 +1618,7 @@ msgs_flags_set( sync_vars_t *svars, int t )
                                        debugn( "(orphaned) " );
                                } else if (expunge_other && (srec->status & 
S_DEL(t^1))) {
                                        debugn( "(orphaning) " );
-                               } else if (t == N && (srec->status & (S_EXPIRE 
| S_EXPIRED))) {
+                               } else if (t == xt && (srec->status & (S_EXPIRE 
| S_EXPIRED))) {
                                        // Expiration overrides mirroring, as 
otherwise the combination
                                        // makes no sense at all.
                                        debugn( "(expire) " );
@@ -1644,7 +1659,7 @@ msgs_flags_set( sync_vars_t *svars, int t )
                }
                debugn( "  message %u ", tmsg->uid );
                if ((srec = tmsg->srec)) {
-                       if (t == N && (srec->status & (S_EXPIRE | S_EXPIRED))) {
+                       if (t == xt && (srec->status & (S_EXPIRE | S_EXPIRED))) 
{
                                // Don't trash messages that are deleted only 
due to expiring.
                                // However, this is an unlikely configuration 
to start with ...
                                debug( "is expired\n" );
@@ -1814,12 +1829,17 @@ box_closed_p2( sync_vars_t *svars, int t )
        }
 
        debug( "purging obsolete entries\n" );
+       int xt = svars->chan->expire_side;
        for (srec = svars->srecs; srec; srec = srec->next) {
                if (srec->status & S_DEAD)
                        continue;
-               if (!srec->uid[N] || (srec->status & S_GONE(N))) {
-                       if (!srec->uid[F] || (srec->status & S_GONE(F)) ||
-                               ((srec->status & S_EXPIRED) && svars->maxuid[F] 
>= srec->uid[F] && svars->maxxfuid >= srec->uid[F])) {
+               if ((srec->status & S_EXPIRED) &&
+                   (!srec->uid[xt] || (srec->status & S_GONE(xt))) &&
+                   svars->maxuid[xt^1] >= srec->uid[xt^1] && svars->maxxfuid 
>= srec->uid[xt^1]) {
+                   PC_JLOG( "- %u %u", (srec->uid[F], srec->uid[N]), "killing 
expired" );
+                       srec->status = S_DEAD;
+               } else if (!srec->uid[N] || (srec->status & S_GONE(N))) {
+                       if (!srec->uid[F] || (srec->status & S_GONE(F))) {
                                PC_JLOG( "- %u %u", (srec->uid[F], 
srec->uid[N]), "killing" );
                                srec->status = S_DEAD;
                        } else if (srec->uid[N] && (srec->status & S_DEL(F))) {
diff --git a/src/sync.h b/src/sync.h
index b8d5be41..8e0eed06 100644
--- a/src/sync.h
+++ b/src/sync.h
@@ -57,6 +57,7 @@ typedef struct channel_conf {
        string_list_t *patterns;
        int ops[2];
        int max_messages;  // For near side only.
+       int expire_side;
        signed char expire_unread;
        char use_internal_date;
 } channel_conf_t;
diff --git a/src/sync_p.h b/src/sync_p.h
index 66f09e4b..1c9e7e4e 100644
--- a/src/sync_p.h
+++ b/src/sync_p.h
@@ -11,8 +11,8 @@
 
 BIT_ENUM(
        S_DEAD,         // ephemeral: the entry was killed and should be ignored
-       S_EXPIRE,       // the entry is being expired (near side message 
removal scheduled)
-       S_EXPIRED,      // the entry is expired (near side message removal 
confirmed)
+       S_EXPIRE,       // the entry is being expired (expire-side message 
removal scheduled)
+       S_EXPIRED,      // the entry is expired (expire-side message removal 
confirmed)
        S_NEXPIRE,      // temporary: new expiration state
        S_PENDING,      // the entry is new and awaits propagation (possibly a 
retry)
        S_DUMMY(2),     // f/n message is only a placeholder
@@ -62,7 +62,7 @@ typedef struct {
        uint uidval[2];     // UID validity value
        uint newuidval[2];  // UID validity obtained from driver
        uint finduid[2];    // TUID lookup makes sense only for UIDs >= this
-       uint maxxfuid;      // highest expired UID on far side
+       uint maxxfuid;      // highest expired UID on full side
        uchar good_flags[2], bad_flags[2], can_crlf[2];
 } sync_vars_t;
 
diff --git a/src/sync_state.c b/src/sync_state.c
index 385d089f..5567f100 100644
--- a/src/sync_state.c
+++ b/src/sync_state.c
@@ -126,6 +126,7 @@ load_state( sync_vars_t *svars )
        char fbuf[16];  // enlarge when support for keywords is added
        char buf[128], buf1[64], buf2[64];
 
+       int xt = svars->chan->expire_side;
        if ((jfp = fopen( svars->dname, "r" ))) {
                if (!lock_state( svars ))
                        goto jbail;
@@ -148,6 +149,8 @@ load_state( sync_vars_t *svars )
                                        error( "Error: invalid sync state 
header in %s\n", svars->dname );
                                        goto jbail;
                                }
+                               if (maxxnuid && xt != N)
+                                       goto sidefail;
                                goto gothdr;
                        }
                        uint uid;
@@ -164,8 +167,19 @@ load_state( sync_vars_t *svars )
                        } else if (!strcmp( buf1, "MaxPushedUid" )) {
                                svars->maxuid[N] = uid;
                        } else if (!strcmp( buf1, "MaxExpiredFarUid" ) || 
!strcmp( buf1, "MaxExpiredMasterUid" ) /* Pre-1.4 legacy */) {
+                               if (xt != N) {
+                                 sidefail:
+                                       error( "Error: state file %s does not 
match ExpireSide setting\n", svars->dname );
+                                       goto jbail;
+                               }
+                               svars->maxxfuid = uid;
+                       } else if (!strcmp( buf1, "MaxExpiredNearUid" )) {
+                               if (xt != F)
+                                       goto sidefail;
                                svars->maxxfuid = uid;
                        } else if (!strcmp( buf1, "MaxExpiredSlaveUid" )) {  // 
Pre-1.3 legacy
+                               if (xt != N)
+                                       goto sidefail;
                                maxxnuid = uid;
                        } else {
                                error( "Error: unrecognized sync state header 
entry at %s:%d\n", svars->dname, line );
@@ -397,8 +411,8 @@ load_state( sync_vars_t *svars )
                                                break;
                                        case '~':
                                                srec->status = (srec->status & 
~S_LOGGED) | t3;
-                                               if ((srec->status & S_EXPIRED) 
&& svars->maxxfuid < srec->uid[F])
-                                                       svars->maxxfuid = 
srec->uid[F];
+                                               if ((srec->status & S_EXPIRED) 
&& svars->maxxfuid < srec->uid[xt^1])
+                                                       svars->maxxfuid = 
srec->uid[xt^1];
                                                debug( "status now %s\n", 
fmt_sts( srec->status ).str );
                                                break;
                                        case '_':
@@ -490,7 +504,9 @@ save_state( sync_vars_t *svars )
                 "FarUidValidity %u\nNearUidValidity %u\nMaxPulledUid 
%u\nMaxPushedUid %u\n",
                 svars->uidval[F], svars->uidval[N], svars->maxuid[F], 
svars->maxuid[N] );
        if (svars->maxxfuid)
-               Fprintf( svars->nfp, "MaxExpiredFarUid %u\n", svars->maxxfuid );
+               Fprintf( svars->nfp,
+                        svars->chan->expire_side == N ? "MaxExpiredFarUid 
%u\n" : "MaxExpiredNearUid %u\n",
+                        svars->maxxfuid );
        Fprintf( svars->nfp, "\n" );
        for (sync_rec_t *srec = svars->srecs; srec; srec = srec->next) {
                if (srec->status & S_DEAD)


_______________________________________________
isync-devel mailing list
isync-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/isync-devel

Reply via email to