diff --git a/README.md b/README.md index bef77d6..72f7345 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ subject=PrintAudit Report from=printaudit@example.com attach_csv=true attach_html=true +report_in_body=true # Include full report text in email body ``` #### `[costs]` - Cost Calculation diff --git a/printaudit.conf.sample b/printaudit.conf.sample index ab22860..bf3d873 100644 --- a/printaudit.conf.sample +++ b/printaudit.conf.sample @@ -24,6 +24,7 @@ subject=PrintAudit Report from= attach_csv=true attach_html=true +report_in_body=true [costs] # Basic cost settings (unitless numbers, interpreted as your local currency) diff --git a/src/printaudit/config.py b/src/printaudit/config.py index 1c6a7e9..2f20e43 100644 --- a/src/printaudit/config.py +++ b/src/printaudit/config.py @@ -38,6 +38,7 @@ class EmailSettings: recipients: list[str] = field(default_factory=list) attach_csv: bool = False attach_html: bool = False + report_in_body: bool = True subject: str = "PrintAudit Report" from_address: str = "" @@ -142,6 +143,7 @@ def _load_email_settings(email: EmailSettings, raw: dict[str, str]) -> None: "email.recipients": ("recipients", _split_csv), "email.attach_csv": ("attach_csv", _bool), "email.attach_html": ("attach_html", _bool), + "email.report_in_body": ("report_in_body", _bool), "email.subject": ("subject", str), "email.from": ("from_address", str), } diff --git a/src/printaudit/outputs/email_sender.py b/src/printaudit/outputs/email_sender.py index cfa3a07..fbfbc71 100644 --- a/src/printaudit/outputs/email_sender.py +++ b/src/printaudit/outputs/email_sender.py @@ -2,11 +2,13 @@ from __future__ import annotations +from io import StringIO from pathlib import Path from ..analysis import AnalysisReport from ..emailer import EmailClient, EmailDeliveryError -from .base import OutputModule, register_output +from .base import OutputContext, OutputModule, register_output +from .cli import CliOutput @register_output("email") @@ -28,6 +30,10 @@ def render(self, report: AnalysisReport) -> None: f"Pages: {totals.pages}\n" f"Window: {start_date} -> {end_date}\n" ) + if settings.report_in_body: + cli_output = self._render_cli_output(report).strip() + if cli_output: + body = f"{body}\n{cli_output}\n" try: client.send_report(subject, body, attachments) except EmailDeliveryError as exc: # pragma: no cover - network heavy @@ -43,3 +49,13 @@ def _select_attachments(self) -> list[Path]: if path.suffix in {".htm", ".html"} and settings.attach_html: selected.append(path) return selected + + def _render_cli_output(self, report: AnalysisReport) -> str: + buffer = StringIO() + cli_context = OutputContext( + config=self.context.config, + attachments=list(self.context.attachments), + stdout=buffer, + ) + CliOutput(cli_context).render(report) + return buffer.getvalue() diff --git a/tests/test_config.py b/tests/test_config.py index 0bb12a9..549c30a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -13,6 +13,7 @@ def test_config_defaults(): assert config.cost_default == 0.0 assert config.currency_symbol == "" assert config.currency_code == "" + assert config.email.report_in_body is True def test_parse_config_missing_file(tmp_path): @@ -93,3 +94,20 @@ def test_parse_config_printer_rates(tmp_path): assert "printer01" in config.cost_printer_rates assert config.cost_printer_rates["printer01"] == 0.01 assert config.cost_printer_rates["printer02"] == 0.05 + + +def test_parse_config_email_report_in_body(tmp_path): + """Test parsing email report_in_body setting.""" + config_file = tmp_path / "test.conf" + config_file.write_text( + """[core] +page_log_path=/var/log/cups/page_log + +[email] +enabled=true +report_in_body=true +""" + ) + config = parse_config(config_file) + assert config.email.enabled is True + assert config.email.report_in_body is True diff --git a/tests/test_email_output.py b/tests/test_email_output.py new file mode 100644 index 0000000..70f5e3d --- /dev/null +++ b/tests/test_email_output.py @@ -0,0 +1,67 @@ +"""Tests for email output behavior.""" + +from printaudit.analysis.aggregator import UsageAggregator +from printaudit.config import Config +from printaudit.outputs.base import OutputContext +from printaudit.outputs.email_sender import EmailOutput +from printaudit.parser import parse_line + + +def _build_report(): + aggregator = UsageAggregator() + entry = parse_line( + "Printer01 alice 12345 [01/Apr/2025:09:03:11 -0300] " + "total 5 - 192.168.1.1 doc.pdf - -" + ) + aggregator.ingest(entry) + return aggregator.build_report() + + +def test_email_body_includes_report_in_body_when_enabled(monkeypatch): + config = Config() + config.email.enabled = True + config.email.recipients = ["ops@example.com"] + config.email.report_in_body = True + + captured = {} + + def fake_send_report(self, subject, body, attachments): + captured["subject"] = subject + captured["body"] = body + captured["attachments"] = attachments + + monkeypatch.setattr( + "printaudit.emailer.EmailClient.send_report", fake_send_report + ) + + output = EmailOutput(OutputContext(config=config)) + output.render(_build_report()) + separator = "\n" + ("=" * 72) + "\n" + + assert "PrintAudit summary" in captured["body"] + assert "Console output" not in captured["body"] + assert "PrintAudit Summary" in captured["body"] + assert separator in captured["body"] + + +def test_email_body_omits_report_in_body_when_disabled(monkeypatch): + config = Config() + config.email.enabled = True + config.email.recipients = ["ops@example.com"] + config.email.report_in_body = False + + captured = {} + + def fake_send_report(self, subject, body, attachments): + captured["body"] = body + + monkeypatch.setattr( + "printaudit.emailer.EmailClient.send_report", fake_send_report + ) + + output = EmailOutput(OutputContext(config=config)) + output.render(_build_report()) + + assert "PrintAudit summary" in captured["body"] + assert "Console output" not in captured["body"] + assert "PrintAudit Summary" not in captured["body"]