Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions printaudit.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/printaudit/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""

Expand Down Expand Up @@ -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),
}
Expand Down
18 changes: 17 additions & 1 deletion src/printaudit/outputs/email_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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()
18 changes: 18 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
67 changes: 67 additions & 0 deletions tests/test_email_output.py
Original file line number Diff line number Diff line change
@@ -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"]
Loading