diff --git a/Makefile b/Makefile index babe0c5..fe1746c 100644 --- a/Makefile +++ b/Makefile @@ -135,12 +135,14 @@ TEST_RELATION_TYPE_SERVICE := tests/test_relation_type_service TEST_CONTROLLED_VOCAB := tests/test_controlled_vocab TEST_BANK_PROPOSAL := tests/test_bank_proposal TEST_EML_PIPELINE_TASK := tests/test_eml_pipeline_task +TEST_EML_MIME_EXTRACTOR := tests/test_eml_mime_extractor all: $(TARGET) $(TEST_BANK_PROPOSAL): \ tests/test_bank_proposal.c \ src/core/bank_proposal.c \ + src/core/iban_analyzer.c \ src/core/controlled_vocab.c $(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \ $(shell $(PKG_CONFIG) --libs glib-2.0) @@ -160,6 +162,12 @@ $(TEST_EML_PIPELINE_TASK): \ $(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_EML_MIME_EXTRACTOR): \ + tests/test_eml_mime_extractor.c \ + src/core/eml_mime_extractor.c \ + src/core/file_hash.c + $(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS) + $(TEST_RELATION_TYPE_NORMALIZER): \ @@ -829,7 +837,8 @@ test: \ $(TEST_RELATION_TYPE_SERVICE) \ $(TEST_CONTROLLED_VOCAB) \ $(TEST_BANK_PROPOSAL) \ - $(TEST_EML_PIPELINE_TASK) + $(TEST_EML_PIPELINE_TASK) \ + $(TEST_EML_MIME_EXTRACTOR) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -898,6 +907,7 @@ test: \ @$(TEST_CONTROLLED_VOCAB) @$(TEST_BANK_PROPOSAL) @$(TEST_EML_PIPELINE_TASK) + @$(TEST_EML_MIME_EXTRACTOR) @echo "Tous les tests sont valides." %.o: %.c @@ -969,7 +979,8 @@ clean: $(TEST_RELATION_TYPE_SERVICE) \ $(TEST_CONTROLLED_VOCAB) \ $(TEST_BANK_PROPOSAL) \ - $(TEST_EML_PIPELINE_TASK) + $(TEST_EML_PIPELINE_TASK) \ + $(TEST_EML_MIME_EXTRACTOR) diff --git a/include/core/bank_proposal.h b/include/core/bank_proposal.h index ee1965d..237e301 100644 --- a/include/core/bank_proposal.h +++ b/include/core/bank_proposal.h @@ -16,6 +16,7 @@ typedef struct BankProposal char *id; /**< UUID de la proposition */ char *raw_iban; /**< Graphie IBAN brute lue/OCR */ char *normalized_iban; /**< IBAN nettoyé et majuscule */ + char *raw_bic; /**< Graphie BIC brute observée */ char *bic; /**< BIC / SWIFT (8 ou 11 car) */ char *holder_name; /**< Titulaire du compte */ char *bank_name; /**< Nom de la banque */ @@ -26,6 +27,7 @@ typedef struct BankProposal char *account_number; /**< Numéro de compte (11 car) */ char *rib_key; /**< Clé RIB (2 ch) */ gboolean is_iban_valid; /**< VRAI si MOD-97 et format valides */ + char *iban_validation; /**< Résultat : valid, invalid ou indeterminate */ gboolean is_derived_bban; /**< VRAI si composants dérivés de l'IBAN */ char *suggested_ocr_fix; /**< Proposition de correction OCR (ex: "O->0") */ char *verification_status; /**< Code contrôlé: proposed, confirmed, rejected, etc. */ diff --git a/include/core/eml_analyzer.h b/include/core/eml_analyzer.h index 0e44000..b2d8a1f 100644 --- a/include/core/eml_analyzer.h +++ b/include/core/eml_analyzer.h @@ -37,5 +37,11 @@ const GPtrArray *eml_analysis_get_destination_ip_addresses( const EmlAnalysis *analysis); /** @brief Retourne une copie UTF-8 des en-têtes bruts. */ const char *eml_analysis_get_raw_headers(const EmlAnalysis *analysis); +/** + * @brief Retourne la date du message normalisée en UTC, ou NULL. + * + * La valeur brute reste disponible via l'en-tête `Date`. + */ +const char *eml_analysis_get_date_utc(const EmlAnalysis *analysis); G_END_DECLS #endif diff --git a/include/core/eml_mime_extractor.h b/include/core/eml_mime_extractor.h index bc653dc..c59bb77 100644 --- a/include/core/eml_mime_extractor.h +++ b/include/core/eml_mime_extractor.h @@ -5,23 +5,39 @@ #ifndef LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H #define LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H -#include +#include G_BEGIN_DECLS +/** @brief Limites de sécurité centralisées de l'extracteur MIME. */ +#define EML_MIME_MAX_FILE_SIZE (50U * 1024U * 1024U) +#define EML_MIME_MAX_PART_DECODED_SIZE (8U * 1024U * 1024U) +#define EML_MIME_MAX_TOTAL_DECODED_SIZE (32U * 1024U * 1024U) +#define EML_MIME_MAX_PARTS 128U +#define EML_MIME_MAX_DEPTH 12U +#define EML_MIME_MAX_FILENAME_LENGTH 240U +#define EML_MIME_MAX_HEADER_VALUE_LENGTH (64U * 1024U) + /** @brief Représentation d'une pièce jointe extraite d'un message EML. */ typedef struct EmlAttachment { char *part_index; /**< Chemin/index MIME (ex: "1.2") */ char *declared_filename; /**< Nom de fichier d'origine */ + char *decoded_filename; /**< Nom déclaré décodé RFC 2047/2231 */ char *sanitized_filename; /**< Nom assaini (anti path-traversal) */ char *extracted_path; /**< Chemin absolu dans 02_Preuves_Traitees */ char *relative_path; /**< Chemin relatif par rapport à la racine d'enquête */ char *content_type; /**< Type MIME déclaré */ char *detected_mime; /**< Type MIME détecté */ char *content_id; /**< Content-ID pour les images/pièces inline */ + char *normalized_content_id;/**< Content-ID sans chevrons */ + char *content_disposition; /**< Content-Disposition brut de la partie */ + char *normalized_disposition; /**< attachment, inline ou NULL */ char *transfer_encoding; /**< Content-Transfer-Encoding */ + char *extracted_at_utc; /**< Date UTC de l'extraction */ gboolean is_inline; /**< VRAI si disposition inline */ + gboolean is_attachment; /**< VRAI si disposition attachment */ + gboolean is_truncated; /**< VRAI si le contenu a dû être tronqué */ gsize encoded_size; /**< Taille encodée */ gsize decoded_size; /**< Taille décodée */ char *sha256; /**< Empreinte SHA-256 du fichier extrait */ @@ -63,6 +79,22 @@ EmlMimeResult *eml_mime_extract_attachments(const char *eml_path, const char *target_dir, GError **error); +/** + * @brief Variante annulable de l'extraction MIME. + * + * @param eml_path Chemin du fichier EML source. + * @param target_dir Dossier de destination. + * @param cancellable Objet d'annulation facultatif. + * @param error Destination d'erreur facultative. + * @return Résultat MIME, ou NULL en cas d'erreur. + */ +EmlMimeResult *eml_mime_extract_attachments_cancellable( + const char *eml_path, + const char *target_dir, + GCancellable *cancellable, + GError **error +); + G_END_DECLS #endif /* LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H */ diff --git a/src/core/bank_proposal.c b/src/core/bank_proposal.c index 64de07a..fb149bd 100644 --- a/src/core/bank_proposal.c +++ b/src/core/bank_proposal.c @@ -3,6 +3,7 @@ * @brief Détection, normalisation et modèle de proposition bancaire (IBAN, RIB, BIC). ******************************************************************************/ #include "core/bank_proposal.h" +#include "core/iban_analyzer.h" #include #include #include @@ -15,6 +16,7 @@ void bank_proposal_free(BankProposal *p) g_free(p->id); g_free(p->raw_iban); g_free(p->normalized_iban); + g_free(p->raw_bic); g_free(p->bic); g_free(p->holder_name); g_free(p->bank_name); @@ -24,6 +26,7 @@ void bank_proposal_free(BankProposal *p) g_free(p->branch_code); g_free(p->account_number); g_free(p->rib_key); + g_free(p->iban_validation); g_free(p->suggested_ocr_fix); g_free(p->verification_status); g_free(p->provenance_kind); @@ -34,22 +37,143 @@ void bank_proposal_free(BankProposal *p) g_free(p); } +static char *bank_proposal_collapse_spaces(const char *value) +{ + GString *result = NULL; + gboolean previous_was_space = FALSE; + + if (value == NULL) + return NULL; + + result = g_string_new(NULL); + for (const char *cursor = value; *cursor != '\0'; cursor++) + { + if (g_ascii_isspace(*cursor)) + { + if (!previous_was_space) + g_string_append_c(result, ' '); + previous_was_space = TRUE; + } + else + { + g_string_append_c(result, *cursor); + previous_was_space = FALSE; + } + } + g_strstrip(result->str); + return g_string_free(result, FALSE); +} + +static char *bank_proposal_extract_label( + const char *text, + const char *labels_pattern +) +{ + char *pattern = g_strdup_printf( + "(?im)^(?:%s)[ \\t]*:[ \\t]*(.+)$", + labels_pattern + ); + GRegex *regex = g_regex_new(pattern, G_REGEX_OPTIMIZE, 0, NULL); + GMatchInfo *match = NULL; + char *raw_value = NULL; + char *value = NULL; + + g_free(pattern); + g_regex_match(regex, text, 0, &match); + if (g_match_info_matches(match)) + raw_value = g_match_info_fetch(match, 1); + value = bank_proposal_collapse_spaces(raw_value); + g_free(raw_value); + g_match_info_free(match); + g_regex_unref(regex); + return value; +} + +static void bank_proposal_extract_bic( + BankProposal *proposal, + const char *text +) +{ + GRegex *regex = g_regex_new( + "(?i)\\b[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?\\b", + G_REGEX_OPTIMIZE, + 0, + NULL + ); + GMatchInfo *match = NULL; + + g_regex_match(regex, text, 0, &match); + while (g_match_info_matches(match)) + { + char *candidate = g_match_info_fetch(match, 0); + char *normalized = g_ascii_strup(candidate, -1); + + if (bank_proposal_validate_bic(normalized)) + { + proposal->raw_bic = candidate; + proposal->bic = normalized; + break; + } + g_free(candidate); + g_free(normalized); + if (!g_match_info_next(match, NULL)) + break; + } + g_match_info_free(match); + g_regex_unref(regex); +} + gboolean bank_proposal_validate_iban(const char *iban) { - if (iban == NULL) + static const struct + { + const char *country_code; + gsize length; + } national_lengths[] = { + { "BE", 16 }, { "DE", 22 }, { "ES", 24 }, { "FR", 27 }, + { "GB", 22 }, { "IT", 27 }, { "LU", 20 }, { "NL", 18 }, + { "PT", 25 } + }; + char *normalized = iban_analyzer_normalize(iban); + + if (normalized == NULL) return FALSE; - gsize len = strlen(iban); + gsize len = strlen(normalized); if (len < 15 || len > 34) + { + g_free(normalized); return FALSE; + } /* Vérification des 2 premières lettres (Code pays) */ - if (!g_ascii_isalpha(iban[0]) || !g_ascii_isalpha(iban[1])) + if (!g_ascii_isalpha(normalized[0]) || + !g_ascii_isalpha(normalized[1]) || + !g_ascii_isdigit(normalized[2]) || + !g_ascii_isdigit(normalized[3])) + { + g_free(normalized); return FALSE; + } + + for (guint index = 0; index < G_N_ELEMENTS(national_lengths); index++) + { + if (g_ascii_strncasecmp( + normalized, + national_lengths[index].country_code, + 2 + ) == 0 && + len != national_lengths[index].length) + { + g_free(normalized); + return FALSE; + } + } /* Repositionnement des 4 premiers caractères à la fin */ - GString *rearranged = g_string_new(iban + 4); - g_string_append_len(rearranged, iban, 4); + GString *rearranged = g_string_new(normalized + 4); + g_string_append_len(rearranged, normalized, 4); + g_free(normalized); /* Conversion des lettres en chiffres (A=10, Z=35) */ GString *numeric = g_string_new(NULL); @@ -99,12 +223,7 @@ gboolean bank_proposal_validate_bic(const char *bic) if (len != 8 && len != 11) return FALSE; - for (gsize i = 0; i < 4; i++) - { - if (!g_ascii_isalpha(bic[i])) - return FALSE; - } - for (gsize i = 4; i < 6; i++) + for (gsize i = 0; i < 6; i++) { if (!g_ascii_isalpha(bic[i])) return FALSE; @@ -144,43 +263,42 @@ gboolean bank_proposal_derive_french_rib(BankProposal *proposal) BankProposal *bank_proposal_analyze_text(const char *raw_text, const char *evidence_id) { + GRegex *iban_regex = NULL; + GMatchInfo *iban_match = NULL; + char *raw_iban = NULL; + char *normalized_iban = NULL; + if (raw_text == NULL || raw_text[0] == '\0') return NULL; - /* Nettoyage des espaces pour recherche d'IBAN */ - GString *clean = g_string_new(NULL); - gsize raw_len = strlen(raw_text); - for (gsize i = 0; i < raw_len; i++) - { - char c = raw_text[i]; - if (g_ascii_isalnum(c)) - { - g_string_append_c(clean, g_ascii_toupper(c)); - } - } + iban_regex = g_regex_new( + "(?i)\\b[A-Z]{2}[0-9]{2}(?:[ \\t-]*[A-Z0-9]){11,30}\\b", + G_REGEX_OPTIMIZE, + 0, + NULL + ); + g_regex_match(iban_regex, raw_text, 0, &iban_match); + if (g_match_info_matches(iban_match)) + raw_iban = g_match_info_fetch(iban_match, 0); + g_match_info_free(iban_match); + g_regex_unref(iban_regex); - /* Recherche de motif IBAN (ex: FR76...) */ - const char *data = clean->str; - const char *iban_start = strstr(data, "FR"); - if (iban_start == NULL) + normalized_iban = iban_analyzer_normalize(raw_iban); + if (normalized_iban == NULL) { - /* Essai avec d'autres codes pays à 2 lettres */ - if (clean->len >= 15 && g_ascii_isalpha(data[0]) && g_ascii_isalpha(data[1])) - iban_start = data; - } - - if (iban_start == NULL) - { - g_string_free(clean, TRUE); + g_free(raw_iban); return NULL; } BankProposal *p = g_new0(BankProposal, 1); p->id = g_uuid_string_random(); - p->raw_iban = g_strdup(raw_text); - p->normalized_iban = g_strndup(iban_start, 27 < strlen(iban_start) ? 27 : strlen(iban_start)); + p->raw_iban = raw_iban; + p->normalized_iban = normalized_iban; p->country_code = g_strndup(p->normalized_iban, 2); p->is_iban_valid = bank_proposal_validate_iban(p->normalized_iban); + p->iban_validation = g_strdup( + p->is_iban_valid ? "valid" : "invalid" + ); p->verification_status = g_strdup("proposed"); p->provenance_kind = g_strdup("ocr"); p->evidence_id = evidence_id != NULL ? g_strdup(evidence_id) : NULL; @@ -197,6 +315,37 @@ BankProposal *bank_proposal_analyze_text(const char *raw_text, const char *evide bank_proposal_derive_french_rib(p); } - g_string_free(clean, TRUE); + bank_proposal_extract_bic(p, raw_text); + p->holder_name = bank_proposal_extract_label( + raw_text, + "Titulaire|Account holder" + ); + p->bank_name = bank_proposal_extract_label( + raw_text, + "Banque|Bank" + ); + p->bank_address = bank_proposal_extract_label( + raw_text, + "Adresse(?: de la banque)?|Bank address" + ); + + if (!p->is_iban_valid && + (strchr(p->normalized_iban, 'O') != NULL || + strchr(p->normalized_iban, 'I') != NULL)) + { + char *suggestion = g_strdup(p->normalized_iban); + for (char *cursor = suggestion; *cursor != '\0'; cursor++) + { + if (*cursor == 'O') + *cursor = '0'; + else if (*cursor == 'I') + *cursor = '1'; + } + if (bank_proposal_validate_iban(suggestion)) + p->suggested_ocr_fix = suggestion; + else + g_free(suggestion); + } + return p; } diff --git a/src/core/eml_analyzer.c b/src/core/eml_analyzer.c index 8a88acc..20ea0b4 100644 --- a/src/core/eml_analyzer.c +++ b/src/core/eml_analyzer.c @@ -3,6 +3,7 @@ * @brief Analyse locale et non destructive des en-têtes d'un fichier EML. ******************************************************************************/ #include "core/eml_analyzer.h" +#include #include #define EML_ANALYZER_MAX_FILE_SIZE (25U * 1024U * 1024U) #define EML_ANALYZER_MAX_HEADER_SIZE (2U * 1024U * 1024U) @@ -15,7 +16,105 @@ struct EmlAnalysis GPtrArray *sender_ips; GPtrArray *destination_ips; char *raw_headers; + char *date_utc; }; + +static gint eml_analyzer_month_number(const char *month) +{ + static const char *months[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + + for (guint index = 0; index < G_N_ELEMENTS(months); index++) + if (g_ascii_strcasecmp(month, months[index]) == 0) + return (gint) index + 1; + + return 0; +} + +static char *eml_analyzer_normalize_date(const char *raw_date) +{ + char month_name[4] = { 0 }; + char timezone_text[6] = { 0 }; + const char *date_start = raw_date; + gint day = 0; + gint year = 0; + gint hour = 0; + gint minute = 0; + gint second = 0; + gint month = 0; + gint parsed = 0; + char timezone_identifier[7] = { 0 }; + GTimeZone *timezone = NULL; + GDateTime *date = NULL; + GDateTime *utc_date = NULL; + char *result = NULL; + + if (raw_date == NULL) + return NULL; + + const char *comma = strchr(raw_date, ','); + if (comma != NULL) + date_start = comma + 1; + + parsed = sscanf( + date_start, + " %d %3s %d %d:%d:%d %5s", + &day, + month_name, + &year, + &hour, + &minute, + &second, + timezone_text + ); + if (parsed != 7 || + strlen(timezone_text) != 5 || + (timezone_text[0] != '+' && timezone_text[0] != '-') || + !g_ascii_isdigit(timezone_text[1]) || + !g_ascii_isdigit(timezone_text[2]) || + !g_ascii_isdigit(timezone_text[3]) || + !g_ascii_isdigit(timezone_text[4])) + return NULL; + + month = eml_analyzer_month_number(month_name); + if (month == 0) + return NULL; + + g_snprintf( + timezone_identifier, + sizeof(timezone_identifier), + "%c%c%c:%c%c", + timezone_text[0], + timezone_text[1], + timezone_text[2], + timezone_text[3], + timezone_text[4] + ); + timezone = g_time_zone_new_identifier(timezone_identifier); + if (timezone == NULL) + return NULL; + + date = g_date_time_new( + timezone, + year, + month, + day, + hour, + minute, + (gdouble) second + ); + g_time_zone_unref(timezone); + if (date == NULL) + return NULL; + + utc_date = g_date_time_to_utc(date); + result = g_date_time_format(utc_date, "%Y-%m-%dT%H:%M:%SZ"); + g_date_time_unref(utc_date); + g_date_time_unref(date); + return result; +} /** @brief Libère un tableau de valeurs d'en-tête. */ static void eml_analyzer_values_free(gpointer data) { @@ -164,6 +263,9 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error) eml_analyzer_extract_received_part(received_by_regex, ip_regex, received, analysis->destination_ips); } + analysis->date_utc = eml_analyzer_normalize_date( + eml_analysis_get_first_header(analysis, "date") + ); cleanup: g_clear_pointer(&email_regex, g_regex_unref); g_clear_pointer(&domain_regex, g_regex_unref); g_clear_pointer(&ip_regex, g_regex_unref); g_clear_pointer(¤t_name, g_free); @@ -180,7 +282,7 @@ void eml_analysis_free(EmlAnalysis *analysis) g_ptr_array_unref(analysis->domains); g_ptr_array_unref(analysis->ips); g_ptr_array_unref(analysis->sender_ips); g_ptr_array_unref(analysis->destination_ips); - g_free(analysis->raw_headers); g_free(analysis); + g_free(analysis->raw_headers); g_free(analysis->date_utc); g_free(analysis); } const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis, const char *name) @@ -202,3 +304,4 @@ const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *a) { return a const GPtrArray *eml_analysis_get_sender_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->sender_ips : NULL; } const GPtrArray *eml_analysis_get_destination_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->destination_ips : NULL; } const char *eml_analysis_get_raw_headers(const EmlAnalysis *a) { return a != NULL ? a->raw_headers : NULL; } +const char *eml_analysis_get_date_utc(const EmlAnalysis *a) { return a != NULL ? a->date_utc : NULL; } diff --git a/src/core/eml_mime_extractor.c b/src/core/eml_mime_extractor.c index d05892a..9f5495a 100644 --- a/src/core/eml_mime_extractor.c +++ b/src/core/eml_mime_extractor.c @@ -1,16 +1,41 @@ /****************************************************************************** * @file eml_mime_extractor.c - * @brief Extraction MIME sécurisée et inventaire des pièces jointes d'un EML. + * @brief Extraction MIME récursive, bornée et non destructive. ******************************************************************************/ #include "core/eml_mime_extractor.h" #include "core/file_hash.h" -#include -#include -#include -#define EML_MIME_MAX_FILE_SIZE (50U * 1024U * 1024U) -#define EML_MIME_MAX_PARTS 100U -#define EML_MIME_MAX_DEPTH 10U +#include +#include +#include +#include + +#define EML_MIME_MAX_COLLISIONS 10000U +#define EML_MIME_IO_BLOCK_SIZE 65536U + +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; +} 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) { @@ -18,13 +43,18 @@ void eml_attachment_free(EmlAttachment *attachment) 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); } @@ -33,356 +63,1666 @@ void eml_mime_result_free(EmlMimeResult *result) { if (result == NULL) return; - if (result->attachments != NULL) - g_ptr_array_unref(result->attachments); - if (result->warnings != NULL) - g_ptr_array_unref(result->warnings); + g_clear_pointer(&result->attachments, g_ptr_array_unref); + g_clear_pointer(&result->warnings, g_ptr_array_unref); 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) { - if (raw_filename == NULL || raw_filename[0] == '\0') - return g_strdup("attachment.bin"); + char *valid = raw_filename != NULL + ? g_utf8_make_valid(raw_filename, -1) + : NULL; + GString *sanitized = g_string_new(NULL); - char *clean = g_strdup(raw_filename); - - /* Décodage simple RFC 2047 si présent =?...?= */ - if (strstr(clean, "=?") != NULL) + if (valid != NULL) { - /* Traitement basique ou suppression de préfixe */ - char *start = strstr(clean, "?B?"); - if (start == NULL) start = strstr(clean, "?b?"); - if (start != NULL) + for (const char *cursor = valid; + *cursor != '\0' && + sanitized->len < EML_MIME_MAX_FILENAME_LENGTH; + cursor = g_utf8_next_char(cursor)) { - char *end = strstr(start + 3, "?="); - if (end != NULL) + 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 { - *end = '\0'; - gsize out_len = 0; - guchar *decoded = g_base64_decode(start + 3, &out_len); - if (decoded != NULL && out_len > 0) + 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') { - char *valid = g_utf8_make_valid((const char *) decoded, (gssize) out_len); - g_free(clean); - clean = valid; - g_free(decoded); + 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; + + if (!relevant) + 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 (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); + 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; } } - /* Remplacement des caractères dangereux ou des séparateurs de chemin */ - gsize len = strlen(clean); - GString *sanitized = g_string_new_len(NULL, (gssize) len); + 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; +} - for (gsize i = 0; i < len; i++) +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) { - char c = clean[i]; - if (c == '/' || c == '\\' || c == ':' || c == '\0' || c == '\r' || c == '\n' || c == '\t') - { - g_string_append_c(sanitized, '_'); - } + 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 - { - g_string_append_c(sanitized, c); - } + success = eml_mime_parse_multipart( + context, + data + body_offset, + length - body_offset, + boundary, + part_index, + depth, + error + ); } - g_free(clean); - - /* Suppression des séquences '..' */ - char *res = g_strdup(sanitized->str); - g_string_free(sanitized, TRUE); - - while (strstr(res, "..") != NULL) + else if (g_str_equal(content_type, "message/rfc822")) { - char *pos = strstr(res, ".."); - pos[0] = '_'; - pos[1] = '_'; + 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_strstrip(res); - if (res[0] == '\0' || strcmp(res, ".") == 0 || strcmp(res, "..") == 0) - { - g_free(res); - return g_strdup("attachment.bin"); - } - - return res; + g_hash_table_unref(type_parameters); + g_free(content_type); + eml_mime_headers_free(headers); + return success; } -static GBytes *decode_transfer_encoding(const char *encoding, const char *raw_data, gsize raw_len) +EmlMimeResult *eml_mime_extract_attachments_cancellable( + const char *eml_path, + const char *target_dir, + GCancellable *cancellable, + GError **error +) { - if (encoding != NULL && g_ascii_strcasecmp(encoding, "base64") == 0) - { - gsize out_len = 0; - guchar *decoded = g_base64_decode(raw_data, &out_len); - if (decoded != NULL) - { - return g_bytes_new_take(decoded, out_len); - } - } + GMappedFile *mapped = NULL; + EmlMimeContext context = { 0 }; + EmlMimeResult *result = NULL; + gsize size = 0; - if (encoding != NULL && - g_ascii_strcasecmp(encoding, "quoted-printable") == 0) - { - GByteArray *decoded = g_byte_array_new(); - for (gsize index = 0; index < raw_len; index++) - { - if (raw_data[index] == '=' && index + 2 < raw_len && - raw_data[index + 1] == '\r' && raw_data[index + 2] == '\n') - { - index += 2; - continue; - } - if (raw_data[index] == '=' && index + 2 < raw_len && - g_ascii_isxdigit(raw_data[index + 1]) && - g_ascii_isxdigit(raw_data[index + 2])) - { - char hex[3] = { raw_data[index + 1], raw_data[index + 2], 0 }; - guint8 value = (guint8) g_ascii_strtoll(hex, NULL, 16); - g_byte_array_append(decoded, &value, 1); - index += 2; - continue; - } - g_byte_array_append(decoded, (const guint8 *) &raw_data[index], 1); - } - return g_byte_array_free_to_bytes(decoded); - } - - /* Traitement par défaut ou quoted-printable simple */ - return g_bytes_new(raw_data, raw_len); -} - -EmlMimeResult *eml_mime_extract_attachments(const char *eml_path, - const char *target_dir, - GError **error) -{ 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') + if (eml_path == NULL || eml_path[0] == '\0' || + target_dir == NULL || target_dir[0] == '\0') { - g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_INVAL, - "Les chemins de l'EML et du dossier cible doivent être valides."); + 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; - GMappedFile *mapped = g_mapped_file_new(eml_path, FALSE, error); + mapped = g_mapped_file_new(eml_path, FALSE, error); if (mapped == NULL) return NULL; - - gsize size = g_mapped_file_get_length(mapped); - const char *data = g_mapped_file_get_contents(mapped); - + 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); - g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_INVAL, - "Fichier EML vide ou trop volumineux."); 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); - g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_ACCES, - "Impossible de créer le dossier de destination des pièces jointes."); return NULL; } - EmlMimeResult *result = g_new0(EmlMimeResult, 1); - result->attachments = g_ptr_array_new_with_free_func((GDestroyNotify) eml_attachment_free); + 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; - /* Détection de boundary MIME si multipart */ - const char *boundary_key = "boundary="; - const char *b_pos = strstr(data, boundary_key); - char *boundary = NULL; - - if (b_pos != NULL) + if (!eml_mime_parse_entity( + &context, + g_mapped_file_get_contents(mapped), + size, + "1", + 1, + error + )) { - const char *b_start = b_pos + strlen(boundary_key); - if (*b_start == '"') - { - b_start++; - const char *b_end = strchr(b_start, '"'); - if (b_end != NULL) - boundary = g_strndup(b_start, (gsize)(b_end - b_start)); - } - else - { - const char *b_end = b_start; - while (*b_end && *b_end != '\r' && *b_end != '\n' && *b_end != ';') - b_end++; - boundary = g_strndup(b_start, (gsize)(b_end - b_start)); - } + g_free((gpointer) context.target_canonical); + g_mapped_file_unref(mapped); + eml_mime_result_free(result); + return NULL; } - - if (boundary != NULL) - { - char *delimiter = g_strdup_printf("--%s", boundary); - char **parts = g_strsplit(data, delimiter, EML_MIME_MAX_PARTS); - g_free(delimiter); - g_free(boundary); - - for (guint i = 1; parts[i] != NULL && parts[i][0] != '\0'; i++) - { - if (strncmp(parts[i], "--", 2) == 0) - break; /* Fin du multipart */ - - const char *part_content = parts[i]; - const char *hdr_end = strstr(part_content, "\r\n\r\n"); - if (hdr_end == NULL) hdr_end = strstr(part_content, "\n\n"); - if (hdr_end == NULL) continue; - - gsize hdr_len = (gsize)(hdr_end - part_content); - char *headers = g_strndup(part_content, hdr_len); - const char *body = hdr_end + (strstr(hdr_end, "\r\n\r\n") == hdr_end ? 4 : 2); - - /* Extraction du nom de fichier et content-type */ - char *filename = NULL; - const char *fn_pos = strstr(headers, "filename="); - if (fn_pos == NULL) fn_pos = strstr(headers, "name="); - if (fn_pos != NULL) - { - const char *fn_start = fn_pos + (strstr(fn_pos, "filename=") == fn_pos ? 9 : 5); - if (*fn_start == '"') - { - fn_start++; - const char *fn_end = strchr(fn_start, '"'); - if (fn_end != NULL) filename = g_strndup(fn_start, (gsize)(fn_end - fn_start)); - } - else - { - const char *fn_end = fn_start; - while (*fn_end && *fn_end != '\r' && *fn_end != '\n' && *fn_end != ';') fn_end++; - filename = g_strndup(fn_start, (gsize)(fn_end - fn_start)); - } - } - - gboolean is_attachment = (strstr(headers, "attachment") != NULL) || (filename != NULL); - if (is_attachment) - { - char *sanitized = eml_mime_sanitize_filename(filename); - char *dest_path = g_build_filename(target_dir, sanitized, NULL); - - /* Gestion des collisions */ - guint counter = 1; - while (g_file_test(dest_path, G_FILE_TEST_EXISTS)) - { - g_free(dest_path); - char *new_name = g_strdup_printf("%u_%s", counter++, sanitized); - dest_path = g_build_filename(target_dir, new_name, NULL); - g_free(new_name); - } - - /* Extraction du Content-Transfer-Encoding */ - char *encoding = NULL; - const char *enc_pos = strstr(headers, "Content-Transfer-Encoding:"); - if (enc_pos != NULL) - { - const char *enc_val = enc_pos + 26; - const char *enc_end = strchr(enc_val, '\n'); - if (enc_end != NULL) encoding = g_strndup(enc_val, (gsize)(enc_end - enc_val)); - if (encoding != NULL) g_strstrip(encoding); - } - - /* Extraction du Content-Type */ - char *content_type = NULL; - const char *ct_pos = strstr(headers, "Content-Type:"); - if (ct_pos != NULL) - { - const char *ct_val = ct_pos + 13; - const char *ct_end = strchr(ct_val, ';'); - if (ct_end == NULL) ct_end = strchr(ct_val, '\n'); - if (ct_end != NULL) content_type = g_strndup(ct_val, (gsize)(ct_end - ct_val)); - if (content_type != NULL) g_strstrip(content_type); - } - - /* Extraction du Content-ID */ - char *content_id = NULL; - const char *cid_pos = strstr(headers, "Content-ID:"); - if (cid_pos != NULL) - { - const char *cid_val = cid_pos + 11; - const char *cid_end = strchr(cid_val, '\n'); - if (cid_end != NULL) content_id = g_strndup(cid_val, (gsize)(cid_end - cid_val)); - if (content_id != NULL) g_strstrip(content_id); - } - - gsize body_len = strlen(body); - GBytes *decoded_bytes = decode_transfer_encoding(encoding, body, body_len); - gsize decoded_len = 0; - gconstpointer decoded_data = g_bytes_get_data(decoded_bytes, &decoded_len); - - GError *write_error = NULL; - if (g_file_set_contents(dest_path, decoded_data, (gssize) decoded_len, &write_error)) - { - char *sha256 = NULL; - guint64 file_size = 0; - file_hash_compute_sha256(dest_path, NULL, &sha256, - &file_size, NULL); - - EmlAttachment *att = g_new0(EmlAttachment, 1); - att->part_index = g_strdup_printf("1.%u", i); - att->declared_filename = filename != NULL ? g_strdup(filename) : g_strdup("attachment.bin"); - att->sanitized_filename = g_strdup(sanitized); - att->extracted_path = dest_path; dest_path = NULL; - att->content_type = content_type != NULL ? content_type : g_strdup("application/octet-stream"); content_type = NULL; - att->content_id = content_id; content_id = NULL; - att->transfer_encoding = encoding != NULL ? encoding : g_strdup("7bit"); encoding = NULL; - att->is_inline = (strstr(headers, "inline") != NULL); - att->encoded_size = body_len; - att->decoded_size = decoded_len; - att->sha256 = sha256; - { - char *target_name = g_path_get_basename(target_dir); - att->relative_path = g_build_filename( - "02_Preuves_Traitees", "eml_attachments", - target_name, sanitized, NULL); - g_free(target_name); - } - if (content_type != NULL && - g_content_type_is_a(content_type, "text/plain")) - att->detected_mime = g_strdup("text/plain"); - else - att->detected_mime = g_content_type_guess( - dest_path != NULL ? dest_path : att->extracted_path, - decoded_data, decoded_len, NULL); - if (att->detected_mime == NULL) - att->detected_mime = g_strdup("application/octet-stream"); - { - const char *dot = strrchr(att->sanitized_filename, '.'); - if (dot != NULL && g_ascii_strcasecmp(dot + 1, "txt") == 0 && - g_strcmp0(att->detected_mime, "text/plain") != 0) - att->has_inconsistency = TRUE; - } - - g_ptr_array_add(result->attachments, att); - } - else - { - char *warn = g_strdup_printf("Impossible d'écrire la pièce jointe %s : %s", - sanitized, write_error->message); - g_ptr_array_add(result->warnings, warn); - g_error_free(write_error); - g_free(dest_path); - } - - g_bytes_unref(decoded_bytes); - g_free(content_type); - g_free(content_id); - g_free(encoding); - g_free(sanitized); - } - - g_free(filename); - g_free(headers); - } - - g_strfreev(parts); - } - + 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 + ); +} diff --git a/src/core/eml_pipeline_task.c b/src/core/eml_pipeline_task.c index 0aa166e..317e6c5 100644 --- a/src/core/eml_pipeline_task.c +++ b/src/core/eml_pipeline_task.c @@ -77,12 +77,24 @@ static gboolean eml_pipeline_task_worker(BackgroundTask *task, return FALSE; } - EmlMimeResult *mime_res = eml_mime_extract_attachments(data->eml_path, target_dir, error); + EmlMimeResult *mime_res = eml_mime_extract_attachments_cancellable( + data->eml_path, + target_dir, + cancellable, + error + ); g_free(target_dir); if (mime_res == NULL) { + if (g_cancellable_is_cancelled(cancellable)) + { + eml_analysis_free(analysis); + return FALSE; + } + /* Si l'extraction MIME échoue, on conserve quand même l'analyse des en-têtes (résultat partiel) */ + g_clear_error(error); mime_res = g_new0(EmlMimeResult, 1); mime_res->attachments = g_ptr_array_new_with_free_func((GDestroyNotify) eml_attachment_free); mime_res->warnings = g_ptr_array_new_with_free_func(g_free); @@ -94,6 +106,20 @@ static gboolean eml_pipeline_task_worker(BackgroundTask *task, for (guint i = 0; mime_res->attachments != NULL && i < mime_res->attachments->len; i++) { + if (g_cancellable_is_cancelled(cancellable)) + { + eml_analysis_free(analysis); + eml_mime_result_free(mime_res); + g_ptr_array_unref(bank_proposals); + g_set_error_literal( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "L'analyse EML a été annulée." + ); + return FALSE; + } + EmlAttachment *att = g_ptr_array_index(mime_res->attachments, i); if (att->extracted_path == NULL) continue; diff --git a/tests/test_bank_proposal.c b/tests/test_bank_proposal.c index 2c1beec..fe170ec 100644 --- a/tests/test_bank_proposal.c +++ b/tests/test_bank_proposal.c @@ -47,6 +47,33 @@ static void test_french_rib_derivation(void) bank_proposal_free(proposal); } +static void test_structured_values(void) +{ + static const char text[] = + "IBAN : FR48 3000 2005 5000 0000 0000 052\n" + "BIC : bnpafrppxxx\n" + "Titulaire : Élodie Exemple\n" + "Banque : Banque Synthétique\n" + "Adresse de la banque : 1 rue des Tests\n"; + BankProposal *proposal = bank_proposal_analyze_text( + text, + "synthetic-evidence" + ); + + g_assert_nonnull(proposal); + g_assert_cmpstr(proposal->raw_iban, ==, + "FR48 3000 2005 5000 0000 0000 052"); + g_assert_cmpstr(proposal->normalized_iban, ==, + "FR4830002005500000000000052"); + g_assert_cmpstr(proposal->raw_bic, ==, "bnpafrppxxx"); + g_assert_cmpstr(proposal->bic, ==, "BNPAFRPPXXX"); + g_assert_cmpstr(proposal->holder_name, ==, "Élodie Exemple"); + g_assert_cmpstr(proposal->bank_name, ==, "Banque Synthétique"); + g_assert_cmpstr(proposal->bank_address, ==, "1 rue des Tests"); + g_assert_cmpstr(proposal->iban_validation, ==, "valid"); + bank_proposal_free(proposal); +} + int main(int argc, char **argv) { @@ -54,5 +81,6 @@ int main(int argc, char **argv) g_test_add_func("/bank-proposal/iban-validation", test_iban_validation); g_test_add_func("/bank-proposal/bic-validation", test_bic_validation); g_test_add_func("/bank-proposal/french-rib-derivation", test_french_rib_derivation); + g_test_add_func("/bank-proposal/structured-values", test_structured_values); return g_test_run(); } diff --git a/tests/test_eml_analyzer.c b/tests/test_eml_analyzer.c index 5ad8fa5..80c5520 100644 --- a/tests/test_eml_analyzer.c +++ b/tests/test_eml_analyzer.c @@ -16,6 +16,7 @@ static void test_eml_analyzer_headers(void) "Reply-To: replies@reply.test\r\n" "To: victim@example.net\r\n" "Subject: Synthetic fixture\r\n" + "Date: Wed, 22 Jul 2026 12:00:00 +0200\r\n" "Message-ID: \r\n" "Received: from mail.example.test (mail.example.test [192.0.2.10])\r\n" " by mx.example.net ([198.51.100.20]) with ESMTP; Wed, 22 Jul 2026 10:00:00 +0000\r\n" @@ -35,6 +36,8 @@ static void test_eml_analyzer_headers(void) assert(analysis != NULL && error == NULL); assert(strcmp(eml_analysis_get_first_header(analysis, "from"), "Example Sender ") == 0); + assert(strcmp(eml_analysis_get_date_utc(analysis), + "2026-07-22T10:00:00Z") == 0); received = eml_analysis_get_header_values(analysis, "Received"); assert(received != NULL && received->len == 2); assert(strstr(g_ptr_array_index((GPtrArray *) received, 0), " by mx.example.net") != NULL); diff --git a/tests/test_eml_mime_extractor.c b/tests/test_eml_mime_extractor.c new file mode 100644 index 0000000..6910fd2 --- /dev/null +++ b/tests/test_eml_mime_extractor.c @@ -0,0 +1,736 @@ +/****************************************************************************** + * @file test_eml_mime_extractor.c + * @brief Tests synthétiques de l'extracteur MIME récursif. + ******************************************************************************/ +#include "core/eml_mime_extractor.h" + +#include +#include + +typedef struct +{ + char *directory; + char *eml_path; + char *output_directory; +} MimeFixture; + +static MimeFixture *mime_fixture_new(const char *content) +{ + GError *error = NULL; + MimeFixture *fixture = g_new0(MimeFixture, 1); + fixture->directory = g_dir_make_tmp("labfy-mime-XXXXXX", &error); + g_assert_no_error(error); + fixture->eml_path = g_build_filename( + fixture->directory, + "synthetic.eml", + NULL + ); + fixture->output_directory = g_build_filename( + fixture->directory, + "derived", + NULL + ); + g_assert_true(g_file_set_contents( + fixture->eml_path, + content, + -1, + &error + )); + g_assert_no_error(error); + return fixture; +} + +static void mime_fixture_free(MimeFixture *fixture) +{ + GDir *directory = g_dir_open(fixture->output_directory, 0, NULL); + if (directory != NULL) + { + const char *name = NULL; + while ((name = g_dir_read_name(directory)) != NULL) + { + char *path = g_build_filename( + fixture->output_directory, + name, + NULL + ); + g_assert_cmpint(g_remove(path), ==, 0); + g_free(path); + } + g_dir_close(directory); + g_assert_cmpint(g_rmdir(fixture->output_directory), ==, 0); + } + g_assert_cmpint(g_remove(fixture->eml_path), ==, 0); + g_assert_cmpint(g_rmdir(fixture->directory), ==, 0); + g_free(fixture->output_directory); + g_free(fixture->eml_path); + g_free(fixture->directory); + g_free(fixture); +} + +static char *attachment_contents(EmlAttachment *attachment) +{ + char *contents = NULL; + GError *error = NULL; + g_assert_true(g_file_get_contents( + attachment->extracted_path, + &contents, + NULL, + &error + )); + g_assert_no_error(error); + return contents; +} + +static void test_nested_order_and_encodings(void) +{ + static const char eml[] = + "MIME-Version: 1.0\r\n" + "Content-Type: multipart/mixed; boundary=outer\r\n\r\n" + "--outer\r\nContent-Type: text/plain\r\n\r\nbody\r\n" + "--outer\r\n" + "Content-Type: multipart/related; boundary=inner\r\n\r\n" + "--inner\r\nContent-Type: image/png\r\n" + "Content-Disposition: inline\r\n" + "Content-ID: \r\n" + "Content-Transfer-Encoding: base64\r\n\r\n" + "UE5H\r\n" + "--inner\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment;\r\n" + " filename*0*=UTF-8''rapport%20;\r\n" + " filename*1*=synth%C3%A9tique.txt\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n\r\n" + "ligne=20une=\r\nligne=20deux\r\n" + "--inner--\r\n" + "--outer\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment;\r\n" + " filename=\"=?UTF-8?Q?troisi=C3=A8me.txt?=\"\r\n\r\n" + "third\r\n--outer--\r\n"; + MimeFixture *fixture = mime_fixture_new(eml); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_nonnull(result); + g_assert_cmpuint(result->attachments->len, ==, 3); + + EmlAttachment *first = g_ptr_array_index(result->attachments, 0); + EmlAttachment *second = g_ptr_array_index(result->attachments, 1); + EmlAttachment *third = g_ptr_array_index(result->attachments, 2); + g_assert_cmpstr(first->part_index, ==, "1.2.1"); + g_assert_true(first->is_inline); + g_assert_false(first->is_attachment); + g_assert_cmpstr(first->normalized_content_id, ==, + "synthetic-image@test.invalid"); + g_assert_cmpstr(second->part_index, ==, "1.2.2"); + g_assert_cmpstr(second->sanitized_filename, ==, + "rapport synthétique.txt"); + g_assert_cmpstr(third->part_index, ==, "1.3"); + g_assert_cmpstr(third->sanitized_filename, ==, "troisième.txt"); + + char *first_content = attachment_contents(first); + char *second_content = attachment_contents(second); + char *third_content = attachment_contents(third); + g_assert_cmpstr(first_content, ==, "PNG"); + g_assert_cmpstr(second_content, ==, "ligne uneligne deux"); + g_assert_cmpstr(third_content, ==, "third"); + g_assert_nonnull(first->sha256); + g_assert_nonnull(first->detected_mime); + g_free(first_content); + g_free(second_content); + g_free(third_content); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +static void test_three_levels_and_message(void) +{ + static const char eml[] = + "Content-Type: multipart/mixed; boundary=a\r\n\r\n" + "--a\r\nContent-Type: multipart/alternative; boundary=b\r\n\r\n" + "--b\r\nContent-Type: multipart/related; boundary=c\r\n\r\n" + "--c\r\nContent-Type: text/plain; name=four.txt\r\n" + "Content-Disposition: attachment\r\n\r\nfour\r\n--c--\r\n" + "--b--\r\n--a\r\nContent-Type: message/rfc822\r\n\r\n" + "Content-Type: text/plain; name=inside.txt\r\n" + "Content-Disposition: attachment\r\n\r\ninside\r\n" + "--a--\r\n"; + MimeFixture *fixture = mime_fixture_new(eml); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 2); + EmlAttachment *first = g_ptr_array_index(result->attachments, 0); + EmlAttachment *second = g_ptr_array_index(result->attachments, 1); + g_assert_cmpstr(first->part_index, ==, "1.1.1.1"); + g_assert_cmpstr(second->part_index, ==, "1.2.1"); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +static void test_rfc2047_and_rfc2231_priorities(void) +{ + static const char eml[] = + "Content-Type: multipart/mixed; boundary=x\r\n\r\n" + "--x\r\nContent-Type: text/plain; name=fallback.txt\r\n" + "Content-Disposition: attachment; filename=plain.txt;\r\n" + " filename*=ISO-8859-1''caf%E9.txt\r\n\r\none\r\n" + "--x\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment;\r\n" + " filename=\"ASCII =?UTF-8?B?w6l0dWRl?=.txt\"\r\n\r\ntwo\r\n" + "--x\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment;\r\n" + " filename*0=continued-; filename*1=name.txt\r\n\r\nthree\r\n" + "--x\r\nContent-Type: text/plain;\r\n" + " name*=UTF-8''type%20fallback.txt\r\n" + "Content-Disposition: inline\r\n\r\nfour\r\n" + "--x--\r\n"; + MimeFixture *fixture = mime_fixture_new(eml); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 4); + g_assert_cmpstr( + ((EmlAttachment *) g_ptr_array_index( + result->attachments, 0))->sanitized_filename, + ==, + "café.txt" + ); + g_assert_cmpstr( + ((EmlAttachment *) g_ptr_array_index( + result->attachments, 1))->sanitized_filename, + ==, + "ASCII étude.txt" + ); + g_assert_cmpstr( + ((EmlAttachment *) g_ptr_array_index( + result->attachments, 2))->sanitized_filename, + ==, + "continued-name.txt" + ); + g_assert_cmpstr( + ((EmlAttachment *) g_ptr_array_index( + result->attachments, 3))->sanitized_filename, + ==, + "type fallback.txt" + ); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +static void test_invalid_encodings_are_warnings(void) +{ + static const char eml[] = + "Content-Type: multipart/mixed; boundary=x\r\n\r\n" + "--x\r\nContent-Type: text/plain; name=a.txt\r\n" + "Content-Disposition: attachment\r\n" + "Content-Transfer-Encoding: base64\r\n\r\nA!AA\r\n" + "--x\r\nContent-Type: text/plain; name=b.txt\r\n" + "Content-Disposition: attachment\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n\r\nbad=QZ\r\n" + "--x\r\nContent-Type: text/plain; name=c.txt\r\n" + "Content-Disposition: attachment\r\n" + "Content-Transfer-Encoding: synthetic\r\n\r\nbad\r\n" + "--x--\r\n"; + MimeFixture *fixture = mime_fixture_new(eml); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 0); + g_assert_cmpuint(result->warnings->len, ==, 3); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +static void test_paths_and_collisions(void) +{ + static const char eml[] = + "Content-Type: multipart/mixed; boundary=x\r\n\r\n" + "--x\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment; filename=\"../same.txt\"\r\n\r\n1\r\n" + "--x\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment; filename=\"C:\\\\same.txt\"\r\n\r\n2\r\n" + "--x\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment; filename=\"/same.txt\"\r\n\r\n3\r\n" + "--x\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment; filename=\"../same.txt\"\r\n\r\n4\r\n" + "--x--\r\n"; + MimeFixture *fixture = mime_fixture_new(eml); + GError *error = NULL; + g_assert_cmpint(g_mkdir_with_parents( + fixture->output_directory, 0755), ==, 0); + char *existing = g_build_filename( + fixture->output_directory, + "___same.txt", + NULL + ); + g_assert_true(g_file_set_contents(existing, "existing", -1, &error)); + g_assert_no_error(error); + + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 4); + for (guint index = 0; index < result->attachments->len; index++) + { + EmlAttachment *attachment = g_ptr_array_index( + result->attachments, + index + ); + g_assert_null(strchr(attachment->sanitized_filename, '/')); + g_assert_null(strchr(attachment->sanitized_filename, '\\')); + } + char *existing_content = NULL; + g_assert_true(g_file_get_contents( + existing, &existing_content, NULL, &error)); + g_assert_no_error(error); + g_assert_cmpstr(existing_content, ==, "existing"); + g_free(existing_content); + g_free(existing); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +static void test_malformed_and_incomplete_rfc2231(void) +{ + static const char eml[] = + "Content-Type: multipart/mixed; boundary=x\r\n\r\n" + "--x\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment; filename*0*=UTF-8''bad;\r\n" + " filename*2*=gap.txt; filename=fallback.txt\r\n\r\nok\r\n" + "--x\r\nContent-Type: text/plain\r\n" + "Content-Disposition: attachment; filename*0*=UTF-8''one;\r\n" + " filename*0*=duplicate; filename=duplicate-fallback.txt\r\n\r\ntwo\r\n"; + MimeFixture *fixture = mime_fixture_new(eml); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 2); + EmlAttachment *attachment = g_ptr_array_index(result->attachments, 0); + g_assert_cmpstr(attachment->sanitized_filename, ==, "fallback.txt"); + attachment = g_ptr_array_index(result->attachments, 1); + g_assert_cmpstr( + attachment->sanitized_filename, + ==, + "duplicate-fallback.txt" + ); + g_assert_cmpuint(result->warnings->len, >=, 1); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +static void test_missing_boundary(void) +{ + static const char eml[] = + "Content-Type: multipart/mixed\r\n\r\nnot structured"; + MimeFixture *fixture = mime_fixture_new(eml); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 0); + g_assert_cmpuint(result->warnings->len, ==, 1); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +static void test_cancelled_before_extraction(void) +{ + static const char eml[] = + "Content-Type: text/plain; name=a.txt\r\n" + "Content-Disposition: attachment\r\n\r\ncontent"; + MimeFixture *fixture = mime_fixture_new(eml); + GCancellable *cancellable = g_cancellable_new(); + GError *error = NULL; + g_cancellable_cancel(cancellable); + EmlMimeResult *result = eml_mime_extract_attachments_cancellable( + fixture->eml_path, + fixture->output_directory, + cancellable, + &error + ); + g_assert_null(result); + g_assert_error(error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + g_clear_error(&error); + g_object_unref(cancellable); + mime_fixture_free(fixture); +} + +static void test_source_unchanged(void) +{ + static const char eml[] = + "Content-Type: text/plain; name=a.txt\r\n" + "Content-Disposition: attachment\r\n\r\nimmutable"; + MimeFixture *fixture = mime_fixture_new(eml); + char *before = NULL; + char *after = NULL; + GError *error = NULL; + g_assert_true(g_file_get_contents( + fixture->eml_path, &before, NULL, &error)); + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_true(g_file_get_contents( + fixture->eml_path, &after, NULL, &error)); + g_assert_no_error(error); + g_assert_cmpstr(before, ==, after); + g_free(before); + g_free(after); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +static void test_filename_sanitizer_limits(void) +{ + char *empty = eml_mime_sanitize_filename(" "); + char *unix_path = eml_mime_sanitize_filename("../../absolute/test"); + char *windows_path = eml_mime_sanitize_filename("C:\\temp\\test"); + char *long_name = g_strnfill( + EML_MIME_MAX_FILENAME_LENGTH + 100, + 'a' + ); + char *shortened = eml_mime_sanitize_filename(long_name); + + g_assert_cmpstr(empty, ==, "attachment.bin"); + g_assert_null(strchr(unix_path, '/')); + g_assert_null(strchr(windows_path, '\\')); + g_assert_cmpuint( + strlen(shortened), + <=, + EML_MIME_MAX_FILENAME_LENGTH + ); + g_free(empty); + g_free(unix_path); + g_free(windows_path); + g_free(long_name); + g_free(shortened); +} + +static void test_part_count_limit(void) +{ + GString *eml = g_string_new( + "Content-Type: multipart/mixed; boundary=x\r\n\r\n" + ); + for (guint index = 0; index < EML_MIME_MAX_PARTS + 4; index++) + g_string_append_printf( + eml, + "--x\r\nContent-Type: text/plain; name=p%u.txt\r\n" + "Content-Disposition: attachment\r\n\r\n%u\r\n", + index, + index + ); + g_string_append(eml, "--x--\r\n"); + MimeFixture *fixture = mime_fixture_new(eml->str); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint( + result->attachments->len, + ==, + EML_MIME_MAX_PARTS - 1 + ); + g_assert_cmpuint(result->warnings->len, >, 0); + eml_mime_result_free(result); + mime_fixture_free(fixture); + g_string_free(eml, TRUE); +} + +static void test_depth_limit(void) +{ + GString *eml = g_string_new(NULL); + for (guint depth = 1; depth <= EML_MIME_MAX_DEPTH + 1; depth++) + g_string_append_printf( + eml, + "Content-Type: multipart/mixed; boundary=b%u\r\n\r\n--b%u\r\n", + depth, + depth + ); + g_string_append( + eml, + "Content-Type: text/plain; name=too-deep.txt\r\n" + "Content-Disposition: attachment\r\n\r\ndeep\r\n" + ); + for (gint depth = (gint) EML_MIME_MAX_DEPTH + 1; depth >= 1; depth--) + g_string_append_printf(eml, "--b%d--\r\n", depth); + + MimeFixture *fixture = mime_fixture_new(eml->str); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 0); + g_assert_cmpuint(result->warnings->len, >, 0); + eml_mime_result_free(result); + mime_fixture_free(fixture); + g_string_free(eml, TRUE); +} + +static void test_part_size_and_no_temporary_file(void) +{ + GString *eml = g_string_new( + "Content-Type: text/plain; name=large.txt\r\n" + "Content-Disposition: attachment\r\n\r\n" + ); + char *large_content = g_strnfill( + EML_MIME_MAX_PART_DECODED_SIZE + 1, + 'x' + ); + g_string_append_len( + eml, + large_content, + EML_MIME_MAX_PART_DECODED_SIZE + 1 + ); + g_free(large_content); + MimeFixture *fixture = mime_fixture_new(eml->str); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 0); + g_assert_cmpuint(result->warnings->len, ==, 1); + GDir *directory = g_dir_open(fixture->output_directory, 0, &error); + g_assert_no_error(error); + g_assert_null(g_dir_read_name(directory)); + g_dir_close(directory); + eml_mime_result_free(result); + mime_fixture_free(fixture); + g_string_free(eml, TRUE); +} + +static void test_empty_base64_and_malformed_header(void) +{ + static const char valid[] = + "Content-Type: application/octet-stream; name=empty.bin\r\n" + "Content-Disposition: attachment\r\n" + "Content-Transfer-Encoding: base64\r\n\r\n"; + MimeFixture *fixture = mime_fixture_new(valid); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 1); + EmlAttachment *attachment = g_ptr_array_index(result->attachments, 0); + g_assert_cmpuint(attachment->decoded_size, ==, 0); + eml_mime_result_free(result); + mime_fixture_free(fixture); + + fixture = mime_fixture_new("Malformed header\r\n\r\nbody"); + result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->warnings->len, ==, 1); + eml_mime_result_free(result); + mime_fixture_free(fixture); +} + +typedef struct +{ + GCancellable *cancellable; + const char *output_directory; +} CancellationData; + +static gpointer cancel_when_extraction_starts(gpointer user_data) +{ + CancellationData *data = user_data; + while (!g_file_test(data->output_directory, G_FILE_TEST_IS_DIR)) + g_thread_yield(); + g_cancellable_cancel(data->cancellable); + return NULL; +} + +static void test_cancelled_during_extraction(void) +{ + GString *eml = g_string_new( + "Content-Type: text/plain; name=a.txt\r\n" + "Content-Disposition: attachment\r\n\r\n" + ); + char *large_content = g_strnfill( + EML_MIME_MAX_PART_DECODED_SIZE, + 'c' + ); + g_string_append_len( + eml, + large_content, + EML_MIME_MAX_PART_DECODED_SIZE + ); + g_free(large_content); + MimeFixture *fixture = mime_fixture_new(eml->str); + GCancellable *cancellable = g_cancellable_new(); + CancellationData data = { + .cancellable = cancellable, + .output_directory = fixture->output_directory + }; + GThread *thread = g_thread_new( + "mime-cancel", + cancel_when_extraction_starts, + &data + ); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments_cancellable( + fixture->eml_path, + fixture->output_directory, + cancellable, + &error + ); + g_thread_join(thread); + g_assert_null(result); + g_assert_error(error, G_IO_ERROR, G_IO_ERROR_CANCELLED); + g_clear_error(&error); + GDir *directory = g_dir_open(fixture->output_directory, 0, &error); + g_assert_no_error(error); + g_assert_null(g_dir_read_name(directory)); + g_dir_close(directory); + g_object_unref(cancellable); + mime_fixture_free(fixture); + g_string_free(eml, TRUE); +} + +static void test_total_decoded_limit(void) +{ + const gsize part_length = 7U * 1024U * 1024U; + char *part_content = g_strnfill(part_length, 'z'); + GString *eml = g_string_new( + "Content-Type: multipart/mixed; boundary=total\r\n\r\n" + ); + for (guint index = 0; index < 5; index++) + { + g_string_append_printf( + eml, + "--total\r\nContent-Type: application/octet-stream; " + "name=large-%u.bin\r\n" + "Content-Disposition: attachment\r\n\r\n", + index + ); + g_string_append_len(eml, part_content, (gssize) part_length); + g_string_append(eml, "\r\n"); + } + g_string_append(eml, "--total--\r\n"); + g_free(part_content); + + MimeFixture *fixture = mime_fixture_new(eml->str); + GError *error = NULL; + EmlMimeResult *result = eml_mime_extract_attachments( + fixture->eml_path, + fixture->output_directory, + &error + ); + g_assert_no_error(error); + g_assert_cmpuint(result->attachments->len, ==, 4); + g_assert_cmpuint(result->warnings->len, ==, 1); + eml_mime_result_free(result); + mime_fixture_free(fixture); + g_string_free(eml, TRUE); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + g_test_add_func( + "/eml-mime/nested-order-encodings", + test_nested_order_and_encodings + ); + g_test_add_func( + "/eml-mime/three-levels-message", + test_three_levels_and_message + ); + g_test_add_func( + "/eml-mime/rfc2047-rfc2231-priorities", + test_rfc2047_and_rfc2231_priorities + ); + g_test_add_func( + "/eml-mime/invalid-encodings", + test_invalid_encodings_are_warnings + ); + g_test_add_func( + "/eml-mime/paths-collisions", + test_paths_and_collisions + ); + g_test_add_func( + "/eml-mime/malformed-rfc2231", + test_malformed_and_incomplete_rfc2231 + ); + g_test_add_func( + "/eml-mime/missing-boundary", + test_missing_boundary + ); + g_test_add_func( + "/eml-mime/cancelled-before", + test_cancelled_before_extraction + ); + g_test_add_func( + "/eml-mime/source-unchanged", + test_source_unchanged + ); + g_test_add_func( + "/eml-mime/filename-sanitizer-limits", + test_filename_sanitizer_limits + ); + g_test_add_func( + "/eml-mime/part-count-limit", + test_part_count_limit + ); + g_test_add_func( + "/eml-mime/depth-limit", + test_depth_limit + ); + g_test_add_func( + "/eml-mime/part-size-no-temporary", + test_part_size_and_no_temporary_file + ); + g_test_add_func( + "/eml-mime/empty-base64-malformed-header", + test_empty_base64_and_malformed_header + ); + g_test_add_func( + "/eml-mime/cancelled-during", + test_cancelled_during_extraction + ); + g_test_add_func( + "/eml-mime/total-decoded-limit", + test_total_decoded_limit + ); + return g_test_run(); +} diff --git a/tests/test_eml_pipeline_task.c b/tests/test_eml_pipeline_task.c index 004e3fa..a450dd6 100644 --- a/tests/test_eml_pipeline_task.c +++ b/tests/test_eml_pipeline_task.c @@ -68,9 +68,9 @@ static void test_eml_pipeline_basic(void) EmlAttachment *att = g_ptr_array_index(result->mime_result->attachments, 0); g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt"); att = g_ptr_array_index(result->mime_result->attachments, 1); - g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt"); + g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect-2.txt"); g_assert_true(g_str_has_suffix(att->extracted_path, - "/1____rib_suspect.txt")); + "/___rib_suspect-2.txt")); g_assert_cmpuint(att->decoded_size, >, 0U); /* Vérification de la détection de la proposition bancaire dans la pièce jointe */