Coverage Report

Created: 2026-06-09 06:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib-storage/index/index-attachment.c
Line
Count
Source
1
/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */
2
3
#include "lib.h"
4
#include "safe-mkstemp.h"
5
#include "istream.h"
6
#include "ostream.h"
7
#include "base64.h"
8
#include "hash-format.h"
9
#include "str.h"
10
#include "message-parser.h"
11
#include "rfc822-parser.h"
12
#include "fs-api.h"
13
#include "istream-fs-file.h"
14
#include "istream-attachment-connector.h"
15
#include "istream-attachment-extractor.h"
16
#include "mail-user.h"
17
#include "index-mail.h"
18
#include "index-attachment.h"
19
20
enum mail_attachment_decode_option {
21
  MAIL_ATTACHMENT_DECODE_OPTION_NONE = '-',
22
  MAIL_ATTACHMENT_DECODE_OPTION_BASE64 = 'B',
23
  MAIL_ATTACHMENT_DECODE_OPTION_CRLF = 'C'
24
};
25
26
struct mail_save_attachment {
27
  pool_t pool;
28
  struct fs *fs;
29
  struct istream *input;
30
31
  struct fs_file *cur_file;
32
  ARRAY_TYPE(mail_attachment_extref) extrefs;
33
};
34
35
static bool index_attachment_want(const struct istream_attachment_header *hdr,
36
          void *context)
37
0
{
38
0
  struct mail_save_context *ctx = context;
39
0
  struct mail_attachment_part apart;
40
41
0
  i_zero(&apart);
42
0
  apart.part = hdr->part;
43
0
  apart.content_type = hdr->content_type;
44
0
  apart.content_disposition = hdr->content_disposition;
45
46
0
  if (ctx->part_is_attachment != NULL)
47
0
    return ctx->part_is_attachment(ctx, &apart);
48
49
  /* don't treat text/ parts as attachments */
50
0
  return hdr->content_type != NULL &&
51
0
    !str_begins_icase_with(hdr->content_type, "text/");
52
0
}
53
54
static int index_attachment_open_temp_fd(void *context)
55
0
{
56
0
  struct mail_save_context *ctx = context;
57
0
  struct mail_storage *storage = ctx->transaction->box->storage;
58
0
  string_t *temp_path;
59
0
  int fd;
60
61
0
  temp_path = t_str_new(256);
62
0
  mail_user_set_get_temp_prefix(temp_path, storage->user->set);
63
0
  fd = safe_mkstemp_hostpid(temp_path, 0600, (uid_t)-1, (gid_t)-1);
64
0
  if (fd == -1) {
65
0
    mailbox_set_critical(ctx->transaction->box,
66
0
      "safe_mkstemp(%s) failed: %m", str_c(temp_path));
67
0
    return -1;
68
0
  }
69
0
  if (unlink(str_c(temp_path)) < 0) {
70
0
    mailbox_set_critical(ctx->transaction->box,
71
0
      "unlink(%s) failed: %m", str_c(temp_path));
72
0
    i_close_fd(&fd);
73
0
    return -1;
74
0
  }
75
0
  return fd;
76
0
}
77
78
static int
79
index_attachment_open_ostream(struct istream_attachment_info *info,
80
            struct ostream **output_r,
81
            const char **error_r ATTR_UNUSED, void *context)
82
0
{
83
0
  struct mail_save_context *ctx = context;
84
0
  struct mail_save_attachment *attach = ctx->data.attach;
85
0
  struct mail_storage *storage = ctx->transaction->box->storage;
86
0
  struct mail_attachment_extref *extref;
87
0
  enum fs_open_flags flags = 0;
88
0
  const char *path, *digest = info->hash;
89
0
  guid_128_t guid_128;
90
91
0
  i_assert(attach->cur_file == NULL);
92
93
0
  if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER)
94
0
    flags |= FS_OPEN_FLAG_FSYNC;
95
96
0
  if (strlen(digest) < 4) {
97
    /* make sure we can access first 4 bytes without accessing
98
       out of bounds memory */
99
0
    digest = t_strconcat(digest, "\0\0\0\0", NULL);
100
0
  }
101
102
0
  guid_128_generate(guid_128);
103
0
  path = t_strdup_printf("%s/%c%c/%c%c/%s-%s",
104
0
             storage->set->mail_ext_attachment_path,
105
0
             digest[0], digest[1],
106
0
             digest[2], digest[3], digest,
107
0
             guid_128_to_string(guid_128));
108
0
  attach->cur_file = fs_file_init(attach->fs, path,
109
0
          FS_OPEN_MODE_REPLACE | flags);
110
111
0
  extref = array_append_space(&attach->extrefs);
112
0
  extref->start_offset = info->start_offset;
113
0
  extref->size = info->encoded_size;
