feat: renforcer le pipeline EML et l'extraction MIME

This commit is contained in:
grayTerminal-sh 2026-07-27 20:19:42 +02:00
parent 8fcd6b0e0d
commit 58fbf2daae
12 changed files with 2789 additions and 353 deletions

View file

@ -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)

View file

@ -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. */

View file

@ -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

View file

@ -5,23 +5,39 @@
#ifndef LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
#define LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
#include <glib.h>
#include <gio/gio.h>
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 */

View file

@ -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 <ctype.h>
#include <stdio.h>
#include <stdlib.h>
@ -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;
}

View file

@ -3,6 +3,7 @@
* @brief Analyse locale et non destructive des en-têtes d'un fichier EML.
******************************************************************************/
#include "core/eml_analyzer.h"
#include <stdio.h>
#include <string.h>
#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(&current_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; }

File diff suppressed because it is too large Load diff

View file

@ -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;

View file

@ -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();
}

View file

@ -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: <id-123@example.test>\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 <sender@example.test>") == 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);

View file

@ -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 <glib.h>
#include <glib/gstdio.h>
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: <synthetic-image@test.invalid>\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();
}

View file

@ -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 */