labfy-investigation/src/core/eml_mime_extractor.c

1836 lines
53 KiB
C

/******************************************************************************
* @file eml_mime_extractor.c
* @brief Extraction MIME récursive, bornée et non destructive.
******************************************************************************/
#include "core/eml_mime_extractor.h"
#include "core/file_hash.h"
#include <errno.h>
#include <glib/gstdio.h>
#include <string.h>
#include <unistd.h>
#define EML_MIME_MAX_COLLISIONS 10000U
#define EML_MIME_IO_BLOCK_SIZE 65536U
#define EML_MIME_PREVIEW_BODY_MAX (1024U * 1024U)
typedef struct
{
GHashTable *values;
} EmlMimeHeaders;
typedef struct
{
const char *target_dir;
const char *target_canonical;
GCancellable *cancellable;
EmlMimeResult *result;
guint part_count;
gsize total_decoded_size;
gboolean preview_only;
guint body_rank;
} EmlMimeContext;
static gboolean eml_mime_check_cancelled(
GCancellable *cancellable,
GError **error
)
{
return cancellable != NULL &&
g_cancellable_set_error_if_cancelled(cancellable, error);
}
void eml_attachment_free(EmlAttachment *attachment)
{
if (attachment == NULL)
return;
g_free(attachment->part_index);
g_free(attachment->declared_filename);
g_free(attachment->decoded_filename);
g_free(attachment->sanitized_filename);
g_free(attachment->extracted_path);
g_free(attachment->relative_path);
g_free(attachment->content_type);
g_free(attachment->detected_mime);
g_free(attachment->content_id);
g_free(attachment->normalized_content_id);
g_free(attachment->content_disposition);
g_free(attachment->normalized_disposition);
g_free(attachment->transfer_encoding);
g_free(attachment->extracted_at_utc);
g_free(attachment->sha256);
g_free(attachment);
}
void eml_mime_result_free(EmlMimeResult *result)
{
if (result == NULL)
return;
g_clear_pointer(&result->attachments, g_ptr_array_unref);
g_clear_pointer(&result->warnings, g_ptr_array_unref);
g_free(result->body_text);
g_free(result);
}
static void eml_mime_warn(
EmlMimeContext *context,
const char *part_index,
const char *message
)
{
g_ptr_array_add(
context->result->warnings,
g_strdup_printf(
"Partie MIME %s : %s",
part_index != NULL ? part_index : "1",
message
)
);
}
static EmlMimeHeaders *eml_mime_headers_new(void)
{
EmlMimeHeaders *headers = g_new0(EmlMimeHeaders, 1);
headers->values = g_hash_table_new_full(
g_str_hash,
g_str_equal,
g_free,
g_free
);
return headers;
}
static void eml_mime_headers_free(EmlMimeHeaders *headers)
{
if (headers == NULL)
return;
g_hash_table_unref(headers->values);
g_free(headers);
}
static const char *eml_mime_headers_get(
const EmlMimeHeaders *headers,
const char *name
)
{
char *key = NULL;
const char *value = NULL;
if (headers == NULL || name == NULL)
return NULL;
key = g_ascii_strdown(name, -1);
value = g_hash_table_lookup(headers->values, key);
g_free(key);
return value;
}
static gboolean eml_mime_headers_store(
EmlMimeHeaders *headers,
const char *name,
const char *value,
GError **error
)
{
char *key = NULL;
char *safe_value = NULL;
if (strlen(value) > EML_MIME_MAX_HEADER_VALUE_LENGTH)
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Une valeur d'en-tête MIME dépasse la limite autorisée."
);
return FALSE;
}
key = g_ascii_strdown(name, -1);
safe_value = g_utf8_make_valid(value, -1);
g_strstrip(key);
g_strstrip(safe_value);
g_hash_table_replace(headers->values, key, safe_value);
return TRUE;
}
static EmlMimeHeaders *eml_mime_parse_headers(
const char *data,
gsize length,
gsize *body_offset,
GError **error
)
{
EmlMimeHeaders *headers = eml_mime_headers_new();
GString *current_value = g_string_new(NULL);
char *current_name = NULL;
gsize cursor = 0;
gboolean separator_found = FALSE;
while (cursor < length)
{
gsize line_start = cursor;
gsize line_length = 0;
char *line = NULL;
while (cursor < length && data[cursor] != '\n')
cursor++;
line_length = cursor - line_start;
if (cursor < length)
cursor++;
if (line_length > 0 && data[line_start + line_length - 1] == '\r')
line_length--;
if (line_length == 0)
{
separator_found = TRUE;
break;
}
if (line_length > EML_MIME_MAX_HEADER_VALUE_LENGTH)
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Une ligne d'en-tête MIME dépasse la limite autorisée."
);
goto failure;
}
line = g_strndup(data + line_start, line_length);
if ((line[0] == ' ' || line[0] == '\t') && current_name != NULL)
{
g_string_append_c(current_value, ' ');
g_string_append(current_value, g_strstrip(line));
g_free(line);
continue;
}
if (current_name != NULL &&
!eml_mime_headers_store(
headers,
current_name,
current_value->str,
error
))
{
g_free(line);
goto failure;
}
g_clear_pointer(&current_name, g_free);
g_string_truncate(current_value, 0);
char *colon = strchr(line, ':');
if (colon == NULL || colon == line)
{
g_free(line);
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Une ligne d'en-tête MIME est malformée."
);
goto failure;
}
current_name = g_strndup(line, (gsize) (colon - line));
g_string_assign(current_value, g_strstrip(colon + 1));
g_free(line);
}
if (current_name != NULL &&
!eml_mime_headers_store(
headers,
current_name,
current_value->str,
error
))
goto failure;
g_free(current_name);
g_string_free(current_value, TRUE);
*body_offset = cursor;
if (!separator_found)
*body_offset = length;
return headers;
failure:
g_free(current_name);
g_string_free(current_value, TRUE);
eml_mime_headers_free(headers);
return NULL;
}
static GHashTable *eml_mime_parse_parameters(
const char *header_value,
char **main_value,
gboolean *malformed
)
{
GHashTable *parameters = g_hash_table_new_full(
g_str_hash,
g_str_equal,
g_free,
g_free
);
const char *cursor = header_value != NULL ? header_value : "";
const char *semicolon = strchr(cursor, ';');
*malformed = FALSE;
*main_value = semicolon != NULL
? g_strndup(cursor, (gsize) (semicolon - cursor))
: g_strdup(cursor);
g_strstrip(*main_value);
cursor = semicolon != NULL ? semicolon + 1 : cursor + strlen(cursor);
while (*cursor != '\0')
{
const char *name_start = NULL;
const char *value_start = NULL;
char *name = NULL;
char *value = NULL;
while (g_ascii_isspace(*cursor) || *cursor == ';')
cursor++;
if (*cursor == '\0')
break;
name_start = cursor;
while (*cursor != '\0' && *cursor != '=' && *cursor != ';')
cursor++;
if (*cursor != '=')
{
*malformed = TRUE;
break;
}
name = g_strndup(name_start, (gsize) (cursor - name_start));
g_strstrip(name);
char *lower_name = g_ascii_strdown(name, -1);
g_free(name);
cursor++;
while (g_ascii_isspace(*cursor))
cursor++;
value_start = cursor;
if (*cursor == '"')
{
GString *quoted = g_string_new(NULL);
gboolean closed = FALSE;
cursor++;
while (*cursor != '\0')
{
if (*cursor == '\\' && cursor[1] != '\0')
{
cursor++;
g_string_append_c(quoted, *cursor++);
}
else if (*cursor == '"')
{
cursor++;
closed = TRUE;
break;
}
else
g_string_append_c(quoted, *cursor++);
}
value = g_string_free(quoted, FALSE);
if (!closed)
*malformed = TRUE;
}
else
{
while (*cursor != '\0' && *cursor != ';')
cursor++;
value = g_strndup(value_start, (gsize) (cursor - value_start));
g_strstrip(value);
}
if (g_hash_table_contains(parameters, lower_name))
{
*malformed = TRUE;
g_hash_table_remove(parameters, lower_name);
g_free(lower_name);
g_free(value);
}
else
g_hash_table_insert(parameters, lower_name, value);
while (g_ascii_isspace(*cursor))
cursor++;
if (*cursor != '\0' && *cursor != ';')
*malformed = TRUE;
}
return parameters;
}
static char *eml_mime_convert_charset(
const guint8 *data,
gsize length,
const char *charset
)
{
GError *error = NULL;
char *converted = NULL;
if (charset == NULL ||
charset[0] == '\0' ||
g_ascii_strcasecmp(charset, "UTF-8") == 0 ||
g_ascii_strcasecmp(charset, "US-ASCII") == 0)
return g_utf8_make_valid((const char *) data, (gssize) length);
converted = g_convert(
(const char *) data,
(gssize) length,
"UTF-8",
charset,
NULL,
NULL,
&error
);
g_clear_error(&error);
return converted;
}
static GBytes *eml_mime_percent_decode(const char *value)
{
GByteArray *decoded = g_byte_array_new();
for (gsize index = 0; value[index] != '\0'; index++)
{
guint8 byte = (guint8) value[index];
if (byte == '%')
{
if (!g_ascii_isxdigit(value[index + 1]) ||
!g_ascii_isxdigit(value[index + 2]))
{
g_byte_array_unref(decoded);
return NULL;
}
char hex[3] = { value[index + 1], value[index + 2], '\0' };
byte = (guint8) g_ascii_strtoull(hex, NULL, 16);
index += 2;
}
g_byte_array_append(decoded, &byte, 1);
}
return g_byte_array_free_to_bytes(decoded);
}
static char *eml_mime_decode_extended_value(const char *value)
{
const char *first_quote = strchr(value, '\'');
const char *second_quote = first_quote != NULL
? strchr(first_quote + 1, '\'')
: NULL;
char *charset = NULL;
GBytes *bytes = NULL;
gconstpointer data = NULL;
gsize length = 0;
char *result = NULL;
if (first_quote == NULL || second_quote == NULL)
return NULL;
charset = g_strndup(value, (gsize) (first_quote - value));
bytes = eml_mime_percent_decode(second_quote + 1);
if (bytes != NULL)
{
data = g_bytes_get_data(bytes, &length);
result = eml_mime_convert_charset(data, length, charset);
g_bytes_unref(bytes);
}
g_free(charset);
return result;
}
static char *eml_mime_decode_rfc2047_word(
const char *start,
const char **next
)
{
const char *charset_end = strstr(start + 2, "?");
const char *encoding_end = charset_end != NULL
? strstr(charset_end + 1, "?")
: NULL;
const char *word_end = encoding_end != NULL
? strstr(encoding_end + 1, "?=")
: NULL;
char *charset = NULL;
char encoding = '\0';
GByteArray *decoded = NULL;
char *result = NULL;
if (charset_end == NULL || encoding_end == NULL || word_end == NULL ||
encoding_end != charset_end + 2)
return NULL;
charset = g_strndup(start + 2, (gsize) (charset_end - start - 2));
encoding = g_ascii_toupper(charset_end[1]);
decoded = g_byte_array_new();
if (encoding == 'B')
{
char *payload = g_strndup(
encoding_end + 1,
(gsize) (word_end - encoding_end - 1)
);
gsize output_length = 0;
guchar *output = g_base64_decode(payload, &output_length);
g_free(payload);
if (output == NULL)
goto cleanup;
g_byte_array_append(decoded, output, (guint) output_length);
g_free(output);
}
else if (encoding == 'Q')
{
for (const char *cursor = encoding_end + 1;
cursor < word_end;
cursor++)
{
guint8 byte = (guint8) *cursor;
if (byte == '_')
byte = ' ';
else if (byte == '=')
{
if (cursor + 2 >= word_end ||
!g_ascii_isxdigit(cursor[1]) ||
!g_ascii_isxdigit(cursor[2]))
goto cleanup;
char hex[3] = { cursor[1], cursor[2], '\0' };
byte = (guint8) g_ascii_strtoull(hex, NULL, 16);
cursor += 2;
}
g_byte_array_append(decoded, &byte, 1);
}
}
else
goto cleanup;
result = eml_mime_convert_charset(decoded->data, decoded->len, charset);
if (result != NULL)
*next = word_end + 2;
cleanup:
g_byte_array_unref(decoded);
g_free(charset);
return result;
}
static char *eml_mime_decode_rfc2047(const char *value)
{
GString *result = g_string_new(NULL);
const char *cursor = value;
while (*cursor != '\0')
{
if (cursor[0] == '=' && cursor[1] == '?')
{
const char *next = NULL;
char *decoded = eml_mime_decode_rfc2047_word(cursor, &next);
if (decoded != NULL)
{
g_string_append(result, decoded);
g_free(decoded);
cursor = next;
while (g_ascii_isspace(*cursor) &&
cursor[1] == '=' &&
cursor[2] == '?')
cursor++;
continue;
}
}
g_string_append_c(result, *cursor++);
}
return g_string_free(result, FALSE);
}
static char *eml_mime_resolve_continuation(
GHashTable *parameters,
const char *base_name
)
{
GString *joined = g_string_new(NULL);
gboolean encoded_first = FALSE;
gboolean found = FALSE;
char *charset = NULL;
for (guint index = 0; index < EML_MIME_MAX_PARTS; index++)
{
char *encoded_key = g_strdup_printf("%s*%u*", base_name, index);
char *plain_key = g_strdup_printf("%s*%u", base_name, index);
const char *segment = g_hash_table_lookup(parameters, encoded_key);
gboolean encoded = segment != NULL;
if (segment == NULL)
segment = g_hash_table_lookup(parameters, plain_key);
g_free(encoded_key);
g_free(plain_key);
if (segment == NULL)
{
if (index == 0)
{
g_string_free(joined, TRUE);
return NULL;
}
GHashTableIter iterator;
gpointer key = NULL;
gboolean later_segment = FALSE;
char *prefix = g_strdup_printf("%s*", base_name);
g_hash_table_iter_init(&iterator, parameters);
while (g_hash_table_iter_next(&iterator, &key, NULL))
{
const char *parameter_name = key;
if (g_str_has_prefix(parameter_name, prefix) &&
g_ascii_isdigit(parameter_name[strlen(prefix)]))
{
guint parameter_index = (guint) g_ascii_strtoull(
parameter_name + strlen(prefix),
NULL,
10
);
if (parameter_index > index)
later_segment = TRUE;
}
}
g_free(prefix);
if (later_segment)
{
g_free(charset);
g_string_free(joined, TRUE);
return NULL;
}
break;
}
if (index == 0)
encoded_first = encoded;
if (encoded)
{
GBytes *bytes = NULL;
const char *payload = segment;
if (index == 0 && encoded_first)
{
const char *first_quote = strchr(segment, '\'');
const char *second_quote = first_quote != NULL
? strchr(first_quote + 1, '\'')
: NULL;
if (second_quote == NULL)
{
g_free(charset);
g_string_free(joined, TRUE);
return NULL;
}
charset = g_strndup(
segment,
(gsize) (first_quote - segment)
);
payload = second_quote + 1;
}
bytes = eml_mime_percent_decode(payload);
if (bytes == NULL)
{
g_free(charset);
g_string_free(joined, TRUE);
return NULL;
}
gsize length = 0;
const char *data = g_bytes_get_data(bytes, &length);
g_string_append_len(joined, data, (gssize) length);
g_bytes_unref(bytes);
}
else
g_string_append(joined, segment);
found = TRUE;
}
if (!found)
{
g_free(charset);
g_string_free(joined, TRUE);
return NULL;
}
char *result = eml_mime_convert_charset(
(const guint8 *) joined->str,
joined->len,
charset
);
g_free(charset);
g_string_free(joined, TRUE);
return result;
}
static char *eml_mime_resolve_parameter(
GHashTable *parameters,
const char *base_name
)
{
char *value = eml_mime_resolve_continuation(parameters, base_name);
char *extended_key = NULL;
const char *raw_value = NULL;
if (value != NULL)
return value;
extended_key = g_strdup_printf("%s*", base_name);
raw_value = g_hash_table_lookup(parameters, extended_key);
g_free(extended_key);
if (raw_value != NULL)
{
value = eml_mime_decode_extended_value(raw_value);
if (value != NULL)
return value;
}
raw_value = g_hash_table_lookup(parameters, base_name);
return raw_value != NULL ? eml_mime_decode_rfc2047(raw_value) : NULL;
}
static char *eml_mime_raw_parameter(
GHashTable *parameters,
const char *base_name
)
{
GString *continuation = g_string_new(NULL);
gboolean found = FALSE;
for (guint index = 0; index < EML_MIME_MAX_PARTS; index++)
{
char *encoded_key = g_strdup_printf("%s*%u*", base_name, index);
char *plain_key = g_strdup_printf("%s*%u", base_name, index);
const char *segment = g_hash_table_lookup(parameters, encoded_key);
if (segment == NULL)
segment = g_hash_table_lookup(parameters, plain_key);
g_free(encoded_key);
g_free(plain_key);
if (segment == NULL)
break;
g_string_append(continuation, segment);
found = TRUE;
}
if (found)
return g_string_free(continuation, FALSE);
g_string_free(continuation, TRUE);
char *extended_key = g_strdup_printf("%s*", base_name);
const char *raw = g_hash_table_lookup(parameters, extended_key);
g_free(extended_key);
if (raw == NULL)
raw = g_hash_table_lookup(parameters, base_name);
return g_strdup(raw);
}
char *eml_mime_sanitize_filename(const char *raw_filename)
{
char *valid = raw_filename != NULL
? g_utf8_make_valid(raw_filename, -1)
: NULL;
GString *sanitized = g_string_new(NULL);
if (valid != NULL)
{
for (const char *cursor = valid;
*cursor != '\0' &&
sanitized->len < EML_MIME_MAX_FILENAME_LENGTH;
cursor = g_utf8_next_char(cursor))
{
gunichar character = g_utf8_get_char(cursor);
if (character < 0x20 ||
character == 0x7f ||
character == '/' ||
character == '\\' ||
character == ':' ||
character == '*' ||
character == '?' ||
character == '"' ||
character == '<' ||
character == '>' ||
character == '|')
g_string_append_c(sanitized, '_');
else
{
char utf8[6] = { 0 };
gint width = g_unichar_to_utf8(character, utf8);
if (sanitized->len + (gsize) width >
EML_MIME_MAX_FILENAME_LENGTH)
break;
g_string_append_len(sanitized, utf8, width);
}
}
}
g_free(valid);
while (strstr(sanitized->str, "..") != NULL)
{
char *dots = strstr(sanitized->str, "..");
dots[0] = '_';
dots[1] = '_';
}
g_strstrip(sanitized->str);
g_string_set_size(sanitized, strlen(sanitized->str));
while (sanitized->len > 0 &&
(sanitized->str[sanitized->len - 1] == '.' ||
sanitized->str[sanitized->len - 1] == ' '))
g_string_truncate(sanitized, sanitized->len - 1);
if (sanitized->len == 0 ||
g_str_equal(sanitized->str, ".") ||
g_str_equal(sanitized->str, ".."))
g_string_assign(sanitized, "attachment.bin");
return g_string_free(sanitized, FALSE);
}
static GBytes *eml_mime_decode_transfer(
EmlMimeContext *context,
const char *encoding,
const char *data,
gsize length,
GError **error
)
{
GByteArray *decoded = g_byte_array_new();
char *normalized = encoding != NULL
? g_ascii_strdown(encoding, -1)
: g_strdup("7bit");
if (g_str_equal(normalized, "base64"))
{
GString *compact = g_string_sized_new(length);
guint padding = 0;
for (gsize index = 0; index < length; index++)
{
if ((index % EML_MIME_IO_BLOCK_SIZE) == 0 &&
eml_mime_check_cancelled(context->cancellable, error))
goto failure;
if (g_ascii_isspace(data[index]))
continue;
if (data[index] == '=')
padding++;
else if (!g_ascii_isalnum(data[index]) &&
data[index] != '+' &&
data[index] != '/')
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Le contenu Base64 contient un caractère invalide."
);
g_string_free(compact, TRUE);
goto failure;
}
else if (padding > 0)
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Le padding Base64 est invalide."
);
g_string_free(compact, TRUE);
goto failure;
}
g_string_append_c(compact, data[index]);
}
if (compact->len > 0 &&
(compact->len % 4 != 0 || padding > 2))
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Le contenu Base64 est tronqué ou son padding est invalide."
);
g_string_free(compact, TRUE);
goto failure;
}
if (compact->len > 0)
{
gsize output_length = 0;
guchar *output = g_base64_decode(
compact->str,
&output_length
);
if (output_length > EML_MIME_MAX_PART_DECODED_SIZE)
{
g_free(output);
g_string_free(compact, TRUE);
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_NO_SPACE,
"La taille décodée de la partie dépasse la limite."
);
goto failure;
}
g_byte_array_append(decoded, output, (guint) output_length);
g_free(output);
}
g_string_free(compact, TRUE);
}
else if (g_str_equal(normalized, "quoted-printable"))
{
for (gsize index = 0; index < length; index++)
{
guint8 byte = (guint8) data[index];
if ((index % EML_MIME_IO_BLOCK_SIZE) == 0 &&
eml_mime_check_cancelled(context->cancellable, error))
goto failure;
if (byte == '=')
{
if (index + 1 < length && data[index + 1] == '\n')
{
index++;
continue;
}
if (index + 2 < length &&
data[index + 1] == '\r' &&
data[index + 2] == '\n')
{
index += 2;
continue;
}
if (index + 2 >= length ||
!g_ascii_isxdigit(data[index + 1]) ||
!g_ascii_isxdigit(data[index + 2]))
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Une séquence quoted-printable est invalide."
);
goto failure;
}
char hex[3] = {
data[index + 1],
data[index + 2],
'\0'
};
byte = (guint8) g_ascii_strtoull(hex, NULL, 16);
index += 2;
}
g_byte_array_append(decoded, &byte, 1);
if (decoded->len > EML_MIME_MAX_PART_DECODED_SIZE)
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_NO_SPACE,
"La taille décodée de la partie dépasse la limite."
);
goto failure;
}
}
}
else if (g_str_equal(normalized, "7bit") ||
g_str_equal(normalized, "8bit") ||
g_str_equal(normalized, "binary"))
{
if (length > EML_MIME_MAX_PART_DECODED_SIZE)
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_NO_SPACE,
"La taille décodée de la partie dépasse la limite."
);
goto failure;
}
g_byte_array_append(decoded, (const guint8 *) data, (guint) length);
}
else
{
g_set_error(
error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"L'encodage de transfert « %s » n'est pas pris en charge.",
encoding
);
goto failure;
}
g_free(normalized);
return g_byte_array_free_to_bytes(decoded);
failure:
g_free(normalized);
g_byte_array_unref(decoded);
return NULL;
}
static const char *eml_mime_extension_for_type(const char *content_type)
{
if (g_strcmp0(content_type, "text/plain") == 0)
return "txt";
if (g_strcmp0(content_type, "text/html") == 0)
return "html";
if (g_strcmp0(content_type, "image/png") == 0)
return "png";
if (g_strcmp0(content_type, "image/jpeg") == 0)
return "jpg";
if (g_strcmp0(content_type, "application/pdf") == 0)
return "pdf";
if (g_strcmp0(content_type, "message/rfc822") == 0)
return "eml";
return "bin";
}
static gboolean eml_mime_name_exists_casefold(
const char *directory,
const char *name
)
{
GDir *dir = g_dir_open(directory, 0, NULL);
const char *entry = NULL;
gboolean exists = FALSE;
if (dir == NULL)
{
char *path = g_build_filename(directory, name, NULL);
gboolean exists = g_file_test(path, G_FILE_TEST_EXISTS);
g_free(path);
return exists;
}
while ((entry = g_dir_read_name(dir)) != NULL)
{
if (g_ascii_strcasecmp(entry, name) == 0)
{
exists = TRUE;
break;
}
}
g_dir_close(dir);
return exists;
}
static char *eml_mime_unique_name(
const char *directory,
const char *sanitized,
GError **error
)
{
char *stem = NULL;
char *extension = NULL;
const char *dot = strrchr(sanitized, '.');
if (dot != NULL && dot != sanitized)
{
stem = g_strndup(sanitized, (gsize) (dot - sanitized));
extension = g_strdup(dot);
}
else
{
stem = g_strdup(sanitized);
extension = g_strdup("");
}
for (guint index = 1; index <= EML_MIME_MAX_COLLISIONS; index++)
{
char *candidate = index == 1
? g_strdup(sanitized)
: g_strdup_printf("%s-%u%s", stem, index, extension);
if (!eml_mime_name_exists_casefold(directory, candidate))
{
g_free(stem);
g_free(extension);
return candidate;
}
g_free(candidate);
}
g_free(stem);
g_free(extension);
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_EXISTS,
"Impossible de produire un nom de fichier dérivé unique."
);
return NULL;
}
static gboolean eml_mime_write_atomic(
EmlMimeContext *context,
const char *final_path,
GBytes *bytes,
GError **error
)
{
char *template = g_build_filename(
context->target_dir,
".eml-mime-XXXXXX",
NULL
);
gint descriptor = g_mkstemp(template);
GFile *temporary_file = NULL;
GFile *final_file = NULL;
gboolean success = FALSE;
if (descriptor < 0)
{
g_set_error(
error,
G_FILE_ERROR,
g_file_error_from_errno(errno),
"Impossible de créer le fichier temporaire : %s",
g_strerror(errno)
);
g_free(template);
return FALSE;
}
gsize length = 0;
const guint8 *data = g_bytes_get_data(bytes, &length);
gsize offset = 0;
while (offset < length)
{
gsize block = MIN(
(gsize) EML_MIME_IO_BLOCK_SIZE,
length - offset
);
if (eml_mime_check_cancelled(context->cancellable, error))
goto cleanup;
ssize_t written = write(descriptor, data + offset, block);
if (written < 0)
{
g_set_error(
error,
G_FILE_ERROR,
g_file_error_from_errno(errno),
"Échec d'écriture du fichier temporaire : %s",
g_strerror(errno)
);
goto cleanup;
}
offset += (gsize) written;
}
if (close(descriptor) != 0)
{
descriptor = -1;
g_set_error(
error,
G_FILE_ERROR,
g_file_error_from_errno(errno),
"Échec de fermeture du fichier temporaire : %s",
g_strerror(errno)
);
goto cleanup;
}
descriptor = -1;
if (eml_mime_check_cancelled(context->cancellable, error))
goto cleanup;
temporary_file = g_file_new_for_path(template);
final_file = g_file_new_for_path(final_path);
if (!g_file_move(
temporary_file,
final_file,
G_FILE_COPY_NONE,
context->cancellable,
NULL,
NULL,
error
))
goto cleanup;
success = TRUE;
cleanup:
if (descriptor >= 0)
close(descriptor);
if (!success)
g_remove(template);
g_clear_object(&temporary_file);
g_clear_object(&final_file);
g_free(template);
return success;
}
static char *eml_mime_normalize_content_id(const char *content_id)
{
char *normalized = content_id != NULL ? g_strdup(content_id) : NULL;
if (normalized == NULL)
return NULL;
g_strstrip(normalized);
gsize length = strlen(normalized);
if (length >= 2 && normalized[0] == '<' &&
normalized[length - 1] == '>')
{
normalized[length - 1] = '\0';
memmove(normalized, normalized + 1, length - 1);
}
return normalized;
}
static gboolean eml_mime_extract_leaf(
EmlMimeContext *context,
const EmlMimeHeaders *headers,
const char *body,
gsize body_length,
const char *part_index,
const char *content_type,
GHashTable *type_parameters,
GError **error
)
{
const char *raw_disposition = eml_mime_headers_get(
headers,
"content-disposition"
);
const char *encoding = eml_mime_headers_get(
headers,
"content-transfer-encoding"
);
const char *content_id = eml_mime_headers_get(headers, "content-id");
char *disposition = NULL;
gboolean disposition_malformed = FALSE;
GHashTable *disposition_parameters = eml_mime_parse_parameters(
raw_disposition,
&disposition,
&disposition_malformed
);
char *filename = eml_mime_resolve_parameter(
disposition_parameters,
"filename"
);
char *raw_filename = eml_mime_raw_parameter(
disposition_parameters,
"filename"
);
char *type_name = NULL;
gboolean relevant = FALSE;
GBytes *decoded = NULL;
GError *decode_error = NULL;
if (filename == NULL)
{
type_name = eml_mime_resolve_parameter(type_parameters, "name");
g_clear_pointer(&raw_filename, g_free);
raw_filename = eml_mime_raw_parameter(type_parameters, "name");
}
if (filename == NULL)
filename = g_steal_pointer(&type_name);
relevant = filename != NULL ||
content_id != NULL ||
g_ascii_strcasecmp(disposition, "attachment") == 0 ||
g_ascii_strcasecmp(disposition, "inline") == 0;
gboolean body_candidate = !relevant &&
(g_str_equal(content_type, "text/plain") ||
g_str_equal(content_type, "text/html"));
if (!relevant && !body_candidate)
goto cleanup;
if (disposition_malformed)
eml_mime_warn(
context,
part_index,
"Content-Disposition contient un paramètre malformé."
);
decoded = eml_mime_decode_transfer(
context,
encoding,
body,
body_length,
&decode_error
);
if (decoded == NULL)
{
if (g_error_matches(
decode_error,
G_IO_ERROR,
G_IO_ERROR_CANCELLED
))
{
g_propagate_error(error, decode_error);
g_hash_table_unref(disposition_parameters);
g_free(disposition);
g_free(filename);
g_free(raw_filename);
return FALSE;
}
eml_mime_warn(context, part_index, decode_error->message);
g_clear_error(&decode_error);
goto cleanup;
}
gsize decoded_length = 0;
gconstpointer decoded_data = g_bytes_get_data(decoded, &decoded_length);
if (body_candidate)
{
guint rank = g_str_equal(content_type, "text/plain") ? 2U : 1U;
if (rank >= context->body_rank)
{
gsize kept = MIN(decoded_length,
(gsize) EML_MIME_PREVIEW_BODY_MAX);
char *valid = g_utf8_make_valid(decoded_data, (gssize) kept);
if (rank == 1U)
{
GRegex *active = g_regex_new(
"(?is)<(script|style)\\b[^>]*>.*?</\\1\\s*>",
0, 0, NULL);
char *without_active = g_regex_replace(active, valid, -1,
0, " ", 0, NULL);
GRegex *tags = g_regex_new("(?s)<[^>]+>", 0, 0, NULL);
char *plain = g_regex_replace(tags, without_active, -1,
0, " ", 0, NULL);
g_regex_unref(active); g_regex_unref(tags);
g_free(without_active); g_free(valid); valid = plain;
}
g_free(context->result->body_text);
context->result->body_text = valid;
context->result->body_from_html = rank == 1U;
context->result->body_truncated =
decoded_length > EML_MIME_PREVIEW_BODY_MAX;
context->body_rank = rank;
}
goto cleanup;
}
if (context->total_decoded_size >
EML_MIME_MAX_TOTAL_DECODED_SIZE - decoded_length)
{
eml_mime_warn(
context,
part_index,
"la taille décodée cumulée maximale est dépassée."
);
goto cleanup;
}
char *synthetic = NULL;
if (filename == NULL || filename[0] == '\0')
synthetic = g_strdup_printf(
"part-%s.%s",
part_index,
eml_mime_extension_for_type(content_type)
);
const char *decoded_name = filename != NULL ? filename : synthetic;
const char *declared = raw_filename != NULL
? raw_filename
: decoded_name;
char *sanitized = eml_mime_sanitize_filename(decoded_name);
if (context->preview_only)
{
EmlAttachment *attachment = g_new0(EmlAttachment, 1);
attachment->part_index = g_strdup(part_index);
attachment->declared_filename = g_strdup(declared);
attachment->decoded_filename = g_strdup(decoded_name);
attachment->sanitized_filename = g_strdup(sanitized);
attachment->content_type = g_strdup(content_type);
attachment->detected_mime = g_strdup(content_type);
attachment->content_id = g_strdup(content_id);
attachment->normalized_content_id =
eml_mime_normalize_content_id(content_id);
attachment->content_disposition = g_strdup(raw_disposition);
attachment->normalized_disposition = disposition[0] != '\0'
? g_ascii_strdown(disposition, -1) : NULL;
attachment->transfer_encoding = g_strdup(
encoding != NULL ? encoding : "7bit");
attachment->is_inline =
g_ascii_strcasecmp(disposition, "inline") == 0;
attachment->is_attachment =
g_ascii_strcasecmp(disposition, "attachment") == 0;
attachment->encoded_size = body_length;
attachment->decoded_size = decoded_length;
context->total_decoded_size += decoded_length;
g_ptr_array_add(context->result->attachments, attachment);
g_free(sanitized); g_free(synthetic);
goto cleanup;
}
char *unique = eml_mime_unique_name(
context->target_dir,
sanitized,
error
);
char *final_path = unique != NULL
? g_build_filename(context->target_dir, unique, NULL)
: NULL;
char *canonical = final_path != NULL
? g_canonicalize_filename(final_path, NULL)
: NULL;
if (unique == NULL ||
!g_str_has_prefix(canonical, context->target_canonical) ||
canonical[strlen(context->target_canonical)] != G_DIR_SEPARATOR)
{
if (unique != NULL)
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_PERMISSION_DENIED,
"Le chemin final sortirait du répertoire de destination."
);
g_free(canonical);
g_free(final_path);
g_free(unique);
g_free(sanitized);
g_free(synthetic);
goto fatal;
}
if (!eml_mime_write_atomic(context, final_path, decoded, error))
{
g_free(canonical);
g_free(final_path);
g_free(unique);
g_free(sanitized);
g_free(synthetic);
goto fatal;
}
EmlAttachment *attachment = g_new0(EmlAttachment, 1);
attachment->part_index = g_strdup(part_index);
attachment->declared_filename = g_strdup(declared);
attachment->decoded_filename = g_strdup(decoded_name);
attachment->sanitized_filename = unique;
attachment->extracted_path = final_path;
attachment->relative_path = g_strdup(unique);
attachment->content_type = g_strdup(content_type);
attachment->detected_mime = g_content_type_guess(
final_path,
decoded_data,
decoded_length,
NULL
);
attachment->content_id = g_strdup(content_id);
attachment->normalized_content_id =
eml_mime_normalize_content_id(content_id);
attachment->content_disposition = g_strdup(raw_disposition);
attachment->normalized_disposition =
disposition[0] != '\0' ? g_ascii_strdown(disposition, -1) : NULL;
attachment->transfer_encoding = g_strdup(
encoding != NULL ? encoding : "7bit"
);
attachment->is_inline =
g_ascii_strcasecmp(disposition, "inline") == 0;
attachment->is_attachment =
g_ascii_strcasecmp(disposition, "attachment") == 0;
attachment->encoded_size = body_length;
attachment->decoded_size = decoded_length;
GDateTime *now = g_date_time_new_now_utc();
attachment->extracted_at_utc = g_date_time_format_iso8601(now);
g_date_time_unref(now);
guint64 hashed_size = 0;
if (!file_hash_compute_sha256(
final_path,
context->cancellable,
&attachment->sha256,
&hashed_size,
error
))
{
g_remove(final_path);
eml_attachment_free(attachment);
g_free(canonical);
g_free(sanitized);
g_free(synthetic);
goto fatal;
}
context->total_decoded_size += decoded_length;
g_ptr_array_add(context->result->attachments, attachment);
g_free(canonical);
g_free(sanitized);
g_free(synthetic);
cleanup:
g_clear_pointer(&decoded, g_bytes_unref);
g_hash_table_unref(disposition_parameters);
g_free(disposition);
g_free(filename);
g_free(raw_filename);
g_free(type_name);
return TRUE;
fatal:
g_clear_pointer(&decoded, g_bytes_unref);
g_hash_table_unref(disposition_parameters);
g_free(disposition);
g_free(filename);
g_free(raw_filename);
g_free(type_name);
return FALSE;
}
static gboolean eml_mime_parse_entity(
EmlMimeContext *context,
const char *data,
gsize length,
const char *part_index,
guint depth,
GError **error
);
static gboolean eml_mime_parse_multipart(
EmlMimeContext *context,
const char *body,
gsize body_length,
const char *boundary,
const char *part_index,
guint depth,
GError **error
)
{
char *delimiter = g_strdup_printf("--%s", boundary);
gsize delimiter_length = strlen(delimiter);
gsize cursor = 0;
gsize part_start = 0;
guint child_index = 0;
gboolean opened = FALSE;
gboolean closed = FALSE;
while (cursor <= body_length)
{
gsize line_start = cursor;
gsize line_length = 0;
while (cursor < body_length && body[cursor] != '\n')
cursor++;
line_length = cursor - line_start;
if (cursor < body_length)
cursor++;
if (line_length > 0 && body[line_start + line_length - 1] == '\r')
line_length--;
if (line_length >= delimiter_length &&
memcmp(body + line_start, delimiter, delimiter_length) == 0 &&
(line_length == delimiter_length ||
(line_length == delimiter_length + 2 &&
body[line_start + delimiter_length] == '-' &&
body[line_start + delimiter_length + 1] == '-')))
{
if (opened && part_start < line_start)
{
gsize part_length = line_start - part_start;
while (part_length > 0 &&
(body[part_start + part_length - 1] == '\r' ||
body[part_start + part_length - 1] == '\n'))
part_length--;
child_index++;
char *child_path = g_strdup_printf(
"%s.%u",
part_index,
child_index
);
gboolean success = eml_mime_parse_entity(
context,
body + part_start,
part_length,
child_path,
depth + 1,
error
);
g_free(child_path);
if (!success)
{
g_free(delimiter);
return FALSE;
}
}
opened = TRUE;
if (line_length == delimiter_length + 2)
{
closed = TRUE;
break;
}
part_start = cursor;
}
if (cursor == body_length)
break;
}
if (opened && !closed && part_start < body_length)
{
gsize part_length = body_length - part_start;
while (part_length > 0 &&
(body[part_start + part_length - 1] == '\r' ||
body[part_start + part_length - 1] == '\n'))
part_length--;
child_index++;
char *child_path = g_strdup_printf(
"%s.%u",
part_index,
child_index
);
gboolean success = eml_mime_parse_entity(
context,
body + part_start,
part_length,
child_path,
depth + 1,
error
);
g_free(child_path);
if (!success)
{
g_free(delimiter);
return FALSE;
}
}
if (!opened)
eml_mime_warn(
context,
part_index,
"le multipart déclaré ne contient aucune boundary."
);
else if (!closed)
eml_mime_warn(
context,
part_index,
"la boundary multipart n'est pas fermée."
);
g_free(delimiter);
return TRUE;
}
static gboolean eml_mime_parse_entity(
EmlMimeContext *context,
const char *data,
gsize length,
const char *part_index,
guint depth,
GError **error
)
{
gsize body_offset = 0;
EmlMimeHeaders *headers = NULL;
const char *raw_content_type = NULL;
char *content_type = NULL;
gboolean type_malformed = FALSE;
GHashTable *type_parameters = NULL;
gboolean success = TRUE;
if (eml_mime_check_cancelled(context->cancellable, error))
return FALSE;
if (depth > EML_MIME_MAX_DEPTH)
{
eml_mime_warn(
context,
part_index,
"la profondeur MIME maximale est dépassée."
);
return TRUE;
}
context->part_count++;
if (context->part_count > EML_MIME_MAX_PARTS)
{
eml_mime_warn(
context,
part_index,
"le nombre total maximal de parties est dépassé."
);
return TRUE;
}
GError *header_error = NULL;
headers = eml_mime_parse_headers(
data,
length,
&body_offset,
&header_error
);
if (headers == NULL)
{
eml_mime_warn(context, part_index, header_error->message);
g_clear_error(&header_error);
return TRUE;
}
raw_content_type = eml_mime_headers_get(headers, "content-type");
type_parameters = eml_mime_parse_parameters(
raw_content_type != NULL ? raw_content_type : "text/plain",
&content_type,
&type_malformed
);
char *lower_content_type = g_ascii_strdown(content_type, -1);
g_free(content_type);
content_type = lower_content_type;
if (type_malformed)
eml_mime_warn(
context,
part_index,
"Content-Type contient un paramètre malformé."
);
if (g_str_has_prefix(content_type, "multipart/"))
{
const char *boundary = g_hash_table_lookup(
type_parameters,
"boundary"
);
if (boundary == NULL || boundary[0] == '\0')
eml_mime_warn(
context,
part_index,
"le multipart ne déclare aucune boundary."
);
else
success = eml_mime_parse_multipart(
context,
data + body_offset,
length - body_offset,
boundary,
part_index,
depth,
error
);
}
else if (g_str_equal(content_type, "message/rfc822"))
{
char *child_path = g_strdup_printf("%s.1", part_index);
success = eml_mime_parse_entity(
context,
data + body_offset,
length - body_offset,
child_path,
depth + 1,
error
);
g_free(child_path);
}
else
success = eml_mime_extract_leaf(
context,
headers,
data + body_offset,
length - body_offset,
part_index,
content_type,
type_parameters,
error
);
g_hash_table_unref(type_parameters);
g_free(content_type);
eml_mime_headers_free(headers);
return success;
}
EmlMimeResult *eml_mime_extract_attachments_cancellable(
const char *eml_path,
const char *target_dir,
GCancellable *cancellable,
GError **error
)
{
GMappedFile *mapped = NULL;
EmlMimeContext context = { 0 };
EmlMimeResult *result = NULL;
gsize size = 0;
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (eml_path == NULL || eml_path[0] == '\0' ||
target_dir == NULL || target_dir[0] == '\0')
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_ARGUMENT,
"Les chemins de l'EML et du dossier cible sont invalides."
);
return NULL;
}
if (eml_mime_check_cancelled(cancellable, error))
return NULL;
mapped = g_mapped_file_new(eml_path, FALSE, error);
if (mapped == NULL)
return NULL;
size = g_mapped_file_get_length(mapped);
if (size == 0 || size > EML_MIME_MAX_FILE_SIZE)
{
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"Le fichier EML est vide ou dépasse la limite autorisée."
);
g_mapped_file_unref(mapped);
return NULL;
}
if (g_mkdir_with_parents(target_dir, 0755) != 0)
{
g_set_error(
error,
G_FILE_ERROR,
g_file_error_from_errno(errno),
"Impossible de créer le dossier cible : %s",
g_strerror(errno)
);
g_mapped_file_unref(mapped);
return NULL;
}
result = g_new0(EmlMimeResult, 1);
result->attachments = g_ptr_array_new_with_free_func(
(GDestroyNotify) eml_attachment_free
);
result->warnings = g_ptr_array_new_with_free_func(g_free);
context.target_dir = target_dir;
context.target_canonical = g_canonicalize_filename(target_dir, NULL);
context.cancellable = cancellable;
context.result = result;
if (!eml_mime_parse_entity(
&context,
g_mapped_file_get_contents(mapped),
size,
"1",
1,
error
))
{
g_free((gpointer) context.target_canonical);
g_mapped_file_unref(mapped);
eml_mime_result_free(result);
return NULL;
}
g_free((gpointer) context.target_canonical);
g_mapped_file_unref(mapped);
return result;
}
EmlMimeResult *eml_mime_extract_attachments(
const char *eml_path,
const char *target_dir,
GError **error
)
{
return eml_mime_extract_attachments_cancellable(
eml_path,
target_dir,
NULL,
error
);
}
EmlMimeResult *eml_mime_build_preview(const char *eml_path,
GCancellable *cancellable, GError **error)
{
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (eml_path == NULL || eml_path[0] == '\0')
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Le chemin de l'EML est invalide.");
return NULL;
}
if (eml_mime_check_cancelled(cancellable, error))
return NULL;
GMappedFile *mapped = g_mapped_file_new(eml_path, FALSE, error);
if (mapped == NULL)
return NULL;
gsize size = g_mapped_file_get_length(mapped);
if (size == 0 || size > EML_MIME_MAX_FILE_SIZE)
{
g_mapped_file_unref(mapped);
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
"Le fichier EML est vide ou dépasse la limite autorisée.");
return NULL;
}
EmlMimeResult *result = g_new0(EmlMimeResult, 1);
result->attachments = g_ptr_array_new_with_free_func(
(GDestroyNotify) eml_attachment_free);
result->warnings = g_ptr_array_new_with_free_func(g_free);
EmlMimeContext context = {
.cancellable = cancellable,
.result = result,
.preview_only = TRUE
};
if (!eml_mime_parse_entity(&context, g_mapped_file_get_contents(mapped),
size, "1", 1, error))
{
g_mapped_file_unref(mapped);
eml_mime_result_free(result);
return NULL;
}
g_mapped_file_unref(mapped);
return result;
}