diff --git a/.coverage b/.coverage index 467dd7d..737fdce 100644 Binary files a/.coverage and b/.coverage differ diff --git a/mail.toml.example b/mail.toml.example new file mode 100644 index 0000000..64aa72c --- /dev/null +++ b/mail.toml.example @@ -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" diff --git a/src/mail/notification_detector.py b/src/mail/notification_detector.py index 804e78d..baa9ab4 100644 --- a/src/mail/notification_detector.py +++ b/src/mail/notification_detector.py @@ -17,10 +17,17 @@ class NotificationType: def matches(self, envelope: Dict[str, Any], content: Optional[str] = None) -> bool: """Check if envelope matches this notification type.""" - # Check sender domain + # Check sender domain (more specific check) from_addr = envelope.get("from", {}).get("addr", "").lower() - if any(domain in from_addr for domain in self.domains): - return True + for domain in self.domains: + # 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 subject = envelope.get("subject", "").lower() diff --git a/tests/test_notification_detector.py b/tests/test_notification_detector.py new file mode 100644 index 0000000..411371f --- /dev/null +++ b/tests/test_notification_detector.py @@ -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"])