fix: Improve Confluence/Jira detection precision
- Add domain-specific matching for Atlassian services - Fix Confluence being misclassified as Jira - Add comprehensive test coverage for notification detection - Add example configuration file with new options - All 13 tests now passing Files modified: - src/mail/notification_detector.py: Better atlassian.net handling - tests/test_notification_detector.py: Full test suite - mail.toml.example: Config documentation with examples
This commit is contained in:
82
mail.toml.example
Normal file
82
mail.toml.example
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# LUK Mail Configuration Example
|
||||||
|
# Copy this file to ~/.config/luk/mail.toml and customize
|
||||||
|
|
||||||
|
# [task]
|
||||||
|
# # Task management backend (taskwarrior or dstask)
|
||||||
|
# backend = "taskwarrior"
|
||||||
|
# taskwarrior_path = "task"
|
||||||
|
# dstask_path = "~/.local/bin/dstask"
|
||||||
|
|
||||||
|
[envelope_display]
|
||||||
|
# Sender name maximum length before truncation
|
||||||
|
max_sender_length = 25
|
||||||
|
|
||||||
|
# Date/time formatting
|
||||||
|
date_format = "%m/%d"
|
||||||
|
time_format = "%H:%M"
|
||||||
|
show_date = true
|
||||||
|
show_time = true
|
||||||
|
|
||||||
|
# Group envelopes by date
|
||||||
|
# "relative" = Today, Yesterday, This Week, etc.
|
||||||
|
# "absolute" = December 2025, November 2025, etc.
|
||||||
|
group_by = "relative"
|
||||||
|
|
||||||
|
# Layout: 2-line or 3-line (3-line shows preview)
|
||||||
|
lines = 2
|
||||||
|
show_checkbox = true
|
||||||
|
show_preview = false
|
||||||
|
|
||||||
|
# NerdFont icons
|
||||||
|
icon_unread = "\uf0e0" # nf-fa-envelope (filled)
|
||||||
|
icon_read = "\uf2b6" # nf-fa-envelope_open (open)
|
||||||
|
icon_flagged = "\uf024" # nf-fa-flag
|
||||||
|
icon_attachment = "\uf0c6" # nf-fa-paperclip
|
||||||
|
|
||||||
|
[content_display]
|
||||||
|
# Default view mode: "markdown" or "html"
|
||||||
|
default_view_mode = "markdown"
|
||||||
|
|
||||||
|
# URL compression settings
|
||||||
|
compress_urls = true
|
||||||
|
max_url_length = 50
|
||||||
|
|
||||||
|
# Notification email compression
|
||||||
|
# "summary" - Brief one-page summary
|
||||||
|
# "detailed" - More details in structured format
|
||||||
|
# "off" - Disable notification compression
|
||||||
|
compress_notifications = true
|
||||||
|
notification_compression_mode = "summary"
|
||||||
|
|
||||||
|
[link_panel]
|
||||||
|
# Close link panel after opening a link
|
||||||
|
close_on_open = false
|
||||||
|
|
||||||
|
[mail]
|
||||||
|
# Default folder to archive messages to
|
||||||
|
archive_folder = "Archive"
|
||||||
|
|
||||||
|
[keybindings]
|
||||||
|
# Custom keybindings (leave blank to use defaults)
|
||||||
|
# next_message = "j"
|
||||||
|
# prev_message = "k"
|
||||||
|
# delete = "#"
|
||||||
|
# archive = "e"
|
||||||
|
# open_by_id = "o"
|
||||||
|
# quit = "q"
|
||||||
|
# toggle_header = "h"
|
||||||
|
# create_task = "t"
|
||||||
|
# reload = "%"
|
||||||
|
# toggle_sort = "s"
|
||||||
|
# toggle_selection = "space"
|
||||||
|
# clear_selection = "escape"
|
||||||
|
# scroll_page_down = "pagedown"
|
||||||
|
# scroll_page_up = "b"
|
||||||
|
# toggle_main_content = "w"
|
||||||
|
# open_links = "l"
|
||||||
|
# toggle_view_mode = "m"
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
# Textual theme name
|
||||||
|
# Available themes: monokai, dracula, gruvbox, nord, etc.
|
||||||
|
theme_name = "monokai"
|
||||||
@@ -17,10 +17,17 @@ class NotificationType:
|
|||||||
def matches(self, envelope: Dict[str, Any], content: Optional[str] = None) -> bool:
|
def matches(self, envelope: Dict[str, Any], content: Optional[str] = None) -> bool:
|
||||||
"""Check if envelope matches this notification type."""
|
"""Check if envelope matches this notification type."""
|
||||||
|
|
||||||
# Check sender domain
|
# Check sender domain (more specific check)
|
||||||
from_addr = envelope.get("from", {}).get("addr", "").lower()
|
from_addr = envelope.get("from", {}).get("addr", "").lower()
|
||||||
if any(domain in from_addr for domain in self.domains):
|
for domain in self.domains:
|
||||||
return True
|
# For atlassian.net, check if it's specifically jira or confluence in the address
|
||||||
|
if domain == "atlassian.net":
|
||||||
|
if "jira@" in from_addr:
|
||||||
|
return self.name == "jira"
|
||||||
|
elif "confluence@" in from_addr:
|
||||||
|
return self.name == "confluence"
|
||||||
|
elif domain in from_addr:
|
||||||
|
return True
|
||||||
|
|
||||||
# Check subject patterns
|
# Check subject patterns
|
||||||
subject = envelope.get("subject", "").lower()
|
subject = envelope.get("subject", "").lower()
|
||||||
|
|||||||
172
tests/test_notification_detector.py
Normal file
172
tests/test_notification_detector.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""Tests for notification email detection and classification."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from src.mail.notification_detector import (
|
||||||
|
is_notification_email,
|
||||||
|
classify_notification,
|
||||||
|
extract_notification_summary,
|
||||||
|
NOTIFICATION_TYPES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNotificationDetection:
|
||||||
|
"""Test notification email detection."""
|
||||||
|
|
||||||
|
def test_gitlab_pipeline_notification(self):
|
||||||
|
"""Test GitLab pipeline notification detection."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "notifications@gitlab.com"},
|
||||||
|
"subject": "Pipeline #12345 failed",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is True
|
||||||
|
|
||||||
|
notif_type = classify_notification(envelope)
|
||||||
|
assert notif_type is not None
|
||||||
|
assert notif_type.name == "gitlab"
|
||||||
|
|
||||||
|
def test_gitlab_mr_notification(self):
|
||||||
|
"""Test GitLab merge request notification detection."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "noreply@gitlab.com"},
|
||||||
|
"subject": "[GitLab] Merge request: Update dependencies",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is True
|
||||||
|
|
||||||
|
def test_github_pr_notification(self):
|
||||||
|
"""Test GitHub PR notification detection."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "noreply@github.com"},
|
||||||
|
"subject": "[GitHub] PR #42: Add new feature",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is True
|
||||||
|
|
||||||
|
notif_type = classify_notification(envelope)
|
||||||
|
assert notif_type is not None
|
||||||
|
assert notif_type.name == "github"
|
||||||
|
|
||||||
|
def test_jira_notification(self):
|
||||||
|
"""Test Jira notification detection."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "jira@company.com"},
|
||||||
|
"subject": "[Jira] ABC-123: Fix login bug",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is True
|
||||||
|
|
||||||
|
notif_type = classify_notification(envelope)
|
||||||
|
assert notif_type is not None
|
||||||
|
assert notif_type.name == "jira"
|
||||||
|
|
||||||
|
def test_confluence_notification(self):
|
||||||
|
"""Test Confluence notification detection."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "confluence@atlassian.net"},
|
||||||
|
"subject": "[Confluence] New comment on page",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is True
|
||||||
|
|
||||||
|
notif_type = classify_notification(envelope)
|
||||||
|
assert notif_type is not None
|
||||||
|
assert notif_type.name == "confluence"
|
||||||
|
|
||||||
|
def test_datadog_alert_notification(self):
|
||||||
|
"""Test Datadog alert notification detection."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "alerts@datadoghq.com"},
|
||||||
|
"subject": "[Datadog] Alert: High CPU usage",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is True
|
||||||
|
|
||||||
|
notif_type = classify_notification(envelope)
|
||||||
|
assert notif_type is not None
|
||||||
|
assert notif_type.name == "datadog"
|
||||||
|
|
||||||
|
def test_renovate_notification(self):
|
||||||
|
"""Test Renovate notification detection."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "renovate@renovatebot.com"},
|
||||||
|
"subject": "[Renovate] Update dependency to v2.0.0",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is True
|
||||||
|
|
||||||
|
notif_type = classify_notification(envelope)
|
||||||
|
assert notif_type is not None
|
||||||
|
assert notif_type.name == "renovate"
|
||||||
|
|
||||||
|
def test_general_notification(self):
|
||||||
|
"""Test general notification email detection."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "noreply@example.com"},
|
||||||
|
"subject": "[Notification] Daily digest",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is True
|
||||||
|
|
||||||
|
def test_non_notification_email(self):
|
||||||
|
"""Test that personal emails are not detected as notifications."""
|
||||||
|
envelope = {
|
||||||
|
"from": {"addr": "john.doe@example.com"},
|
||||||
|
"subject": "Let's meet for lunch",
|
||||||
|
}
|
||||||
|
assert is_notification_email(envelope) is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestSummaryExtraction:
|
||||||
|
"""Test notification summary extraction."""
|
||||||
|
|
||||||
|
def test_gitlab_pipeline_summary(self):
|
||||||
|
"""Test GitLab pipeline summary extraction."""
|
||||||
|
content = """
|
||||||
|
Pipeline #12345 failed by john.doe
|
||||||
|
|
||||||
|
The pipeline failed on stage: build
|
||||||
|
View pipeline: https://gitlab.com/project/pipelines/12345
|
||||||
|
"""
|
||||||
|
summary = extract_notification_summary(content, NOTIFICATION_TYPES[0]) # gitlab
|
||||||
|
assert summary["metadata"]["pipeline_id"] == "12345"
|
||||||
|
assert summary["metadata"]["triggered_by"] == "john.doe"
|
||||||
|
assert summary["title"] == "Pipeline #12345"
|
||||||
|
|
||||||
|
def test_github_pr_summary(self):
|
||||||
|
"""Test GitHub PR summary extraction."""
|
||||||
|
content = """
|
||||||
|
PR #42: Add new feature
|
||||||
|
|
||||||
|
@john.doe requested your review
|
||||||
|
View PR: https://github.com/repo/pull/42
|
||||||
|
"""
|
||||||
|
summary = extract_notification_summary(content, NOTIFICATION_TYPES[1]) # github
|
||||||
|
assert summary["metadata"]["number"] == "42"
|
||||||
|
assert summary["metadata"]["title"] == "Add new feature"
|
||||||
|
assert summary["title"] == "#42: Add new feature"
|
||||||
|
|
||||||
|
def test_jira_issue_summary(self):
|
||||||
|
"""Test Jira issue summary extraction."""
|
||||||
|
content = """
|
||||||
|
ABC-123: Fix login bug
|
||||||
|
|
||||||
|
Status changed from In Progress to Done
|
||||||
|
View issue: https://jira.atlassian.net/browse/ABC-123
|
||||||
|
"""
|
||||||
|
summary = extract_notification_summary(content, NOTIFICATION_TYPES[2]) # jira
|
||||||
|
assert summary["metadata"]["issue_key"] == "ABC-123"
|
||||||
|
assert summary["metadata"]["issue_title"] == "Fix login bug"
|
||||||
|
assert summary["metadata"]["status_from"] == "In Progress"
|
||||||
|
assert summary["metadata"]["status_to"] == "Done"
|
||||||
|
|
||||||
|
def test_datadog_alert_summary(self):
|
||||||
|
"""Test Datadog alert summary extraction."""
|
||||||
|
content = """
|
||||||
|
Alert triggered
|
||||||
|
|
||||||
|
Monitor: Production CPU usage
|
||||||
|
Status: Critical
|
||||||
|
View alert: https://app.datadoghq.com/monitors/123
|
||||||
|
"""
|
||||||
|
summary = extract_notification_summary(
|
||||||
|
content, NOTIFICATION_TYPES[4]
|
||||||
|
) # datadog
|
||||||
|
assert summary["metadata"]["monitor"] == "Production CPU usage"
|
||||||
|
assert "investigate" in summary["action_items"][0].lower()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
Reference in New Issue
Block a user