add support for propagating folder deletions

This commit is contained in:
Oswald Buddenhagen 2014-12-29 02:08:48 +01:00
parent a7eddc6ede
commit d9a983add6
10 changed files with 264 additions and 25 deletions

2
NEWS
View File

@ -12,6 +12,8 @@ Support for Windows file systems has been added.
Support for compressed data transfer has been added. Support for compressed data transfer has been added.
Folder deletions can be propagated now.
[1.1.0] [1.1.0]
Support for hierarchical mailboxes in Patterns. Support for hierarchical mailboxes in Patterns.

2
TODO
View File

@ -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. flagging the dummy would fetch the real message. possibly remove --renew.
note that all interaction needs to happen on the slave side probably. 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, don't SELECT boxes unless really needed; in particular not for appending,
and in write-only mode not before changes are made. and in write-only mode not before changes are made.
problem: UIDVALIDITY change detection is delayed, significantly complicating problem: UIDVALIDITY change detection is delayed, significantly complicating

View File

@ -149,6 +149,7 @@ static const struct {
} boxOps[] = { } boxOps[] = {
{ OP_EXPUNGE, "Expunge" }, { OP_EXPUNGE, "Expunge" },
{ OP_CREATE, "Create" }, { OP_CREATE, "Create" },
{ OP_REMOVE, "Remove" },
}; };
static int static int

View File

@ -174,6 +174,19 @@ struct driver {
void (*open_box)( store_t *ctx, void (*open_box)( store_t *ctx,
void (*cb)( int sts, void *aux ), void *aux ); 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_*) /* 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 * will be performed on the mailbox. The driver may extend the set by implicitly
* needed or available operations. */ * needed or available operations. */

View File

@ -2166,6 +2166,55 @@ imap_create_box( store_t *gctx,
free( buf ); 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 *******************/ /******************* imap_load_box *******************/
static void static void
@ -2810,6 +2859,9 @@ struct driver imap_driver = {
imap_select_box, imap_select_box,
imap_create_box, imap_create_box,
imap_open_box, imap_open_box,
imap_confirm_box_empty,
imap_delete_box,
imap_finish_delete_box,
imap_prepare_load_box, imap_prepare_load_box,
imap_load_box, imap_load_box,
imap_fetch_msg, imap_fetch_msg,

View File

@ -1076,6 +1076,73 @@ maildir_create_box( store_t *gctx,
cb( maildir_validate( gctx->path, 1, (maildir_store_t *)gctx ), aux ); 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 static void
maildir_prepare_load_box( store_t *gctx, int opts ) maildir_prepare_load_box( store_t *gctx, int opts )
{ {
@ -1565,6 +1632,9 @@ struct driver maildir_driver = {
maildir_select_box, maildir_select_box,
maildir_create_box, maildir_create_box,
maildir_open_box, maildir_open_box,
maildir_confirm_box_empty,
maildir_delete_box,
maildir_finish_delete_box,
maildir_prepare_load_box, maildir_prepare_load_box,
maildir_load_box, maildir_load_box,
maildir_fetch_msg, maildir_fetch_msg,

View File

@ -299,7 +299,11 @@ main( int argc, char **argv )
mvars->ops[S] |= op; mvars->ops[S] |= op;
else else
goto badopt; 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 )) { } else if (starts_with( opt, -1, "expunge", 7 )) {
opt += 7; opt += 7;
op = OP_EXPUNGE|XOP_HAVE_EXPUNGE; op = OP_EXPUNGE|XOP_HAVE_EXPUNGE;
@ -308,6 +312,8 @@ main( int argc, char **argv )
mvars->ops[M] |= XOP_HAVE_EXPUNGE; mvars->ops[M] |= XOP_HAVE_EXPUNGE;
else if (!strcmp( opt, "no-create" )) else if (!strcmp( opt, "no-create" ))
mvars->ops[M] |= XOP_HAVE_CREATE; mvars->ops[M] |= XOP_HAVE_CREATE;
else if (!strcmp( opt, "no-remove" ))
mvars->ops[M] |= XOP_HAVE_REMOVE;
else if (!strcmp( opt, "full" )) else if (!strcmp( opt, "full" ))
mvars->ops[M] |= XOP_HAVE_TYPE|XOP_PULL|XOP_PUSH; mvars->ops[M] |= XOP_HAVE_TYPE|XOP_PULL|XOP_PUSH;
else if (!strcmp( opt, "noop" )) else if (!strcmp( opt, "noop" ))
@ -386,8 +392,11 @@ main( int argc, char **argv )
ochar++; ochar++;
else else
cops |= op; 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; break;
case 'R':
op = OP_REMOVE|XOP_HAVE_REMOVE;
goto cop;
case 'X': case 'X':
op = OP_EXPUNGE|XOP_HAVE_EXPUNGE; op = OP_EXPUNGE|XOP_HAVE_EXPUNGE;
goto cop; 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_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_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 ); merge_actions( mvars->chan, mvars->ops, XOP_HAVE_EXPUNGE, OP_EXPUNGE, 0 );
mvars->state[M] = mvars->state[S] = ST_FRESH; 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[t] = BOX_PRESENT;
present[1-t] = BOX_ABSENT; present[1-t] = BOX_ABSENT;
mvars->boxes[t] = mbox->next; 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 ))
if (sync_listed_boxes( mvars, mbox, present )) goto syncw;
goto syncw;
} else {
free( mbox );
}
} }
} else { } else {
if (!mvars->list) { if (!mvars->list) {

View File

@ -58,6 +58,9 @@ and exit.
\fB-C\fR[\fBm\fR][\fBs\fR], \fB--create\fR[\fB-master\fR|\fB-slave\fR] \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. Override any \fBCreate\fR options from the config file. See below.
.TP .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] \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. Override any \fBExpunge\fR options from the config file. See below.
.TP .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} \fBCreate\fR {\fINone\fR|\fIMaster\fR|\fISlave\fR|\fIBoth\fR}
Automatically create missing mailboxes [on the Master/Slave]. Automatically create missing mailboxes [on the Master/Slave].
Otherwise print an error message and skip that mailbox pair if a mailbox 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) (Global default: \fINone\fR)
.. ..
.TP .TP
@ -503,7 +519,7 @@ date\fR) is actually the arrival time, but it is usually close enough.
(Default: \fIno\fR) (Default: \fIno\fR)
.. ..
.P .P
\fBSync\fR, \fBCreate\fR, \fBExpunge\fR, \fBSync\fR, \fBCreate\fR, \fBRemove\fR, \fBExpunge\fR,
\fBMaxMessages\fR, and \fBCopyArrivalDate\fR \fBMaxMessages\fR, and \fBCopyArrivalDate\fR
can be used before any section for a global effect. can be used before any section for a global effect.
The global settings are overridden by Channel-specific options, The global settings are overridden by Channel-specific options,

