feat: auto-email sending via SMTP (--send flag)

Closes #45

- New redmine_reporter/mailer.py: SMTP email sending with
  {author}/{period} template substitution, MIME attachment
  with correct content-type per file extension
- Config.get_email_config(): returns EmailConfig from YAML
  or None when not configured
- CLI --send flag: sends report after generation, works with
  --output, --commit, or standalone (saves to template path)
- 31 new tests (22 mailer + 6 CLI + 3 config)
- 249/249 tests passing, ruff clean, mypy clean
This commit is contained in:
Кокос Артем Николаевич
2026-07-10 12:37:02 +07:00
parent b926dd0d49
commit b0e353c565
7 changed files with 693 additions and 63 deletions

View File

@@ -557,3 +557,53 @@ class TestDefaultDateRangeWithLastUsed:
Config._app = AppConfig.from_yaml(yaml_path)
result = Config.get_default_date_range()
assert result == "2026-04-01--2026-04-15"
class TestGetEmailConfig:
"""Tests for Config.get_email_config()."""
@mock.patch.dict(os.environ, {}, clear=True)
def test_get_email_config_from_yaml(self):
"""get_email_config() возвращает EmailConfig из YAML."""
with tempfile.TemporaryDirectory() as tmp:
yaml_path = Path(tmp) / "config.yml"
yaml_path.write_text(
"email:\n"
" smtp:\n"
" host: smtp.example.com\n"
" port: 587\n"
" user: bot@example.com\n"
" password: secret\n"
" tls: true\n"
" from: bot@example.com\n"
" to:\n"
" - boss@example.com\n"
)
Config._app = AppConfig.from_yaml(yaml_path)
cfg = Config.get_email_config()
assert cfg is not None
assert cfg.smtp.host == "smtp.example.com"
assert cfg.smtp.port == 587
assert cfg.smtp.user == "bot@example.com"
assert cfg.smtp.password == "secret"
assert cfg.smtp.tls is True
assert cfg.from_ == "bot@example.com"
assert cfg.to == ["boss@example.com"]
@mock.patch.dict(os.environ, {}, clear=True)
def test_get_email_config_returns_none_when_no_yaml(self):
"""Без YAML-конфига get_email_config() возвращает None."""
Config._app = None
assert Config.get_email_config() is None
@mock.patch.dict(os.environ, {}, clear=True)
def test_get_email_config_returns_none_when_no_host(self):
"""С YAML но без smtp.host — возвращает None."""
with tempfile.TemporaryDirectory() as tmp:
yaml_path = Path(tmp) / "config.yml"
yaml_path.write_text("email:\n smtp:\n host: ''\n")
Config._app = AppConfig.from_yaml(yaml_path)
assert Config.get_email_config() is None