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