1836 lines
53 KiB
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(¤t_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;
|
|
}
|