From 1225f0b86b7b7bdab3d4c9a03d3198ff90b0925a Mon Sep 17 00:00:00 2001 From: Oswald Buddenhagen Date: Wed, 20 Apr 2022 12:19:37 +0200 Subject: [PATCH] add ExpungeSolo option REFMAIL: CAOgBZNonT0s0b_yPs2vx81Ru3cQp5M93xpZ3syWBW-2CNoX_ow@mail.gmail.com --- NEWS | 2 + src/config.c | 1 + src/driver.h | 1 + src/drv_imap.c | 4 +- src/drv_maildir.c | 2 +- src/main.c | 19 +++++++-- src/main_sync.c | 14 ++++++- src/mbsync.1 | 16 +++++++- src/run-tests.pl | 100 ++++++++++++++++++++++++++++++++++++++++++++++ src/sync.c | 69 ++++++++++++++++++++++++++++---- src/sync.h | 3 ++ 11 files changed, 214 insertions(+), 17 deletions(-) diff --git a/NEWS b/NEWS index 3b9ef74..9faa482 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,8 @@ A proper summary is now printed prior to exiting. Added new sync operation 'Old'. +Added support for mirroring deletions more accurately. + [1.4.0] The 'isync' compatibility wrapper was removed. diff --git a/src/config.c b/src/config.c index 240c4cd..d7b4ff6 100644 --- a/src/config.c +++ b/src/config.c @@ -174,6 +174,7 @@ static const struct { const char *name; } boxOps[] = { { OP_EXPUNGE, "Expunge" }, + { OP_EXPUNGE_SOLO, "ExpungeSolo" }, { OP_CREATE, "Create" }, { OP_REMOVE, "Remove" }, }; diff --git a/src/driver.h b/src/driver.h index 7abb594..d3068bc 100644 --- a/src/driver.h +++ b/src/driver.h @@ -54,6 +54,7 @@ flag_str_t ATTR_OPTIMIZE /* force RVO */ fmt_lone_flags( uchar flags ); BIT_ENUM( M_RECENT, // unsyncable flag; maildir_*() depend on this being bit 0 M_DEAD, // expunged + M_EXPUNGE, // for driver_t->close_box() M_FLAGS, // flags are valid // The following are only for IMAP FETCH response parsing M_DATE, diff --git a/src/drv_imap.c b/src/drv_imap.c index 55192ff..3dc8c53 100644 --- a/src/drv_imap.c +++ b/src/drv_imap.c @@ -3122,7 +3122,7 @@ imap_close_box( store_t *gctx, for (msg = ctx->msgs.head; ; ) { for (bl = 0; msg && bl < 960; msg = msg->next) { - if ((msg->status & M_DEAD) || !(msg->flags & F_DELETED)) + if ((msg->status & M_DEAD) || !(msg->status & M_EXPUNGE)) continue; if (bl) buf[bl++] = ','; @@ -3136,7 +3136,7 @@ imap_close_box( store_t *gctx, } else { if (nmsg->seq > 1) break; - if (!(nmsg->flags & F_DELETED)) + if (!(nmsg->flags & M_EXPUNGE)) break; } } diff --git a/src/drv_maildir.c b/src/drv_maildir.c index d87ba3a..b904913 100644 --- a/src/drv_maildir.c +++ b/src/drv_maildir.c @@ -1806,7 +1806,7 @@ maildir_close_box( store_t *gctx, retry = 0; basel = nfsnprintf( buf, sizeof(buf), "%s/", ctx->path ); for (msg = ctx->msgs; msg; msg = msg->next) { - if (!(msg->status & M_DEAD) && (msg->flags & F_DELETED)) { + if (!(msg->status & M_DEAD) && (msg->status & M_EXPUNGE)) { nfsnprintf( buf + basel, _POSIX_PATH_MAX - basel, "%s/%s", subdirs[msg->status & M_RECENT], msg->base ); if (unlink( buf )) { if (errno == ENOENT) diff --git a/src/main.c b/src/main.c index 8381a3c..37304fa 100644 --- a/src/main.c +++ b/src/main.c @@ -42,7 +42,8 @@ PACKAGE " " VERSION " - mailbox synchronizer\n" " -H, --push propagate from near to far side\n" " -C, --create propagate creations of mailboxes\n" " -R, --remove propagate deletions of mailboxes\n" -" -X, --expunge expunge deleted messages\n" +" -X, --expunge expunge deleted messages\n" +" -x, --expunge-solo expunge deleted messages that are not paired\n" " -c, --config CONFIG read an alternate config file (default: ~/." EXE "rc)\n" " -D, --debug debugging modes (see manual)\n" " -V, --verbose display what is happening\n" @@ -52,7 +53,8 @@ PACKAGE " " VERSION " - mailbox synchronizer\n" "\nIf neither --pull nor --push are specified, both are active.\n" "If neither --new, --gone, --flags, nor --upgrade are specified, all are\n" "active. Direction and operation can be concatenated like --pull-new, etc.\n" -"--create, --remove, and --expunge can be suffixed with -far/-near.\n" +"--create, --remove, --expunge, and --expunge-solo can be suffixed with" +"-far/-near.\n" "See the man page for details.\n" "\nSupported mailbox formats are: IMAP4rev1, Maildir\n" "\nCompile time options:\n" @@ -235,15 +237,21 @@ main( int argc, char **argv ) mvars->ops[N] |= op, ms_warn = 1; else goto badopt; - mvars->ops[F] |= op & (XOP_HAVE_CREATE | XOP_HAVE_REMOVE | XOP_HAVE_EXPUNGE); + mvars->ops[F] |= op & (XOP_HAVE_CREATE | XOP_HAVE_REMOVE | XOP_HAVE_EXPUNGE | XOP_HAVE_EXPUNGE_SOLO); } else if (starts_with( opt, -1, "remove", 6 )) { opt += 6; op = OP_REMOVE|XOP_HAVE_REMOVE; goto lcop; + } else if (starts_with( opt, -1, "expunge-solo", 12 )) { + opt += 12; + op = OP_EXPUNGE_SOLO | XOP_HAVE_EXPUNGE_SOLO; + goto lcop; } else if (starts_with( opt, -1, "expunge", 7 )) { opt += 7; op = OP_EXPUNGE|XOP_HAVE_EXPUNGE; goto lcop; + } else if (!strcmp( opt, "no-expunge-solo" )) { + mvars->ops[F] |= XOP_EXPUNGE_SOLO_NOOP | XOP_HAVE_EXPUNGE_SOLO; } else if (!strcmp( opt, "no-expunge" )) { mvars->ops[F] |= XOP_EXPUNGE_NOOP | XOP_HAVE_EXPUNGE; } else if (!strcmp( opt, "no-create" )) { @@ -340,11 +348,14 @@ main( int argc, char **argv ) ochar++; else cops |= op; - mvars->ops[F] |= op & (XOP_HAVE_CREATE | XOP_HAVE_REMOVE | XOP_HAVE_EXPUNGE); + mvars->ops[F] |= op & (XOP_HAVE_CREATE | XOP_HAVE_REMOVE | XOP_HAVE_EXPUNGE | XOP_HAVE_EXPUNGE_SOLO); break; case 'R': op = OP_REMOVE|XOP_HAVE_REMOVE; goto cop; + case 'x': + op = OP_EXPUNGE_SOLO | XOP_HAVE_EXPUNGE_SOLO; + goto cop; case 'X': op = OP_EXPUNGE|XOP_HAVE_EXPUNGE; goto cop; diff --git a/src/main_sync.c b/src/main_sync.c index 14c04d7..564a8a8 100644 --- a/src/main_sync.c +++ b/src/main_sync.c @@ -186,13 +186,20 @@ add_channel( chan_ent_t ***chanapp, channel_conf_t *chan, int ops[] ) merge_actions( chan, ops, XOP_HAVE_CREATE, OP_CREATE, 0 ); merge_actions( chan, ops, XOP_HAVE_REMOVE, OP_REMOVE, 0 ); merge_actions( chan, ops, XOP_HAVE_EXPUNGE, OP_EXPUNGE, 0 ); + merge_actions( chan, ops, XOP_HAVE_EXPUNGE_SOLO, OP_EXPUNGE_SOLO, 0 ); debug( "channel ops (%s):\n far: %s\n near: %s\n", chan->name, fmt_ops( ops[F] ).str, fmt_ops( ops[N] ).str ); for (int t = 0; t < 2; t++) { + if (!(~ops[t] & (OP_EXPUNGE | OP_EXPUNGE_SOLO))) { + error( "Specified both Expunge and ExpungeSolo for %s of Channel '%s'.\n", + str_fn[t], chan->stores[t]->name ); + free( ce ); + return NULL; + } if (chan->ops[t] & OP_MASK_TYPE) ops_any[t] = 1; - if ((chan->ops[t] & OP_EXPUNGE) && + if ((chan->ops[t] & (OP_EXPUNGE | OP_EXPUNGE_SOLO)) && (chan->stores[t]->trash || (chan->stores[t^1]->trash && chan->stores[t^1]->trash_remote_new))) trash_any[t] = 1; @@ -253,6 +260,8 @@ add_named_channel( chan_ent_t ***chanapp, char *channame, int ops[] ) } chan_ent_t *ce = add_channel( chanapp, chan, ops ); + if (!ce) + return NULL; ce->boxes = boxes; ce->boxlist = boxlist; return ce; @@ -297,7 +306,8 @@ sync_chans( core_vars_t *cvars, char **argv ) if (cvars->all) { for (channel_conf_t *chan = channels; chan; chan = chan->next) { - add_channel( &chanapp, chan, cvars->ops ); + if (!add_channel( &chanapp, chan, cvars->ops )) + cvars->ret = 1; if (!chan->patterns) boxes_total++; } diff --git a/src/mbsync.1 b/src/mbsync.1 index d362b2b..dd0ad98 100644 --- a/src/mbsync.1 +++ b/src/mbsync.1 @@ -659,10 +659,24 @@ Note that for safety, non-empty mailboxes are never deleted. \fBExpunge\fR {\fBNone\fR|\fBFar\fR|\fBNear\fR|\fBBoth\fR} Permanently remove all messages [on the far/near side] which are marked for deletion. +Mutually exclusive with \fBExpungeSolo\fR for the same side. See \fBRECOMMENDATIONS\fR below. (Global default: \fBNone\fR) . .TP +\fBExpungeSolo\fR {\fBNone\fR|\fBFar\fR|\fBNear\fR|\fBBoth\fR} +Permanently remove all messages [on the far/near side] which are both +marked for deletion and have no corresponding message in the opposite +Store. +Together with \fBSync Gone\fR, this allows actual mirroring of +expunges. Note, however, that this makes sense only if nothing else +expunges the other messages which are marked for deletion. +Also note that this does not work for IMAP Stores which do not support +the UIDPLUS extension. +Mutually exclusive with \fBExpunge\fR for the same side. +(Global default: \fBNone\fR) +. +.TP \fBCopyArrivalDate\fR {\fByes\fR|\fBno\fR} Selects whether their arrival time should be propagated together with the messages. @@ -673,7 +687,7 @@ date\fR) is actually the arrival time, but it is usually close enough. (Global default: \fBno\fR) . .P -\fBSync\fR, \fBCreate\fR, \fBRemove\fR, \fBExpunge\fR, +\fBSync\fR, \fBCreate\fR, \fBRemove\fR, \fBExpunge\fR, \fBExpungeSolo\fR, \fBMaxMessages\fR, \fBExpireUnread\fR, and \fBCopyArrivalDate\fR can be used before any section for a global effect. The global settings are overridden by Channel-specific options, diff --git a/src/run-tests.pl b/src/run-tests.pl index 3978e35..9786e03 100755 --- a/src/run-tests.pl +++ b/src/run-tests.pl @@ -1816,4 +1816,104 @@ my @X13 = ( ); test("trash new remotely", \@x10, \@X13, \@O13); +# Test "mirroring" expunges. + +my @xa0 = ( + M, 0, M, + # pair + A, "*", "*", "*", + # expire + B, "*", "*", "*S", + # expire with del + C, "*T", "*", "*S", + # pair flag del + D, "*T", "*", "*", + E, "*", "*", "*T", + # pair flag undel + F, "*", "*T", "*T", + G, "*T", "*T", "*", + # pair gone + H, "_", "*", "*", + I, "*", "*", "_", + # upgrade + J, "**", "*>", "*F?", + K, "*F?", "*<", "**", + # doomed upgrade + L, "*T*", "*>", "*F?", + M, "*F?", "*<", "*T*", + # doomed new + N, "", "", "*T", + O, "*T", "", "", +); + +my @Oa1 = ("", "", "ExpungeSolo Both\nMaxMessages 1\nExpireUnread false\n"); +my @Xa1 = ( + N, B, O, + B, "+S", "/", "/", + C, "+S", "+ST", "+T", # This is weird, but it's not worth handling. + D, "", "+T", "+T", + E, "+T", "+T", "", + F, "", "-T", "-T", + G, "-T", "-T", "", + H, "", "/", "/", + I, "/", "/", "", + J, "", ">->", "^*", + J, "", "", "&1/", + K, "^*", "<-<", "", + K, "&1/", "", "", + L, "", ">->+T", "^*T", + L, "", "", "&1/", + M, "^*T", "<-<+T", "", + M, "&1/", "", "", + N, "*T", "*T", "", + O, "", "*T", "*T", +); +test("expunge solo both", \@xa0, \@Xa1, \@Oa1); + +my @Oa2 = ("", "", "ExpungeSolo Near\nMaxMessages 1\nExpireUnread false\n"); +my @Xa2 = ( + N, B, O, + B, "+S", "/", "/", + C, "+S", "+ST", "+T", # As above. + D, "", "+T", "+T", + E, "+T", "+T", "", + F, "", "-T", "-T", + G, "-T", "-T", "", + H, "", "/", "/", + I, "+T", ">", "", + J, "", ">->", "^*", + J, "", "", "&1/", + K, "^*", "<-<", "", + K, "&1+T", "^", "|", + L, "", ">->+T", "^*T", + L, "", "", "&1/", + M, "^*T", "<-<+T", "", + M, "&1+T", "^", "|", + N, "*T", "*T", "", + O, "", "*T", "*T", +); +test("expunge solo near", \@xa0, \@Xa2, \@Oa2); + +my @Oa3 = ("", "", "Expunge Far\nExpungeSolo Near\nMaxMessages 1\nExpireUnread false\n"); +my @Xa3 = ( + K, B, J, + B, "+S", "/", "/", + C, "/", "/", "/", + D, "/", "/", "/", + E, "/", "/", "/", + F, "", "-T", "-T", + G, "-T", "-T", "", + H, "", "/", "/", + I, "/", "/", "", + J, "", ">->", "^*", + J, "", "", "&1/", + K, "^*", "<-<", "", + K, "&1/", "", "", + L, "/", "/", "/", + M, "/", "/", "/", + N, "", "", "/", + O, "/", "", "", +); +test("expunge far & solo near", \@xa0, \@Xa3, \@Oa3); + print "OK.\n"; diff --git a/src/sync.c b/src/sync.c index b851867..d37dfc0 100644 --- a/src/sync.c +++ b/src/sync.c @@ -788,9 +788,12 @@ box_opened2( sync_vars_t *svars, int t ) if ((chan->ops[t] | chan->ops[t^1]) & OP_EXPUNGE) // Don't propagate doomed msgs opts[t^1] |= OPEN_FLAGS; } - if (chan->ops[t] & OP_EXPUNGE) { + if (chan->ops[t] & (OP_EXPUNGE | OP_EXPUNGE_SOLO)) { opts[t] |= OPEN_EXPUNGE; - if (chan->stores[t]->trash) { + if (chan->ops[t] & OP_EXPUNGE_SOLO) { + opts[t] |= OPEN_OLD | OPEN_NEW | OPEN_FLAGS | OPEN_UID_EXPUNGE; + opts[t^1] |= OPEN_OLD; + } else if (chan->stores[t]->trash) { if (!chan->stores[t]->trash_only_new) opts[t] |= OPEN_OLD; opts[t] |= OPEN_NEW | OPEN_FLAGS | OPEN_UID_EXPUNGE; @@ -816,6 +819,11 @@ box_opened2( sync_vars_t *svars, int t ) for (t = 0; t < 2; t++) { svars->opts[t] = svars->drv[t]->prepare_load_box( ctx[t], opts[t] ); if (opts[t] & ~svars->opts[t] & OPEN_UID_EXPUNGE) { + if (chan->ops[t] & OP_EXPUNGE_SOLO) { + error( "Error: Store %s does not support ExpungeSolo.\n", + svars->chan->stores[t]->name ); + goto bail; + } if (!ctx[t]->racy_trash) { ctx[t]->racy_trash = 1; notice( "Notice: Trashing in Store %s is prone to race conditions.\n", @@ -1490,7 +1498,8 @@ box_loaded( int sts, message_t *msgs, int total_msgs, int recent_msgs, void *aux dflags |= F_DELETED; } } - if ((svars->chan->ops[t] & OP_EXPUNGE) && (((srec->msg[t] ? srec->msg[t]->flags : 0) | aflags) & ~dflags & F_DELETED) && + if ((svars->chan->ops[t] & OP_EXPUNGE) && + (((srec->msg[t] ? srec->msg[t]->flags : 0) | aflags) & ~dflags & F_DELETED) && (!svars->ctx[t]->conf->trash || svars->ctx[t]->conf->trash_only_new)) { /* If the message is going to be expunged, don't propagate anything but the deletion. */ @@ -1748,8 +1757,54 @@ msgs_flags_set( sync_vars_t *svars, int t ) if (check_cancel( svars )) goto out; - if (!(svars->chan->ops[t] & OP_EXPUNGE)) + int only_solo; + if (svars->chan->ops[t] & OP_EXPUNGE_SOLO) + only_solo = 1; + else if (svars->chan->ops[t] & OP_EXPUNGE) + only_solo = 0; + else goto skip; + 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. + debug( "preparing expunge of %s on %s, %sexpunging %s\n", + only_solo ? "solo" : "all", str_fn[t], expunge_other ? "" : "NOT ", str_fn[t^1] ); + for (tmsg = svars->msgs[t]; tmsg; tmsg = tmsg->next) { + if (tmsg->status & M_DEAD) + continue; + if (!(tmsg->flags & F_DELETED)) { + //debug( " message %u is not deleted\n", tmsg->uid ); // Too noisy + continue; + } + debugn( " message %u ", tmsg->uid ); + if (only_solo) { + if ((srec = tmsg->srec)) { + if (!srec->uid[t^1]) { + debugn( "(solo) " ); + } else if (srec->status & S_GONE(t^1)) { + debugn( "(orphaned) " ); + } else if (expunge_other && (srec->status & S_DEL(t^1))) { + debugn( "(orphaning) " ); + } else if (t == N && (srec->status & (S_EXPIRE | S_EXPIRED))) { + // Expiration overrides mirroring, as otherwise the combination + // makes no sense at all. + debugn( "(expire) " ); + } else { + debug( "is not solo\n" ); + continue; + } + if (srec->status & S_PENDING) { + debug( "is being paired\n" ); + continue; + } + } else { + debugn( "(isolated) " ); + } + } + debug( "- expunging\n" ); + tmsg->status |= M_EXPUNGE; + } + int remote, only_new; if (svars->ctx[t]->conf->trash) { only_new = svars->ctx[t]->conf->trash_only_new; @@ -1765,8 +1820,8 @@ msgs_flags_set( sync_vars_t *svars, int t ) for (tmsg = svars->msgs[t]; tmsg; tmsg = tmsg->next) { if (tmsg->status & M_DEAD) continue; - if (!(tmsg->flags & F_DELETED)) { - //debug( " message %u is not deleted\n", tmsg->uid ); // Too noisy + if (!(tmsg->status & M_EXPUNGE)) { + //debug( " message %u is not being expunged\n", tmsg->uid ); // Too noisy continue; } debugn( " message %u ", tmsg->uid ); @@ -1881,7 +1936,7 @@ sync_close( sync_vars_t *svars, int t ) return; svars->state[t] |= ST_CLOSING; - if ((svars->chan->ops[t] & OP_EXPUNGE) && !(DFlags & FAKEEXPUNGE) + if ((svars->chan->ops[t] & (OP_EXPUNGE | OP_EXPUNGE_SOLO)) && !(DFlags & FAKEEXPUNGE) /*&& !(svars->state[t] & ST_TRASH_BAD)*/) { debug( "expunging %s\n", str_fn[t] ); svars->drv[t]->close_box( svars->ctx[t], box_closed, AUX ); diff --git a/src/sync.h b/src/sync.h index 27a719f..b8d5be4 100644 --- a/src/sync.h +++ b/src/sync.h @@ -21,6 +21,7 @@ BIT_ENUM( OP_GONE, OP_FLAGS, OP_EXPUNGE, + OP_EXPUNGE_SOLO, OP_CREATE, OP_REMOVE, @@ -29,12 +30,14 @@ BIT_ENUM( XOP_HAVE_TYPE, // Aka mode; have at least one of dir and type (see below) // The following must all have the same bit shift from the corresponding OP_* flags. XOP_HAVE_EXPUNGE, + XOP_HAVE_EXPUNGE_SOLO, XOP_HAVE_CREATE, XOP_HAVE_REMOVE, // ... until here. XOP_TYPE_NOOP, // ... and here again from scratch. XOP_EXPUNGE_NOOP, + XOP_EXPUNGE_SOLO_NOOP, XOP_CREATE_NOOP, XOP_REMOVE_NOOP, )