View File

@ -205,6 +205,8 @@ static int check_cancel( sync_vars_t *svars );
#define ST_SELECTED (1<<10) #define ST_SELECTED (1<<10)
#define ST_DID_EXPUNGE (1<<11) #define ST_DID_EXPUNGE (1<<11)
#define ST_CLOSING (1<<12) #define ST_CLOSING (1<<12)
#define ST_CONFIRMED (1<<13)
#define ST_PRESENT (1<<14)
static void static void
@ -923,7 +925,20 @@ load_state( sync_vars_t *svars )
return 1; 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_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_created( int sts, void *aux );
static void box_opened( int sts, void *aux ); static void box_opened( int sts, void *aux );
static void box_opened2( sync_vars_t *svars, int t ); 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++) { for (t = 0; ; t++) {
info( "Opening %s box %s...\n", str_ms[t], svars->orig_name[t] ); info( "Opening %s box %s...\n", str_ms[t], svars->orig_name[t] );
if (present[t] == BOX_ABSENT) if (present[t] == BOX_ABSENT)
box_confirmed( DRV_BOX_BAD, AUX ); box_confirmed2( svars, t );
else else
svars->drv[t]->open_box( ctx[t], box_confirmed, AUX ); svars->drv[t]->open_box( ctx[t], box_confirmed, AUX );
if (t || check_cancel( svars )) if (t || check_cancel( svars ))
@ -1008,16 +1023,80 @@ box_confirmed( int sts, void *aux )
if (check_cancel( svars )) if (check_cancel( svars ))
return; return;
if (sts == DRV_BOX_BAD) { if (sts == DRV_OK)
if (!(svars->chan->ops[t] & OP_CREATE)) { svars->state[t] |= ST_PRESENT;
box_opened( sts, aux ); 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 { } else {
info( "Creating %s %s...\n", str_ms[t], svars->orig_name[t] ); box_opened2( svars, t );
svars->drv[t]->create_box( svars->ctx[t], box_created, AUX );
} }
} else { if (t || check_cancel( svars ))
box_opened2( svars, t ); 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 static void

View File

@ -35,12 +35,14 @@
#define OP_MASK_TYPE (OP_NEW|OP_RENEW|OP_DELETE|OP_FLAGS) /* asserted in the target ops */ #define OP_MASK_TYPE (OP_NEW|OP_RENEW|OP_DELETE|OP_FLAGS) /* asserted in the target ops */
#define OP_EXPUNGE (1<<4) #define OP_EXPUNGE (1<<4)
#define OP_CREATE (1<<5) #define OP_CREATE (1<<5)
#define XOP_PUSH (1<<6) #define OP_REMOVE (1<<6)
#define XOP_PULL (1<<7) #define XOP_PUSH (1<<8)
#define XOP_PULL (1<<9)
#define XOP_MASK_DIR (XOP_PUSH|XOP_PULL) #define XOP_MASK_DIR (XOP_PUSH|XOP_PULL)
#define XOP_HAVE_TYPE (1<<8) #define XOP_HAVE_TYPE (1<<10)
#define XOP_HAVE_EXPUNGE (1<<9) #define XOP_HAVE_EXPUNGE (1<<11)
#define XOP_HAVE_CREATE (1<<10) #define XOP_HAVE_CREATE (1<<12)
#define XOP_HAVE_REMOVE (1<<13)
typedef struct channel_conf { typedef struct channel_conf {
struct channel_conf *next; struct channel_conf *next;