diff --git a/src/logwatcher/cli.py b/src/logwatcher/cli.py index fd64243..3d83ac9 100644 --- a/src/logwatcher/cli.py +++ b/src/logwatcher/cli.py @@ -12,6 +12,7 @@ from logwatcher.mail_reader import ( connect_to_mailbox, extract_logs_from_mails, fetch_log_messages, + move_analyzed_mails, ) from logwatcher.models import LogEntry from logwatcher.parser import parse_file, parse_lines @@ -90,6 +91,7 @@ def from_mail( write_log_report( SourceType.MAIL, relevant, irrelevant, nb_files, output_dir=output_dir ) + move_analyzed_mails(account, messages) logger.info("============ logwatcher mail ended ============") diff --git a/src/logwatcher/mail_reader.py b/src/logwatcher/mail_reader.py index d70127a..efb7c59 100644 --- a/src/logwatcher/mail_reader.py +++ b/src/logwatcher/mail_reader.py @@ -9,6 +9,7 @@ from exchangelib import ( Configuration, Credentials, FileAttachment, + Folder, Message, Version, ) @@ -17,7 +18,8 @@ from exchangelib.errors import UnauthorizedError logger = logging.getLogger(__name__) -MAILBOX_DIR = "Logs" +LOG_DIR = "Logs" +ANALYZED_LOG_DIR = "Analyzed" LOG_IN_ATTACHMENT_PATTERN = "Le compte-rendu contient plus de 100 lignes." @@ -138,7 +140,7 @@ def fetch_log_messages(account: Account) -> list[Message]: Returns: queryset of log-related mails """ - logs_folder = account.msg_folder_root / MAILBOX_DIR + logs_folder = account.msg_folder_root / LOG_DIR return list(logs_folder.all()) @@ -178,3 +180,57 @@ def extract_logs_from_mails(messages: list[Message]) -> list[str]: content = _get_mail_body(message) logs.extend(extract_log_lines(content)) return logs + + +def _get_or_create_analyzed_folder(account: Account) -> Folder: + """ + Return the 'Analyzed' folder, creating it if it doesn't exist. + + Args: + account: The mailbox account owning the Logs folder. + + Returns: + The existing or newly created 'Analyzed' folder. + + """ + log_folder = account.msg_folder_root / LOG_DIR + matches = list(log_folder.glob(ANALYZED_LOG_DIR)) + + if matches: + logger.info(f"\tFolder '{ANALYZED_LOG_DIR}' found.") + return matches[0] + + logger.info(f"\tCreating '{ANALYZED_LOG_DIR}' folder.") + analyzed_folder = Folder(parent=log_folder, name=ANALYZED_LOG_DIR) + analyzed_folder.save() + return analyzed_folder + + +def move_analyzed_mails(account: Account, messages: list[Message]) -> None: + """ + Move processed mails from the Logs folder to the Analyzed subfolder. + + Args: + account: The mailbox account owning the Logs folder. + messages: The processed mails to move. + + Note: + Partial failure is possible: if a move fails mid-batch, some mails + may remain in Logs and be retried on the next run (at-least-once). + """ + if not messages: + logger.info("No mails to move.") + return + + logger.info( + f"Moving {len(messages)} mails from '{LOG_DIR}' directory to '{ANALYZED_LOG_DIR}'" + ) + + analyzed_folder = _get_or_create_analyzed_folder(account) + + ids = [(m.id, m.changekey) for m in messages] + account.bulk_move(ids=ids, to_folder=analyzed_folder) + + logger.info( + f"Mails were successfully moved from '{LOG_DIR}' to '{ANALYZED_LOG_DIR}'." + ) diff --git a/src/logwatcher/reporter.py b/src/logwatcher/reporter.py index e998455..3763be2 100644 --- a/src/logwatcher/reporter.py +++ b/src/logwatcher/reporter.py @@ -3,7 +3,7 @@ from datetime import datetime from pathlib import Path from string import Template -from logwatcher.config import DATETIME_FORMAT, RESULT_PATH, SourceType +from logwatcher.config import DATETIME_FORMAT, FRENCH_TIMEZONE, RESULT_PATH, SourceType from logwatcher.models import LogEntry logger = logging.getLogger(__name__) @@ -12,6 +12,7 @@ logger = logging.getLogger(__name__) BASE_TEMPLATE = Template(""" RAPPORT D'ANALYSE DE LOGS ========================= +Généré le\t : $today Période\t : $start_date -> $end_date $source_type(s) lu(s)\t : $nb_files Nombre total d'erreur(s)\t: $nb_errors @@ -126,10 +127,12 @@ def build_reports( logger.info("\t\t\tn2 and other reports were generated.") logger.info("\t\tbuilding reports job completed.") + today = datetime.now(FRENCH_TIMEZONE).strftime(DATETIME_FORMAT) source_value = source_type.value.capitalize() return { "n2": BASE_TEMPLATE.substitute( + today=today, source_type=source_value, start_date=start_date, end_date=end_date, @@ -138,6 +141,7 @@ def build_reports( sub_content=relevant_report, ), "other": BASE_TEMPLATE.substitute( + today=today, source_type=source_value, start_date=start_date, end_date=end_date, @@ -146,6 +150,7 @@ def build_reports( sub_content=irrelevant_report, ), "all": BASE_TEMPLATE.substitute( + today=today, source_type=source_value, start_date=start_date, end_date=end_date,