114
0
  extref->path = p_strdup(attach->pool, path +
115
0
      strlen(storage->set->mail_ext_attachment_path) + 1);
116
0
  extref->base64_blocks_per_line = info->base64_blocks_per_line;
117
0
  extref->base64_have_crlf = info->base64_have_crlf;
118
119
0
  *output_r = fs_write_stream(attach->cur_file);
120
0
  return 0;
121
0
}
122
123
static int
124
index_attachment_close_ostream(struct ostream *output, bool success,
125
             const char **error, void *context)
126
0
{
127
0
  struct mail_save_context *ctx = context;
128
0
  struct mail_save_attachment *attach = ctx->data.attach;
129
0
  int ret = success ? 0 : -1;
130
131
0
  i_assert(attach->cur_file != NULL);
132
133
0
  if (ret < 0)
134
0
    fs_write_stream_abort_error(attach->cur_file, &output, "%s", *error);
135
0
  else if (fs_write_stream_finish(attach->cur_file, &output) < 0) {
136
0
    *error = t_strdup_printf("Couldn't create attachment %s: %s",
137
0
           fs_file_path(attach->cur_file),
138
0
           fs_file_last_error(attach->cur_file));
139
0
    ret = -1;
140
0
  }
141
0
  fs_file_deinit(&attach->cur_file);
142
143
0
  if (ret < 0) {
144
0
    array_pop_back(&attach->extrefs);
145
0
  }
146
0
  return ret;
147
0
}
148
149
void index_attachment_save_begin(struct mail_save_context *ctx,
150
         struct fs *fs, struct istream *input)
151
0
{
152
0
  struct mail_storage *storage = ctx->transaction->box->storage;
153
0
  struct mail_save_attachment *attach;
154
0
  struct istream_attachment_settings set;
155
0
  const char *error;
156
0
  pool_t pool;
157
158
0
  i_assert(ctx->data.attach == NULL);
159
160
0
  if (*storage->set->mail_ext_attachment_path == '\0')
161
0
    return;
162
163
0
  i_zero(&set);
164
0
  set.min_size = storage->set->mail_ext_attachment_min_size;
165
0
  if (hash_format_init(storage->set->mail_ext_attachment_hash,
166
0
           &set.hash_format, &error) < 0) {
167
    /* we already checked this when verifying settings */
168
0
    i_panic("mail_attachment_hash=%s unexpectedly failed: %s",
169
0
      storage->set->mail_ext_attachment_hash, error);
170
0
  }
171
0
  set.want_attachment = index_attachment_want;
172
0
  set.open_temp_fd = index_attachment_open_temp_fd;
173
0
  set.open_attachment_ostream = index_attachment_open_ostream;
174
0
  set.close_attachment_ostream = index_attachment_close_ostream;
175
176
0
  pool = pool_alloconly_create("save attachment", 1024);
177
0
  attach = p_new(pool, struct mail_save_attachment, 1);
178
0
  attach->pool = pool;
179
0
  attach->fs = fs;
180
0
  attach->input = i_stream_create_attachment_extractor(input, &set, ctx);
181
0
  p_array_init(&attach->extrefs, attach->pool, 8);
182
0
  ctx->data.attach = attach;
183
0
}
184
185
static int save_check_write_error(struct mail_save_context *ctx,
186
          struct ostream *output)
