From 78ab945a4dc852dd515239121659390bd935b85a Mon Sep 17 00:00:00 2001 From: Bendt Date: Sun, 28 Dec 2025 10:51:45 -0500 Subject: [PATCH] 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 --- .coverage | Bin 69632 -> 69632 bytes mail.toml.example | 82 +++++++++++++ src/mail/notification_detector.py | 13 ++- tests/test_notification_detector.py | 172 ++++++++++++++++++++++++++++ 4 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 mail.toml.example create mode 100644 tests/test_notification_detector.py diff --git a/.coverage b/.coverage index 467dd7dded96b2fe7d285e13fd2ba70b0420549f..737fdced222d2d152223a5af946202799f5f9e51 100644 GIT binary patch delta 4396 zcmeHKeQ;FO6@T}=`@Y}4_ckBUB#;jfWJ(ALlLV3g*^td~8NYr!*OW6`OkSw+o zW3pH*cS{7X5iY*4c0)AA#-M;4`brH3V-#EB=w?}%l>72%N3D%knc{BFJ${}Ug>+i)>= zmHQbN;&RzPus>jHSd31i$B`e|nfI83!@~u~pK~0+f_DLCVmYMFS)SD$>5kE zV!A5?@2n));(|zTG`uO?9gK#1BVFCSTetOv`ultP0Ory>j@beDij^JFa8EyY9Jyr3 z?O>PXkWbuIWXQ9^))5MAY3vJcgvG|0B<69W8S%3+pxx)S6Txeu?D(*nG+VrOl$Ba7 zLYeVcVxvqhdvj3+K|VLK$M+hrz0@y}0iTFWxHh;GUp6*4W{}kVegR$5yL%&>!rz3P zD|PrDr0Bi{kWbb&1iQDu#ZxRcz_O6^`e!4J?D01s6>f4N85zrGJxbzZKquz7$$I2T zvlC3G1l!9(m=W0f)>WOa6gZ}z4Eq)NXkJqOo5*aZX%M;V4Ot66v=<3)Hf zhVi17p?snSlo`q~rCWYfK7%rtLufb4qX0U?{Ly$5-@=P{hmgUIbDOxI z!^4HgpKm^bu@3{ohbI=1p#~}Qc9t(7L#L9;hGdXg~$&G55Q5luwLx zmHg}!-u!8IJ&okEmT@~MJ~_qlJaY1PGV6IL0{yNQ4TrPT1;83rBVKCDJMDM zQPE6t^pdp&h;Q&S0ys81Nlm*w&>M+vu20qlpiXvjZz0Me$JVb#naM~psqkfj7K+vs zQ>KdvqS@hM0h2Ean=80ME9l>R8N5-8V<^Rbb&CDE6|C1k3xNzeih+^3mf%uX7v$F9 zzL${Ft3dHFlN65EP$3`%yj~<*2Sx%pNV0+?sgK2_Tt8$gQ4)ZJ<=g-@De(~xXsOmL z+9OO(t+OmK35XoJR!V!oktLWworVT^wm?;N+ACye?M zsIl_>Z1u+*YUV#Q^6|jP_qp@*=*bK4WK8&TfE*YG)Bl)}c!XtHM+z*k#!be!W!!{7 zW5)5D&*-ItZA>Y1h*i(2Pif7!D~DYUi#xdz(9hqgg4kns(vTdGGo^FV_oN!}L-9GW zOUx0@36B{=Mq|2iQo;c|3^V*x-tp(p{&&>F&(roX7W)?D!GU^TWC+dCTgitT+=9~GCa65k(dczS|dKb+=_luq4Pla>hO!N_Z03Bgn z>_w@>*lfJaJ%ml}ZEmqzsEjEuDBG3!@}K4XvR~$;UrBuytF4Cz$FePWef^8gXn1OY zegZ;O#8*gAQubX?(R!e#WoDpzQXiH0UI|HPomF$piy2z~&PX>DsE%eFn@iAW8Sz}Q z8&gHIJ$p~b{;X%1?I>e3(ucSJWsoE9+rB=b^bd{6}#HKQh4W1+Il$46Dv zY(b%B+yM{Nl!PMF6DGgfs^I`6{AU}y*b`slV z6ZuH~yOsA$3!qHCx40p1BWg@#BTrm0kt=1z9XjN0sK+tc$^j-%3dUsQ1n?eI zPcf4{*D}y5t1XxpWm0fm5pFB+@TOBVUOlZ5{KulM zPf3KkV@sg)qy;{|MI*h}e}ox^E3uksqU!rmURn=Nt6X2MT9yY4svtMc&qVja{S*^= z*Eh~@j>srvASEq~4;dwo|qN+fwaA?H5|7CaABdK~+(HtvsNV$fNQB*)83YUXVH^MSMei zP%Ib5g=d7d@LnbetnXer2bNQ1v7SzA0!vCuos^b)J)M<2=pH)5u2fQrbFHIxBi~_7 zx@8v~Y*Tx@6pEuQHIRTTo7$+W8MV?Gb2VzA+5BiN{pZA6gW;a|gEFd`YzwTrndT}_ zIG{1Gt|po#;h?QS;a6Mv){Iuk$?oD0(9t%4{P7VLwevn$Ur3(4P!>O_A)R&tVYL^0 zMJZn{@0r|U+E4*<)ARM}WK(c8S~;N^h$L`D<6Tz9XD^h}_8>SLQYP&WcHRl546#e8 mY4r!zU2lC0=nXRY3rwW0Rf&MmE7;mPa_ZCCSD(~T5BfJ}ucVFu delta 3824 zcmeH~Yj9Q7701sy@AtWvB)pQ41PF!zc_;)Dh(lu5<3aNuae&zjXS+ zFX!I9_gZT|&Ts$MI$eEyS0CSRmbaT%&DYF5W`%Lxc-45&NYjV(AL^AltG%l|p#{{N zYQNg5rYP?#ol24XcX?2LO!mp9^q$lutro9|hs9=bmhf9)zYrA^evt3vS9907UvRs) z#q2rOW^UW4?rD$cn@?TVL|5|_gRRPka zuxc*HJmL;K8dPBp_C}Uqe=r^Q21`*5>B1~>OvU<&9P9~sycL@_`yPIveV2dx&bGWA zdm=OKx13G&2ff%A@(L+ajqysz1Ic(Z6osUCSOp&sC&LVTzeP?xAz5$Pv4g^tdC8B9 zPa+%5&0E{I?rLtPku@HSR=B2G>-P35PhGU%}b%^x$d zsm2Rhyjd&aV`DO=T=cL(_13;k)K{%g03XN{tk5;=idrCHPjn?Oa?JOzery?DkLn;e zYtF^|S`YAab|tKza}Iks_MBI&)@@tqOlWBVmNt@Ndrc@lV1g!=fwM8YMTUaT*sd6c z@szOi1nU?*8Ou7R9aCS?807vv@rd|Wag)?7{aTzYt&#ogK`xgKvm@4^^{|y^K5AB)e>RUATa0w$ zhHMPzRr-he)4jd<1BdEf=CPxpSl-q0(06FvIoNfk5Wn;#33pWT#B5>13Sw4)ML2Y_ zm~b^A8=FssU?E;T6%-cudE&C;N)c|lqQiXht%fWd8eQtbO(Y_JxdclCCZAEnbx^Q8 z(S6PH@HgvMK{^hdu7ETwYhL02$fp)_kC4qWb>aoj#hwjIiK#SvqUmmeP=nd{$~Q`r zZ-Z}E4c|?{nu-ZM56mQ!2&7>Cu;Z%WUEw(dUo4P=T@pdqanV6lkz&-%z@hugd2cD# zNkIxHr}tpphBC0oH$ad%+j5xt`2~$&%h(vgc^mIaAO#xr-08LmCQxJ6E^ zbrYYda@H7Q-LS4&V*_5#$5yt;@t~O8TO#ZHvhxMt75Y`lowF`8))&?_>oS?1u_Tc! zfa3lnmlE>Mf;Y*Dx{(ClsOnIF78`fX0nhu>ps$RvEIPy5j$NvMKf<-U$O}aKY1ZUd6 z!c@>1h8>PsOlsh*zzW0!TQkMx-#{yfq**~n4<}Dz`UDFu!B|+GV|SU5-08qr#JtW} z?^%1T67w(SPt9+cbBzy;eMW;J>2K)U^<3?uTA#%I@Tnxqf*u^Eya;g#QD7fE(9R)nWA~>NYh?IjQVZ*2-VX19F>7S?tk! zXf?@2?0&uQ$63%s*ErGACokdp_$5q-`{K8B2I^-OXiT*1UrKa=?xkzL!p@|>`a^r# zTS-qF>iF&jc=ddtT`ohaux@J8gC(D7m^WPHjCt|t&t#}~7jxq59Jyupt}WzWm*}vc zQ9y!PQne^VCnRBfSiv`jHK>`;j`86c5OJ1)&RF!EB!pdcq$Oa%h-GeWd7y23J4FsP z^4TT3Ac%2Ejko*Y4A4kG_ei21h0A9Mz!Ooo;Z{16B0T z2bGw2VFgCco4D-4Q!%LoeX59lg)Alnlrr?KbdsD-mC32{`JW~8HsIX+mU&4=Yn_fi z{`{bOz6AjRoxV%W$@u+CPG*W{vRKZ;hDJAwK?dz_BDX=NlL>`|x8*>}i?G1SYeqEbRuQs)xq+-?YP?{lSGuHdoJ}Yb`nWxQX z%{o&x-Zj2!lr^K5o&W5F=-0d$9yMG`fYftDla#vi^G~I~;*kp~3yk>Hl 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"])