1 /* Copyright (c) 2004-2018 Dovecot authors, see the included COPYING file */
2 
3 #include "lib.h"
4 #include "array.h"
5 #include "buffer.h"
6 #include "istream.h"
7 #include "ostream.h"
8 #include "str.h"
9 #include "write-full.h"
10 #include "message-parser.h"
11 #include "mbox-storage.h"
12 #include "mbox-sync-private.h"
13 #include "istream-raw-mbox.h"
14 
mbox_move(struct mbox_sync_context * sync_ctx,uoff_t dest,uoff_t source,uoff_t size)15 int mbox_move(struct mbox_sync_context *sync_ctx,
16 	      uoff_t dest, uoff_t source, uoff_t size)
17 {
18 	struct mbox_mailbox *mbox = sync_ctx->mbox;
19 	struct istream *input;
20 	struct ostream *output;
21 	int ret;
22 
23 	i_assert(source > 0 || (dest != 1 && dest != 2));
24 	i_assert(size < OFF_T_MAX);
25 
26 	if (size == 0 || source == dest)
27 		return 0;
28 
29 	i_stream_sync(sync_ctx->input);
30 
31 	output = o_stream_create_fd_file(sync_ctx->write_fd, UOFF_T_MAX, FALSE);
32 	i_stream_seek(sync_ctx->file_input, source);
33 	if (o_stream_seek(output, dest) < 0) {
34 		mbox_ostream_set_syscall_error(sync_ctx->mbox, output,
35 					       "o_stream_seek()");
36 		o_stream_unref(&output);
37 		return -1;
38 	}
39 
40 	/* we're moving data within a file. it really shouldn't be failing at
41 	   this point or we're corrupted. */
42 	input = i_stream_create_limit(sync_ctx->file_input, size);
43 	o_stream_nsend_istream(output, input);
44 	if (input->stream_errno != 0) {
45 		mailbox_set_critical(&mbox->box,
46 			"read() failed with mbox: %s",
47 			i_stream_get_error(input));
48 		ret = -1;
49 	} else if (output->stream_errno != 0) {
50 		mailbox_set_critical(&mbox->box,
51 			"write() failed with mbox: %s",
52 			o_stream_get_error(output));
53 		ret = -1;
54 	} else if (input->v_offset != size) {
55 		mbox_sync_set_critical(sync_ctx,
56 			"mbox_move(%"PRIuUOFF_T", %"PRIuUOFF_T", %"PRIuUOFF_T
57 			") moved only %"PRIuUOFF_T" bytes",
58 			dest, source, size, input->v_offset);
59 		ret = -1;
60 	} else {
61 		ret = 0;
62 	}
63 	i_stream_unref(&input);
64 
65 	mbox_sync_file_updated(sync_ctx, FALSE);
66 	o_stream_destroy(&output);
67 	return ret;
68 }
69 
mbox_fill_space(struct mbox_sync_context * sync_ctx,uoff_t offset,uoff_t size)70 static int mbox_fill_space(struct mbox_sync_context *sync_ctx,
71 			   uoff_t offset, uoff_t size)
72 {
73 	unsigned char space[1024];
74 
75 	memset(space, ' ', sizeof(space));
76 	while (size > sizeof(space)) {
77 		if (pwrite_full(sync_ctx->write_fd, space,
78 				sizeof(space), offset) < 0) {
79 			mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
80 			return -1;
81 		}
82 		size -= sizeof(space);
83 	}
84 
85 	if (pwrite_full(sync_ctx->write_fd, space, size, offset) < 0) {
86 		mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
87 		return -1;
88 	}
89 	mbox_sync_file_updated(sync_ctx, TRUE);
90 	return 0;
91 }
92 
mbox_sync_headers_add_space(struct mbox_sync_mail_context * ctx,size_t size)93 void mbox_sync_headers_add_space(struct mbox_sync_mail_context *ctx,
94 				 size_t size)
95 {
96 	size_t data_size, pos, start_pos;
97 	const unsigned char *data;
98 	void *p;
99 
100 	i_assert(size < SSIZE_T_MAX);
101 
102 	if (ctx->mail.pseudo)
103 		start_pos = ctx->hdr_pos[MBOX_HDR_X_IMAPBASE];
104 	else if (ctx->mail.space > 0) {
105 		/* update the header using the existing offset.
106 		   otherwise we might chose wrong header and just decrease
107 		   the available space */
108 		start_pos = ctx->mail.offset - ctx->hdr_offset;
109 	} else {
110 		/* Append at the end of X-Keywords header,
111 		   or X-UID if it doesn't exist */
112 		start_pos = ctx->hdr_pos[MBOX_HDR_X_KEYWORDS] != SIZE_MAX ?
113 			ctx->hdr_pos[MBOX_HDR_X_KEYWORDS] :
114 			ctx->hdr_pos[MBOX_HDR_X_UID];
115 	}
116 
117 	data = str_data(ctx->header);
118 	data_size = str_len(ctx->header);
119 	i_assert(start_pos < data_size);
120 
121 	for (pos = start_pos; pos < data_size; pos++) {
122 		if (data[pos] == '\n') {
123 			/* possibly continues in next line */
124 			if (pos+1 == data_size || !IS_LWSP(data[pos+1]))
125 				break;
126 			start_pos = pos+1;
127 		} else if (!IS_LWSP(data[pos]) && data[pos] != '\r') {
128 			start_pos = pos+1;
129 		}
130 	}
131 
132 	/* pos points to end of header now, and start_pos to beginning
133 	   of whitespace. */
134 	mbox_sync_move_buffer(ctx, pos, size, 0);
135 
136 	p = buffer_get_space_unsafe(ctx->header, pos, size);
137 	memset(p, ' ', size);
138 
139 	if (ctx->header_first_change > pos)
140 		ctx->header_first_change = pos;
141 	ctx->header_last_change = SIZE_MAX;
142 
143 	ctx->mail.space = (pos - start_pos) + size;
144 	ctx->mail.offset = ctx->hdr_offset;
145 	if (ctx->mail.space > 0)
146 		ctx->mail.offset += start_pos;
147 }
148 
mbox_sync_header_remove_space(struct mbox_sync_mail_context * ctx,size_t start_pos,size_t * size)149 static void mbox_sync_header_remove_space(struct mbox_sync_mail_context *ctx,
150 					  size_t start_pos, size_t *size)
151 {
152 	const unsigned char *data;
153 	size_t data_size, pos, last_line_pos;
154 
155 	/* find the end of the LWSP */
156 	data = str_data(ctx->header);
157 	data_size = str_len(ctx->header);
158 
159 	for (pos = last_line_pos = start_pos; pos < data_size; pos++) {
160 		if (data[pos] == '\n') {
161 			/* possibly continues in next line */
162 			if (pos+1 == data_size || !IS_LWSP(data[pos+1])) {
163 				data_size = pos;
164 				break;
165 			}
166                         last_line_pos = pos+1;
167 		} else if (!IS_LWSP(data[pos]) && data[pos] != '\r') {
168 			start_pos = last_line_pos = pos+1;
169 		}
170 	}
171 
172 	if (start_pos == data_size)
173 		return;
174 
175 	/* and remove what we can */
176 	if (ctx->header_first_change > start_pos)
177 		ctx->header_first_change = start_pos;
178 	ctx->header_last_change = SIZE_MAX;
179 
180 	if (data_size - start_pos <= *size) {
181 		/* remove it all */
182 		mbox_sync_move_buffer(ctx, start_pos, 0, data_size - start_pos);
183 		*size -= data_size - start_pos;
184 		return;
185 	}
186 
187 	/* we have more space than needed. since we're removing from
188 	   the beginning of header instead of end, we don't have to
189 	   worry about multiline-headers. */
190 	mbox_sync_move_buffer(ctx, start_pos, 0, *size);
191 	if (last_line_pos <= start_pos + *size)
192 		last_line_pos = start_pos;
193 	else
194 		last_line_pos -= *size;
195 	data_size -= *size;
196 
197 	*size = 0;
198 
199 	if (ctx->mail.space < (off_t)(data_size - last_line_pos)) {
200 		ctx->mail.space = data_size - last_line_pos;
201 		ctx->mail.offset = ctx->hdr_offset;
202 		if (ctx->mail.space > 0)
203 			ctx->mail.offset += last_line_pos;
204 	}
205 }
206 
mbox_sync_headers_remove_space(struct mbox_sync_mail_context * ctx,size_t size)207 static void mbox_sync_headers_remove_space(struct mbox_sync_mail_context *ctx,
208 					   size_t size)
209 {
210 	static enum header_position space_positions[] = {
211                 MBOX_HDR_X_UID,
212                 MBOX_HDR_X_KEYWORDS,
213                 MBOX_HDR_X_IMAPBASE
214 	};
215         enum header_position pos;
216 	int i;
217 
218 	ctx->mail.space = 0;
219 	ctx->mail.offset = ctx->hdr_offset;
220 
221 	for (i = 0; i < 3 && size > 0; i++) {
222 		pos = space_positions[i];
223 		if (ctx->hdr_pos[pos] != SIZE_MAX) {
224 			mbox_sync_header_remove_space(ctx, ctx->hdr_pos[pos],
225 						      &size);
226 		}
227 	}
228 
229 	/* FIXME: see if we could remove X-Keywords header completely */
230 }
231 
mbox_sync_first_mail_written(struct mbox_sync_mail_context * ctx,uoff_t hdr_offset)232 static void mbox_sync_first_mail_written(struct mbox_sync_mail_context *ctx,
233 					 uoff_t hdr_offset)
234 {
235 	/* we wrote the first mail. update last-uid offset so we can find
236 	   it later */
237 	i_assert(ctx->last_uid_value_start_pos != 0);
238 	i_assert(ctx->hdr_pos[MBOX_HDR_X_IMAPBASE] != SIZE_MAX);
239 
240 	ctx->sync_ctx->base_uid_last_offset = hdr_offset +
241 		ctx->hdr_pos[MBOX_HDR_X_IMAPBASE] +
242 		ctx->last_uid_value_start_pos;
243 
244 	if (ctx->imapbase_updated) {
245 		/* update so a) we don't try to update it later needlessly,
246 		   b) if we do actually update it, we see the correct value */
247 		ctx->sync_ctx->base_uid_last = ctx->last_uid_updated_value;
248 	}
249 }
250 
mbox_sync_try_rewrite(struct mbox_sync_mail_context * ctx,off_t move_diff)251 int mbox_sync_try_rewrite(struct mbox_sync_mail_context *ctx, off_t move_diff)
252 {
253         struct mbox_sync_context *sync_ctx = ctx->sync_ctx;
254 	size_t old_hdr_size, new_hdr_size;
255 
256 	i_assert(sync_ctx->mbox->mbox_lock_type == F_WRLCK);
257 
258 	old_hdr_size = ctx->body_offset - ctx->hdr_offset;
259 	new_hdr_size = str_len(ctx->header);
260 
261 	if (new_hdr_size <= old_hdr_size) {
262 		/* add space. note that we must call add_space() even if we're
263 		   not adding anything so mail.offset gets fixed. */
264 		mbox_sync_headers_add_space(ctx, old_hdr_size - new_hdr_size);
265 	} else if (new_hdr_size > old_hdr_size) {
266 		/* try removing the space where we can */
267 		mbox_sync_headers_remove_space(ctx,
268 					       new_hdr_size - old_hdr_size);
269 		new_hdr_size = str_len(ctx->header);
270 
271 		if (new_hdr_size <= old_hdr_size) {
272 			/* good, we removed enough. */
273 			i_assert(new_hdr_size == old_hdr_size);
274 		} else if (move_diff < 0 &&
275 			   new_hdr_size - old_hdr_size <= (uoff_t)-move_diff) {
276 			/* moving backwards - we can use the extra space from
277 			   it, just update expunged_space accordingly */
278 			i_assert(ctx->mail.space == 0);
279 			i_assert(sync_ctx->expunged_space >=
280 				 (off_t)(new_hdr_size - old_hdr_size));
281 			sync_ctx->expunged_space -= new_hdr_size - old_hdr_size;
282 		} else {
283 			/* couldn't get enough space */
284 			i_assert(ctx->mail.space == 0);
285 			ctx->mail.space =
286 				-(ssize_t)(new_hdr_size - old_hdr_size);
287 			return 0;
288 		}
289 	}
290 
291 	i_assert(ctx->mail.space >= 0);
292 
293 	if (ctx->header_first_change == SIZE_MAX && move_diff == 0) {
294 		/* no changes actually. we get here if index sync record told
295 		   us to do something that was already there */
296 		return 1;
297 	}
298 
299 	if (move_diff != 0) {
300 		/* forget about partial write optimizations */
301 		ctx->header_first_change = 0;
302 		ctx->header_last_change = 0;
303 	}
304 
305 	if (ctx->header_last_change != SIZE_MAX &&
306 	    ctx->header_last_change != 0)
307 		str_truncate(ctx->header, ctx->header_last_change);
308 
309 	if (pwrite_full(sync_ctx->write_fd,
310 			str_data(ctx->header) + ctx->header_first_change,
311 			str_len(ctx->header) - ctx->header_first_change,
312 			(off_t)ctx->hdr_offset + (off_t)ctx->header_first_change +
313 			move_diff) < 0) {
314 		mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
315 		return -1;
316 	}
317 
318 	if (sync_ctx->dest_first_mail &&
319 	    (ctx->imapbase_updated || ctx->sync_ctx->base_uid_last != 0)) {
320 		/* the position might have moved as a result of moving
321 		   whitespace */
322 		mbox_sync_first_mail_written(ctx, (off_t)ctx->hdr_offset + move_diff);
323 	}
324 
325 	mbox_sync_file_updated(sync_ctx, FALSE);
326 	return 1;
327 }
328 
mbox_sync_read_next(struct mbox_sync_context * sync_ctx,struct mbox_sync_mail_context * mail_ctx,struct mbox_sync_mail * mails,uint32_t seq,uint32_t idx,uoff_t expunged_space)329 static int mbox_sync_read_next(struct mbox_sync_context *sync_ctx,
330 			       struct mbox_sync_mail_context *mail_ctx,
331 			       struct mbox_sync_mail *mails,
332 			       uint32_t seq, uint32_t idx,
333 			       uoff_t expunged_space)
334 {
335 	unsigned int first_mail_expunge_extra;
336 	uint32_t orig_next_uid;
337 
338 	i_zero(mail_ctx);
339 	mail_ctx->sync_ctx = sync_ctx;
340 	mail_ctx->seq = seq;
341 	mail_ctx->header = sync_ctx->header;
342 
343 	if (istream_raw_mbox_get_header_offset(sync_ctx->input,
344 					       &mail_ctx->mail.offset) < 0) {
345 		mbox_sync_set_critical(sync_ctx,
346 			"Couldn't get header offset for seq=%u", seq);
347 		return -1;
348 	}
349 	mail_ctx->mail.body_size = mails[idx].body_size;
350 
351 	orig_next_uid = sync_ctx->next_uid;
352 	if (mails[idx].uid != 0) {
353 		/* This will force the UID to be the one that we originally
354 		   assigned to it, regardless of whether it's broken or not in
355 		   the file. */
356 		sync_ctx->next_uid = mails[idx].uid;
357 		sync_ctx->prev_msg_uid = mails[idx].uid - 1;
358 	} else {
359 		/* Pseudo mail shouldn't have X-UID header at all */
360 		i_assert(mails[idx].pseudo);
361 		sync_ctx->prev_msg_uid = 0;
362 	}
363 
364 	first_mail_expunge_extra = 1 +
365 		(sync_ctx->first_mail_crlf_expunged ? 1 : 0);
366 	if (mails[idx].from_offset +
367 	    first_mail_expunge_extra - expunged_space != 0) {
368 		sync_ctx->dest_first_mail = mails[idx].from_offset == 0;
369 	} else {
370 		/* we need to skip over the initial \n (it's already counted in
371 		   expunged_space) */
372 		sync_ctx->dest_first_mail = TRUE;
373 		mails[idx].from_offset += first_mail_expunge_extra;
374 	}
375 
376 	if (mbox_sync_parse_next_mail(sync_ctx->input, mail_ctx) < 0)
377 		return -1;
378 	i_assert(mail_ctx->mail.pseudo == mails[idx].pseudo);
379 
380 	/* set next_uid back before updating the headers. this is important
381 	   if we're updating the first message to make X-IMAP[base] header
382 	   have the correct value. */
383 	sync_ctx->next_uid = orig_next_uid;
384 
385 	if (mails[idx].space != 0) {
386 		if (mails[idx].space < 0) {
387 			/* remove all possible spacing before updating */
388 			mbox_sync_headers_remove_space(mail_ctx, SIZE_MAX);
389 		}
390 		mbox_sync_update_header_from(mail_ctx, &mails[idx]);
391 	} else {
392 		/* updating might just try to add headers and mess up our
393 		   calculations completely. so only add the EOH here. */
394 		if (mail_ctx->have_eoh)
395 			str_append_c(mail_ctx->header, '\n');
396 	}
397 	return 0;
398 }
399 
mbox_sync_read_and_move(struct mbox_sync_context * sync_ctx,struct mbox_sync_mail_context * mail_ctx,struct mbox_sync_mail * mails,uint32_t seq,uint32_t idx,uint32_t padding,off_t move_diff,uoff_t expunged_space,uoff_t end_offset,bool first_nonexpunged)400 static int mbox_sync_read_and_move(struct mbox_sync_context *sync_ctx,
401                                    struct mbox_sync_mail_context *mail_ctx,
402 				   struct mbox_sync_mail *mails,
403 				   uint32_t seq, uint32_t idx, uint32_t padding,
404 				   off_t move_diff, uoff_t expunged_space,
405 				   uoff_t end_offset, bool first_nonexpunged)
406 {
407 	struct mbox_sync_mail_context new_mail_ctx;
408 	uoff_t offset, dest_offset;
409 	size_t need_space;
410 
411 	if (mail_ctx == NULL) {
412 		if (mbox_sync_seek(sync_ctx, mails[idx].from_offset) < 0)
413 			return -1;
414 
415 		if (mbox_sync_read_next(sync_ctx, &new_mail_ctx, mails, seq, idx,
416 					expunged_space) < 0)
417 			return -1;
418 		mail_ctx = &new_mail_ctx;
419 	} else {
420 		i_assert(seq == mail_ctx->seq);
421 		if (mail_ctx->mail.space < 0)
422 			mail_ctx->mail.space = 0;
423 		i_stream_seek(sync_ctx->input, mail_ctx->body_offset);
424 	}
425 
426 	if (mail_ctx->mail.space <= 0) {
427 		need_space = str_len(mail_ctx->header) - mail_ctx->mail.space -
428 			(mail_ctx->body_offset - mail_ctx->hdr_offset);
429 		if (need_space != (uoff_t)-mails[idx].space) {
430 			/* this check works only if we're doing the first
431 			   write, or if the file size was changed externally */
432 			mbox_sync_file_update_ext_modified(sync_ctx);
433 
434 			mbox_sync_set_critical(sync_ctx,
435 				"seq=%u uid=%u uid_broken=%d "
436 				"originally needed %"PRIuUOFF_T
437 				" bytes, now needs %zu bytes",
438 				seq, mails[idx].uid, mails[idx].uid_broken ? 1 : 0,
439 				(uoff_t)-mails[idx].space, need_space);
440 			return -1;
441 		}
442 	}
443 
444 	if (first_nonexpunged && expunged_space > 0) {
445 		/* move From-line (after parsing headers so we don't
446 		   overwrite them) */
447 		i_assert(mails[idx].from_offset >= expunged_space);
448 		if (mbox_move(sync_ctx, mails[idx].from_offset - expunged_space,
449 			      mails[idx].from_offset,
450 			      mails[idx].offset - mails[idx].from_offset) < 0)
451 			return -1;
452 	}
453 
454 	if (mails[idx].space == 0) {
455 		/* don't touch spacing */
456 	} else if (padding < (uoff_t)mail_ctx->mail.space) {
457 		mbox_sync_headers_remove_space(mail_ctx, mail_ctx->mail.space -
458 					       padding);
459 	} else {
460 		mbox_sync_headers_add_space(mail_ctx, padding -
461 					    mail_ctx->mail.space);
462 	}
463 
464 	/* move the body of this message and headers of next message forward,
465 	   then write the headers */
466 	offset = sync_ctx->input->v_offset;
467 	dest_offset = offset + move_diff;
468 	i_assert(offset <= end_offset);
469 	if (mbox_move(sync_ctx, dest_offset, offset, end_offset - offset) < 0)
470 		return -1;
471 
472 	/* the header may actually be moved backwards if there was expunged
473 	   space which we wanted to remove */
474 	i_assert(dest_offset >= str_len(mail_ctx->header));
475 	dest_offset -= str_len(mail_ctx->header);
476 	i_assert(dest_offset >= mails[idx].from_offset - expunged_space);
477 	if (pwrite_full(sync_ctx->write_fd, str_data(mail_ctx->header),
478 			str_len(mail_ctx->header), dest_offset) < 0) {
479 		mbox_set_syscall_error(sync_ctx->mbox, "pwrite_full()");
480 		return -1;
481 	}
482 	mbox_sync_file_updated(sync_ctx, TRUE);
483 
484 	if (sync_ctx->dest_first_mail) {
485 		mbox_sync_first_mail_written(mail_ctx, dest_offset);
486 		sync_ctx->dest_first_mail = FALSE;
487 	}
488 
489 	mails[idx].offset = dest_offset +
490 		(mail_ctx->mail.offset - mail_ctx->hdr_offset);
491 	mails[idx].space = mail_ctx->mail.space;
492 	return 0;
493 }
494 
mbox_sync_rewrite(struct mbox_sync_context * sync_ctx,struct mbox_sync_mail_context * mail_ctx,uoff_t end_offset,off_t move_diff,uoff_t extra_space,uint32_t first_seq,uint32_t last_seq)495 int mbox_sync_rewrite(struct mbox_sync_context *sync_ctx,
496 		      struct mbox_sync_mail_context *mail_ctx,
497 		      uoff_t end_offset, off_t move_diff, uoff_t extra_space,
498 		      uint32_t first_seq, uint32_t last_seq)
499 {
500 	struct mbox_sync_mail *mails;
501 	uoff_t offset, dest_offset, next_end_offset, next_move_diff;
502 	uoff_t start_offset, expunged_space;
503 	uint32_t idx, first_nonexpunged_idx, padding_per_mail;
504 	uint32_t orig_prev_msg_uid;
505 	unsigned int count;
506 	int ret = 0;
507 
508 	i_assert(extra_space < OFF_T_MAX);
509 	i_assert(sync_ctx->mbox->mbox_lock_type == F_WRLCK);
510 
511 	mails = array_get_modifiable(&sync_ctx->mails, &count);
512 	i_assert(count == last_seq - first_seq + 1);
513 
514 	/* if there's expunges in mails[], we would get more correct balancing
515 	   by counting only them here. however, that might make us overwrite
516 	   data which hasn't yet been copied backwards. to avoid too much
517 	   complexity, we just leave all the rest of the extra space to first
518 	   mail */
519 	idx = last_seq - first_seq + 1;
520 	padding_per_mail = extra_space / idx;
521 
522 	/* after expunge the next mail must have been missing space, or we
523 	   would have moved it backwards already */
524 	expunged_space = 0;
525 	start_offset = mails[0].from_offset;
526 	for (first_nonexpunged_idx = 0;; first_nonexpunged_idx++) {
527 		i_assert(first_nonexpunged_idx != idx);
528 		if (!mails[first_nonexpunged_idx].expunged)
529 			break;
530                 expunged_space += mails[first_nonexpunged_idx].space;
531 	}
532 	i_assert(mails[first_nonexpunged_idx].space < 0);
533 
534 	orig_prev_msg_uid = sync_ctx->prev_msg_uid;
535 
536 	/* start moving backwards. */
537 	while (idx > first_nonexpunged_idx) {
538 		idx--;
539 		if (idx == first_nonexpunged_idx) {
540 			/* give the rest of the extra space to first mail.
541 			   we might also have to move the mail backwards to
542 			   fill the expunged space */
543 			padding_per_mail = move_diff + (off_t)expunged_space +
544 				(off_t)mails[idx].space;
545 		}
546 
547 		next_end_offset = mails[idx].offset;
548 
549 		if (mails[idx].space <= 0 && !mails[idx].expunged) {
550 			/* give space to this mail. end_offset is left to
551 			   contain this message's From-line (ie. below we
552 			   move only headers + body). */
553 			bool first_nonexpunged = idx == first_nonexpunged_idx;
554 
555 			next_move_diff = -mails[idx].space;
556 			if (mbox_sync_read_and_move(sync_ctx, mail_ctx, mails,
557 						    first_seq + idx, idx,
558 						    padding_per_mail,
559 						    move_diff, expunged_space,
560 						    end_offset,
561 						    first_nonexpunged) < 0) {
562 				ret = -1;
563 				break;
564 			}
565 			move_diff -= next_move_diff + mails[idx].space;
566 		} else {
567 			/* this mail provides more space. just move it forward
568 			   from the extra space offset and set end_offset to
569 			   point to beginning of extra space. that way the
570 			   header will be moved along with previous mail's
571 			   body.
572 
573 			   if this is expunged mail, we're moving following
574 			   mail's From-line and maybe headers. */
575 			offset = mails[idx].offset + mails[idx].space;
576 			dest_offset = offset + move_diff;
577 			i_assert(offset <= end_offset);
578 			if (mbox_move(sync_ctx, dest_offset, offset,
579 				      end_offset - offset) < 0) {
580 				ret = -1;
581 				break;
582 			}
583 
584 			move_diff += mails[idx].space;
585 			if (!mails[idx].expunged) {
586 				move_diff -= padding_per_mail;
587 				mails[idx].space = padding_per_mail;
588 
589 				if (mbox_fill_space(sync_ctx, move_diff +
590 						    mails[idx].offset,
591 						    padding_per_mail) < 0) {
592 					ret = -1;
593 					break;
594 				}
595 			}
596 			mails[idx].offset += move_diff;
597 		}
598 		mail_ctx = NULL;
599 
600 		i_assert(move_diff >= 0 || idx == first_nonexpunged_idx);
601 		i_assert(next_end_offset <= end_offset);
602 
603 		end_offset = next_end_offset;
604 		mails[idx].from_offset += move_diff;
605 	}
606 
607 	if (ret == 0) {
608 		i_assert(mails[idx].from_offset == start_offset);
609 		i_assert(move_diff + (off_t)expunged_space >= 0);
610 	}
611 
612 	mbox_sync_file_updated(sync_ctx, FALSE);
613 	sync_ctx->prev_msg_uid = orig_prev_msg_uid;
614 	return ret;
615 }
616