187
0
{
188
0
  struct mail_storage *storage = ctx->transaction->box->storage;
189
190
0
  if (output->stream_errno == 0)
191
0
    return 0;
192
193
0
  if (!mail_storage_set_error_from_errno(storage)) {
194
0
    mail_set_critical(ctx->dest_mail, "write(%s) failed: %s",
195
0
      o_stream_get_name(output), o_stream_get_error(output));
196
0
  }
197
0
  return -1;
198
0
}
199
200
int index_attachment_save_continue(struct mail_save_context *ctx)
201
0
{
202
0
  struct mail_save_attachment *attach = ctx->data.attach;
203
0
  const unsigned char *data;
204
0
  size_t size;
205
0
  ssize_t ret;
206
207
0
  if (attach->input->stream_errno != 0)
208
0
    return -1;
209
210
0
  do {
211
0
    ret = i_stream_read(attach->input);
212
0
    if (ret > 0 || ret == -2) {
213
0
      data = i_stream_get_data(attach->input, &size);
214
0
      o_stream_nsend(ctx->data.output, data, size);
215
0
      i_stream_skip(attach->input, size);
216
0
    }
217
0
    index_mail_cache_parse_continue(ctx->dest_mail);
218
0
    if (ret == 0 && !i_stream_attachment_extractor_can_retry(attach->input)) {
219
      /* need more input */
220
0
      return 0;
221
0
    }
222
0
  } while (ret != -1);
223
224
0
  if (attach->input->stream_errno != 0) {
225
0
    mail_set_critical(ctx->dest_mail, "read(%s) failed: %s",
226
0
            i_stream_get_name(attach->input),
227
0
            i_stream_get_error(attach->input));
228
0
    return -1;
229
0
  }
230
0
  if (ctx->data.output != NULL) {
231
0
    if (save_check_write_error(ctx, ctx->data.output) < 0)
232
0
      return -1;
233
0
  }
234
0
  return 0;
235
0
}
236
237
int index_attachment_save_finish(struct mail_save_context *ctx)
238
0
{
239
0
  struct mail_save_attachment *attach = ctx->data.attach;
240
241
0
  (void)i_stream_read(attach->input);
242
0
  i_assert(attach->input->eof);
243
0
  return attach->input->stream_errno == 0 ? 0 : -1;
244
0
}
245
246
void index_attachment_save_free(struct mail_save_context *ctx)
247
0
{
248
0
  struct mail_save_attachment *attach = ctx->data.attach;
249
250
0
  if (attach != NULL) {
251
0
    i_stream_unref(&attach->input);
252
0
    pool_unref(&attach->pool);
253
0
    ctx->data.attach = NULL;
254
0
  }
255
0
}
256
257
const ARRAY_TYPE(mail_attachment_extref) *
258
index_attachment_save_get_extrefs(struct mail_save_context *ctx)
259
0
{
260
0
  return ctx->data.attach == NULL ? NULL :
261
0
    &ctx->data.attach->extrefs;
262
0
}
263
264
static int
265
index_attachment_delete_real(struct mail_storage *storage,
266
           struct fs *fs, const char *name)
267
0
{
268
0
  struct fs_file *file;
269
0
  const char *path;
270
0
  int ret;
271
272
0
  path = t_strdup_printf("%s/%s", storage->set->mail_ext_attachment_path, name);
273
0
  file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
274
0
  if ((ret = fs_delete(file)) < 0)
275
0
    mail_storage_set_critical(storage, "%s", fs_file_last_error(file));
276
0
  fs_file_deinit(&file);
277
0
  return ret;
278
0
}
279
280
int index_attachment_delete(struct mail_storage *storage,
281
          struct fs *fs, const char *name)
282
0
{
283
0
  int ret;
284
285
0
  T_BEGIN {
286
0
    ret = index_attachment_delete_real(storage, fs, name);
287
0
  } T_END;
288
0
  return ret;
289
0
}
290
291
void index_attachment_append_extrefs(string_t *str,
292
  const ARRAY_TYPE(mail_attachment_extref) *extrefs)
293
0
{
294
0
  const struct mail_attachment_extref *extref;
295
0
  bool add_space = FALSE;
296
0
  unsigned int startpos;
297
298
0
  array_foreach(extrefs, extref) {
299
0
    if (!add_space)
300
0
      add_space = TRUE;
301
0
    else
302
0
      str_append_c(str, ' ');
303
0
    str_printfa(str, "%"PRIuUOFF_T" %"PRIuUOFF_T" ",
304
0
          extref->start_offset, extref->size);
305
306
0
    startpos = str_len(str);
307
0
    if (extref->base64_have_crlf)
308
0
      str_append_c(str, MAIL_ATTACHMENT_DECODE_OPTION_CRLF);
309
0
    if (extref->base64_blocks_per_line > 0) {
310
0
      str_printfa(str, "%c%u",
311
0
            MAIL_ATTACHMENT_DECODE_OPTION_BASE64,
312
0
            extref->base64_blocks_per_line * 4);
313
0
    }
314
0
    if (startpos == str_len(str)) {
315
      /* make it clear there are no options */
316
0
      str_append_c(str, MAIL_ATTACHMENT_DECODE_OPTION_NONE);
317
0
    }
318
0
    str_append_c(str, ' ');
319
0
    str_append(str, extref->path);
320
0
  }
321
0
}
322
323
static bool
324
parse_extref_decode_options(const char *str,
325
          struct mail_attachment_extref *extref)
326
0
{
327
0
  unsigned int num;
328
329
0
  if (*str == MAIL_ATTACHMENT_DECODE_OPTION_NONE)
330
0
    return str[1] == '\0';
331
332
0
  while (*str != '\0') {
333
0
    switch (*str) {
334
0
    case MAIL_ATTACHMENT_DECODE_OPTION_BASE64:
335
0
      str++; num = 0;
336
0
      while (*str >= '0' && *str <= '9') {
337
0
        num = num*10 + (*str-'0');
338
0
        str++;
339
0
      }
340
0
      if (num == 0 || num % 4 != 0)
341
0
        return FALSE;
342
343
0
      extref->base64_blocks_per_line = num/4;
344
0
      break;
345
0
    case MAIL_ATTACHMENT_DECODE_OPTION_CRLF:
346
0
      extref->base64_have_crlf = TRUE;
347
0
      str++;
348
0
      break;
349
0
    default:
350
0
      return FALSE;
351
0
    }
352
0
  }
353
0
  return TRUE;
354
0
}
355
356
bool index_attachment_parse_extrefs(const char *line, pool_t pool,
357
            ARRAY_TYPE(mail_attachment_extref) *extrefs)
