From d9a983add6c627f50f3894f938e4e4892476ad22 Mon Sep 17 00:00:00 2001 From: Oswald Buddenhagen Date: Mon, 29 Dec 2014 02:08:48 +0100 Subject: [PATCH] add support for propagating folder deletions --- NEWS | 2 + TODO | 2 - src/config.c | 1 + src/driver.h | 13 +++++++ src/drv_imap.c | 52 ++++++++++++++++++++++++++ src/drv_maildir.c | 70 ++++++++++++++++++++++++++++++++++ src/main.c | 22 +++++++---- src/mbsync.1 | 20 +++++++++- src/sync.c | 95 +++++++++++++++++++++++++++++++++++++++++++---- src/sync.h | 12 +++--- 10 files changed, 264 insertions(+), 25 deletions(-) diff --git a/NEWS b/NEWS index fe8c6f5..5861575 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,8 @@ Support for Windows file systems has been added. Support for compressed data transfer has been added. +Folder deletions can be propagated now. + [1.1.0] Support for hierarchical mailboxes in Patterns. diff --git a/TODO b/TODO index 02d80b4..ec1cf2e 100644 --- a/TODO +++ b/TODO @@ -56,8 +56,6 @@ create dummies describing MIME structure of messages bigger than MaxSize. flagging the dummy would fetch the real message. possibly remove --renew. note that all interaction needs to happen on the slave side probably. -propagate folder deletions. for safety, the target must be empty. - don't SELECT boxes unless really needed; in particular not for appending, and in write-only mode not before changes are made. problem: UIDVALIDITY change detection is delayed, significantly complicating diff --git a/src/config.c b/src/config.c index 25080ed..fb9dc5f 100644 --- a/src/config.c +++ b/src/config.c @@ -149,6 +149,7 @@ static const struct { } boxOps[] = { { OP_EXPUNGE, "Expunge" }, { OP_CREATE, "Create" }, + { OP_REMOVE, "Remove" }, }; static int diff --git a/src/driver.h b/src/driver.h index 217d624..9416686 100644 --- a/src/driver.h +++ b/src/driver.h @@ -174,6 +174,19 @@ struct driver { void (*open_box)( store_t *ctx, void (*cb)( int sts, void *aux ), void *aux ); + /* Confirm that the open mailbox is empty. */ + int (*confirm_box_empty)( store_t *ctx ); + + /* Delete the open mailbox. The mailbox is expected to be empty. + * Subfolders of the mailbox are *not* deleted. + * Some artifacts of the mailbox may remain, but they won't be + * recognized as a mailbox any more. */ + void (*delete_box)( store_t *ctx, + void (*cb)( int sts, void *aux ), void *aux ); + + /* Remove the last artifacts of the open mailbox, as far as possible. */ + int (*finish_delete_box)( store_t *ctx ); + /* Invoked before load_box(), this informs the driver which operations (OP_*) * will be performed on the mailbox. The driver may extend the set by implicitly * needed or available operations. */ diff --git a/src/drv_imap.c b/src/drv_imap.c index 2db2670..b9505dd 100644 --- a/src/drv_imap.c +++ b/src/drv_imap.c @@ -2166,6 +2166,55 @@ imap_create_box( store_t *gctx, free( buf ); } +/******************* imap_delete_box *******************/ + +static int +imap_confirm_box_empty( store_t *gctx ) +{ + return gctx->count ? DRV_BOX_BAD : DRV_OK; +} + +static void imap_delete_box_p2( imap_store_t *, struct imap_cmd *, int ); + +static void +imap_delete_box( store_t *gctx, + void (*cb)( int sts, void *aux ), void *aux ) +{ + imap_store_t *ctx = (imap_store_t *)gctx; + struct imap_cmd_simple *cmd; + + INIT_IMAP_CMD(imap_cmd_simple, cmd, cb, aux) + imap_exec( ctx, &cmd->gen, imap_delete_box_p2, "CLOSE" ); +} + +static void +imap_delete_box_p2( imap_store_t *ctx, struct imap_cmd *gcmd, int response ) +{ + struct imap_cmd_simple *cmdp = (struct imap_cmd_simple *)gcmd; + struct imap_cmd_simple *cmd; + char *buf; + + if (response != RESP_OK) { + imap_done_simple_box( ctx, &cmdp->gen, response ); + return; + } + + if (prepare_box( &buf, ctx ) < 0) { + imap_done_simple_box( ctx, &cmdp->gen, RESP_NO ); + return; + } + INIT_IMAP_CMD(imap_cmd_simple, cmd, cmdp->callback, cmdp->callback_aux) + imap_exec( ctx, &cmd->gen, imap_done_simple_box, + "DELETE \"%\\s\"", buf ); + free( buf ); +} + +static int +imap_finish_delete_box( store_t *gctx ATTR_UNUSED ) +{ + return DRV_OK; +} + /******************* imap_load_box *******************/ static void @@ -2810,6 +2859,9 @@ struct driver imap_driver = { imap_select_box, imap_create_box, imap_open_box, + imap_confirm_box_empty, + imap_delete_box, + imap_finish_delete_box, imap_prepare_load_box, imap_load_box, imap_fetch_msg, diff --git a/src/drv_maildir.c b/src/drv_maildir.c index 0a4ea9f..613590d 100644 --- a/src/drv_maildir.c +++ b/src/drv_maildir.c @@ -1076,6 +1076,73 @@ maildir_create_box( store_t *gctx, cb( maildir_validate( gctx->path, 1, (maildir_store_t *)gctx ), aux ); } +static int +maildir_confirm_box_empty( store_t *gctx ) +{ + maildir_store_t *ctx = (maildir_store_t *)gctx; + msglist_t msglist; + + ctx->nexcs = ctx->minuid = ctx->maxuid = ctx->newuid = 0; + + if (maildir_scan( ctx, &msglist ) != DRV_OK) + return DRV_BOX_BAD; + maildir_free_scan( &msglist ); + return gctx->count ? DRV_BOX_BAD : DRV_OK; +} + +static void +maildir_delete_box( store_t *gctx, + void (*cb)( int sts, void *aux ), void *aux ) +{ + int i, bl, ret = DRV_OK; + struct stat st; + char buf[_POSIX_PATH_MAX]; + + bl = nfsnprintf( buf, sizeof(buf) - 4, "%s/", gctx->path ); + if (stat( buf, &st )) { + if (errno != ENOENT) { + sys_error( "Maildir error: cannot access mailbox '%s'", gctx->path ); + ret = DRV_BOX_BAD; + } + } else if (!S_ISDIR(st.st_mode)) { + error( "Maildir error: '%s' is no valid mailbox\n", gctx->path ); + ret = DRV_BOX_BAD; + } else if ((ret = maildir_clear_tmp( buf, sizeof(buf), bl )) == DRV_OK) { + nfsnprintf( buf + bl, sizeof(buf) - bl, ".uidvalidity" ); + if (unlink( buf ) && errno != ENOENT) + goto badrm; +#ifdef USE_DB + nfsnprintf( buf + bl, sizeof(buf) - bl, ".isyncuidmap.db" ); + if (unlink( buf ) && errno != ENOENT) + goto badrm; +#endif + /* We delete cur/ last, as it is the indicator for a present mailbox. + * That way an interrupted operation can be resumed. */ + for (i = 3; --i >= 0; ) { + memcpy( buf + bl, subdirs[i], 4 ); + if (rmdir( buf ) && errno != ENOENT) { + badrm: + sys_error( "Maildir error: cannot remove '%s'", buf ); + ret = DRV_BOX_BAD; + break; + } + } + } + cb( ret, aux ); +} + +static int +maildir_finish_delete_box( store_t *gctx ) +{ + /* Subfolders are not deleted; the deleted folder is only "stripped of its mailboxness". + * Consequently, the rmdir may legitimately fail. This behavior follows the IMAP spec. */ + if (rmdir( gctx->path ) && errno != ENOENT && errno != ENOTEMPTY) { + sys_error( "Maildir warning: cannot remove '%s'", gctx->path ); + return DRV_BOX_BAD; + } + return DRV_OK; +} + static void maildir_prepare_load_box( store_t *gctx, int opts ) { @@ -1565,6 +1632,9 @@ struct driver maildir_driver = { maildir_select_box, maildir_create_box, maildir_open_box, + maildir_confirm_box_empty, + maildir_delete_box, + maildir_finish_delete_box, maildir_prepare_load_box, maildir_load_box, maildir_fetch_msg, diff --git a/src/main.c b/src/main.c index b93b8c5..6c0b822 100644 --- a/src/main.c +++ b/src/main.c @@ -299,7 +299,11 @@ main( int argc, char **argv ) mvars->ops[S] |= op; else goto badopt; - mvars->ops[M] |= op & (XOP_HAVE_CREATE|XOP_HAVE_EXPUNGE); + mvars->ops[M] |= op & (XOP_HAVE_CREATE|XOP_HAVE_REMOVE|XOP_HAVE_EXPUNGE); + } 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", 7 )) { opt += 7; op = OP_EXPUNGE|XOP_HAVE_EXPUNGE; @@ -308,6 +312,8 @@ main( int argc, char **argv ) mvars->ops[M] |= XOP_HAVE_EXPUNGE; else if (!strcmp( opt, "no-create" )) mvars->ops[M] |= XOP_HAVE_CREATE; + else if (!strcmp( opt, "no-remove" )) + mvars->ops[M] |= XOP_HAVE_REMOVE; else if (!strcmp( opt, "full" )) mvars->ops[M] |= XOP_HAVE_TYPE|XOP_PULL|XOP_PUSH; else if (!strcmp( opt, "noop" )) @@ -386,8 +392,11 @@ main( int argc, char **argv ) ochar++; else cops |= op; - mvars->ops[M] |= op & (XOP_HAVE_CREATE|XOP_HAVE_EXPUNGE); + mvars->ops[M] |= op & (XOP_HAVE_CREATE|XOP_HAVE_REMOVE|XOP_HAVE_EXPUNGE); break; + case 'R': + op = OP_REMOVE|XOP_HAVE_REMOVE; + goto cop; case 'X': op = OP_EXPUNGE|XOP_HAVE_EXPUNGE; goto cop; @@ -589,6 +598,7 @@ sync_chans( main_vars_t *mvars, int ent ) } merge_actions( mvars->chan, mvars->ops, XOP_HAVE_TYPE, OP_MASK_TYPE, OP_MASK_TYPE ); merge_actions( mvars->chan, mvars->ops, XOP_HAVE_CREATE, OP_CREATE, 0 ); + merge_actions( mvars->chan, mvars->ops, XOP_HAVE_REMOVE, OP_REMOVE, 0 ); merge_actions( mvars->chan, mvars->ops, XOP_HAVE_EXPUNGE, OP_EXPUNGE, 0 ); mvars->state[M] = mvars->state[S] = ST_FRESH; @@ -652,12 +662,8 @@ sync_chans( main_vars_t *mvars, int ent ) present[t] = BOX_PRESENT; present[1-t] = BOX_ABSENT; mvars->boxes[t] = mbox->next; - if ((mvars->chan->ops[1-t] & OP_MASK_TYPE) && (mvars->chan->ops[1-t] & OP_CREATE)) { - if (sync_listed_boxes( mvars, mbox, present )) - goto syncw; - } else { - free( mbox ); - } + if (sync_listed_boxes( mvars, mbox, present )) + goto syncw; } } else { if (!mvars->list) { diff --git a/src/mbsync.1 b/src/mbsync.1 index 2f7e7ed..e5ad1fb 100644 --- a/src/mbsync.1 +++ b/src/mbsync.1 @@ -58,6 +58,9 @@ and exit. \fB-C\fR[\fBm\fR][\fBs\fR], \fB--create\fR[\fB-master\fR|\fB-slave\fR] Override any \fBCreate\fR options from the config file. See below. .TP +\fB-R\fR[\fBm\fR][\fBs\fR], \fB--remove\fR[\fB-master\fR|\fB-slave\fR] +Override any \fBRemove\fR options from the config file. See below. +.TP \fB-X\fR[\fBm\fR][\fBs\fR], \fB--expunge\fR[\fB-master\fR|\fB-slave\fR] Override any \fBExpunge\fR options from the config file. See below. .TP @@ -483,7 +486,20 @@ Note that it is not allowed to assert a cell in two ways, e.g. \fBCreate\fR {\fINone\fR|\fIMaster\fR|\fISlave\fR|\fIBoth\fR} Automatically create missing mailboxes [on the Master/Slave]. Otherwise print an error message and skip that mailbox pair if a mailbox -does not exist. +and the corresponding sync state does not exist. +(Global default: \fINone\fR) +.. +.TP +\fBRemove\fR {\fINone\fR|\fIMaster\fR|\fISlave\fR|\fIBoth\fR} +Propagate mailbox deletions [to the Master/Slave]. +Otherwise print an error message and skip that mailbox pair if a mailbox +does not exist but the corresponding sync state does. +.br +For MailDir mailboxes it is sufficient to delete the cur/ subdirectory to +mark them as deleted. This ensures compatibility with \fBSyncState *\fR. +.br +Note that for safety, non-empty mailboxes are never deleted. +.br (Global default: \fINone\fR) .. .TP @@ -503,7 +519,7 @@ date\fR) is actually the arrival time, but it is usually close enough. (Default: \fIno\fR) .. .P -\fBSync\fR, \fBCreate\fR, \fBExpunge\fR, +\fBSync\fR, \fBCreate\fR, \fBRemove\fR, \fBExpunge\fR, \fBMaxMessages\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/sync.c b/src/sync.c index 82d7f40..736f9ca 100644 --- a/src/sync.c +++ b/src/sync.c @@ -205,6 +205,8 @@ static int check_cancel( sync_vars_t *svars ); #define ST_SELECTED (1<<10) #define ST_DID_EXPUNGE (1<<11) #define ST_CLOSING (1<<12) +#define ST_CONFIRMED (1<<13) +#define ST_PRESENT (1<<14) static void @@ -923,7 +925,20 @@ load_state( sync_vars_t *svars ) return 1; } +static void +delete_state( sync_vars_t *svars ) +{ + unlink( svars->nname ); + unlink( svars->jname ); + if (unlink( svars->dname ) || unlink( svars->lname )) { + sys_error( "Error: channel %s: sync state cannot be deleted", svars->chan->name ); + svars->ret = SYNC_FAIL; + } +} + static void box_confirmed( int sts, void *aux ); +static void box_confirmed2( sync_vars_t *svars, int t ); +static void box_deleted( int sts, void *aux ); static void box_created( int sts, void *aux ); static void box_opened( int sts, void *aux ); static void box_opened2( sync_vars_t *svars, int t ); @@ -988,7 +1003,7 @@ sync_boxes( store_t *ctx[], const char *names[], int present[], channel_conf_t * for (t = 0; ; t++) { info( "Opening %s box %s...\n", str_ms[t], svars->orig_name[t] ); if (present[t] == BOX_ABSENT) - box_confirmed( DRV_BOX_BAD, AUX ); + box_confirmed2( svars, t ); else svars->drv[t]->open_box( ctx[t], box_confirmed, AUX ); if (t || check_cancel( svars )) @@ -1008,16 +1023,80 @@ box_confirmed( int sts, void *aux ) if (check_cancel( svars )) return; - if (sts == DRV_BOX_BAD) { - if (!(svars->chan->ops[t] & OP_CREATE)) { - box_opened( sts, aux ); + if (sts == DRV_OK) + svars->state[t] |= ST_PRESENT; + box_confirmed2( svars, t ); +} + +static void +box_confirmed2( sync_vars_t *svars, int t ) +{ + svars->state[t] |= ST_CONFIRMED; + if (!(svars->state[1-t] & ST_CONFIRMED)) + return; + + sync_ref( svars ); + for (t = 0; ; t++) { + if (!(svars->state[t] & ST_PRESENT)) { + if (!(svars->state[1-t] & ST_PRESENT)) { + if (!svars->existing) { + error( "Error: channel %s: both master %s and slave %s cannot be opened.\n", + svars->chan->name, svars->orig_name[M], svars->orig_name[S] ); + bail: + svars->ret = SYNC_FAIL; + } else { + /* This can legitimately happen if a deletion propagation was interrupted. + * We have no place to record this transaction, so we just assume it. + * Of course this bears the danger of clearing the state if both mailboxes + * temorarily cannot be opened for some weird reason (while the stores can). */ + delete_state( svars ); + } + done: + sync_bail( svars ); + break; + } + if (svars->existing) { + if (!(svars->chan->ops[1-t] & OP_REMOVE)) { + error( "Error: channel %s: %s %s cannot be opened.\n", + svars->chan->name, str_ms[t], svars->orig_name[t] ); + goto bail; + } + if (svars->drv[1-t]->confirm_box_empty( svars->ctx[1-t] ) != DRV_OK) { + warn( "Warning: channel %s: %s %s cannot be opened and %s %s not empty.\n", + svars->chan->name, str_ms[t], svars->orig_name[t], str_ms[1-t], svars->orig_name[1-t] ); + goto done; + } + info( "Deleting %s %s...\n", str_ms[1-t], svars->orig_name[1-t] ); + svars->drv[1-t]->delete_box( svars->ctx[1-t], box_deleted, INV_AUX ); + } else { + if (!(svars->chan->ops[t] & OP_CREATE)) { + box_opened( DRV_BOX_BAD, AUX ); + } else { + info( "Creating %s %s...\n", str_ms[t], svars->orig_name[t] ); + svars->drv[t]->create_box( svars->ctx[t], box_created, AUX ); + } + } } else { - info( "Creating %s %s...\n", str_ms[t], svars->orig_name[t] ); - svars->drv[t]->create_box( svars->ctx[t], box_created, AUX ); + box_opened2( svars, t ); } - } else { - box_opened2( svars, t ); + if (t || check_cancel( svars )) + break; } + sync_deref( svars ); +} + +static void +box_deleted( int sts, void *aux ) +{ + DECL_SVARS; + + if (check_ret( sts, aux )) + return; + INIT_SVARS(aux); + + delete_state( svars ); + svars->drv[t]->finish_delete_box( svars->ctx[t] ); + sync_bail( svars ); } static void diff --git a/src/sync.h b/src/sync.h index f3b8039..65a23ce 100644 --- a/src/sync.h +++ b/src/sync.h @@ -35,12 +35,14 @@ #define OP_MASK_TYPE (OP_NEW|OP_RENEW|OP_DELETE|OP_FLAGS) /* asserted in the target ops */ #define OP_EXPUNGE (1<<4) #define OP_CREATE (1<<5) -#define XOP_PUSH (1<<6) -#define XOP_PULL (1<<7) +#define OP_REMOVE (1<<6) +#define XOP_PUSH (1<<8) +#define XOP_PULL (1<<9) #define XOP_MASK_DIR (XOP_PUSH|XOP_PULL) -#define XOP_HAVE_TYPE (1<<8) -#define XOP_HAVE_EXPUNGE (1<<9) -#define XOP_HAVE_CREATE (1<<10) +#define XOP_HAVE_TYPE (1<<10) +#define XOP_HAVE_EXPUNGE (1<<11) +#define XOP_HAVE_CREATE (1<<12) +#define XOP_HAVE_REMOVE (1<<13) typedef struct channel_conf { struct channel_conf *next;