feat: renforcer le pipeline EML et l'extraction MIME
This commit is contained in:
parent
8fcd6b0e0d
commit
58fbf2daae
12 changed files with 2789 additions and 353 deletions
15
Makefile
15
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)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(¤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; }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
736
tests/test_eml_mime_extractor.c
Normal file
736
tests/test_eml_mime_extractor.c
Normal 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();
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Reference in a new issue