358
0
{
359
0
  struct mail_attachment_extref extref;
360
0
  const char *const *args;
361
0
  unsigned int i, len;
362
0
  uoff_t last_voffset;
363
364
0
  args = t_strsplit(line, " ");
365
0
  len = str_array_length(args);
366
0
  if ((len % 4) != 0)
367
0
    return FALSE;
368
369
0
  last_voffset = 0;
370
0
  for (i = 0; args[i] != NULL; i += 4) {
371
0
    const char *start_offset_str = args[i+0];
372
0
    const char *size_str = args[i+1];
373
0
    const char *decode_options = args[i+2];
374
0
    const char *path = args[i+3];
375
376
0
    i_zero(&extref);
377
0
    if (str_to_uoff(start_offset_str, &extref.start_offset) < 0 ||
378
0
        str_to_uoff(size_str, &extref.size) < 0 ||
379
0
        extref.start_offset < last_voffset ||
380
0
        !parse_extref_decode_options(decode_options, &extref))
381
0
      return FALSE;
382
383
0
    last_voffset += extref.size +
384
0
      (extref.start_offset - last_voffset);
385
386
0
    extref.path = p_strdup(pool, path);
387
0
    array_push_back(extrefs, &extref);
388
0
  }
389
0
  return TRUE;
390
0
}
391
392
uoff_t index_attachment_base64_decoded_size(const struct mail_attachment_extref *extref)
393
0
{
394
0
  uoff_t encoded_size = extref->size;
395
0
  unsigned int nl_size = extref->base64_have_crlf ? 2 : 1;
396
0
  unsigned int line_size = extref->base64_blocks_per_line * 4 + nl_size;
397
0
  uoff_t nl_count = (encoded_size - 1) / line_size;
398
0
  uoff_t nl_bytes = nl_size * nl_count;
399
400
0
  i_assert(extref->base64_blocks_per_line > 0);
401
0
  return MAX_BASE64_DECODED_SIZE(encoded_size - nl_bytes);
402
0
}
403
404
int index_attachment_stream_get(struct fs *fs, const char *attachment_dir,
405
        const char *path_suffix,
406
        struct istream **stream, uoff_t full_size,
407
        const char *ext_refs, const char **error_r)
408
0
{
409
0
  ARRAY_TYPE(mail_attachment_extref) extrefs_arr;
410
0
  const struct mail_attachment_extref *extref;
411
0
  struct istream_attachment_connector *conn;
412
0
  struct istream *input;
413
0
  struct fs_file *file;
414
0
  const char *path;
415
0
  int ret;
416
417
0
  *error_r = NULL;
418
419
0
  t_array_init(&extrefs_arr, 16);
420
0
  if (!index_attachment_parse_extrefs(ext_refs, pool_datastack_create(),
421
0
              &extrefs_arr)) {
422
0
    *error_r = "Broken ext-refs string";
423
0
    return -1;
424
0
  }
425
0
  conn = istream_attachment_connector_begin(*stream, full_size);
426
427
0
  array_foreach(&extrefs_arr, extref) {
428
0
    path = t_strdup_printf("%s/%s%s", attachment_dir,
429
0
               extref->path, path_suffix);
430
0
    file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY |
431
0
            FS_OPEN_FLAG_SEEKABLE);
432
0
    uoff_t raw_size;
433
0
    if (extref->base64_blocks_per_line > 0)
434
0
      raw_size = index_attachment_base64_decoded_size(extref);
435
0
    else
436
0
      raw_size = extref->size;
437
0
    fs_set_metadata(file, FS_METADATA_FILE_SIZE, t_strdup_printf("%"PRIuUOFF_T, raw_size));
438
0
    input = i_stream_create_fs_file(&file, IO_BLOCK_SIZE);
439
440
0
    ret = istream_attachment_connector_add(conn, input,
441
0
          extref->start_offset, extref->size,
442
0
          extref->base64_blocks_per_line,
443
0
          extref->base64_have_crlf, error_r);
444
0
    i_stream_unref(&input);
445
0
    if (ret < 0) {
446
0
      istream_attachment_connector_abort(&conn);
447
0
      return -1;
448
0
    }
449
0
  }
450
451
0
  input = istream_attachment_connector_finish(&conn);
452
0
  i_stream_set_name(input, t_strdup_printf(
453
0
    "attachments-connector(%s)", i_stream_get_name(*stream)));
454
0
  i_stream_unref(stream);
455
0
  *stream = input;
456
0
  return 0;
457
0
}