Browse Source

add support for propagating folder deletions

Oswald Buddenhagen 10 năm trước cách đây
mục cha
commit
d9a983add6
10 tập tin đã thay đổi với 264 bổ sung25 xóa
  1. 2 0
      NEWS
  2. 0 2
      TODO
  3. 1 0
      src/config.c
  4. 13 0
      src/driver.h
  5. 52 0
      src/drv_imap.c
  6. 70 0
      src/drv_maildir.c
  7. 14 8
      src/main.c
  8. 18 2
      src/mbsync.1
  9. 87 8
      src/sync.c
  10. 7 5
      src/sync.h

+ 2 - 0
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.

+ 0 - 2
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

+ 1 - 0
src/config.c

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

+ 13 - 0
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. */

+ 52 - 0
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,

+ 70 - 0
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,

+ 14 - 8
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) {

+ 18 - 2
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,

+ 87 - 8
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

+ 7 - 5
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;