Skip to content

Commit 8495ad5

Browse files
Lukas Geigerclaude
andcommitted
fix: extract_msg.Message resource leak in _convert_msg_to_pdf bei pisa-Exception
Ressource wird jetzt per try/finally garantiert geschlossen, auch wenn pisa.CreatePDF eine Exception wirft. Explizite msg.close()-Aufrufe entfernt. Zwei neue Tests: close() bei Fehler und close() bei Erfolg. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b44da71 commit 8495ad5

2 files changed

Lines changed: 66 additions & 5 deletions

File tree

UniversalInvoiceMail.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3500,6 +3500,7 @@ def _convert_msg_to_pdf(self, msg_path: Path) -> Optional[Path]:
35003500
)
35013501
return None
35023502

3503+
msg = None
35033504
try:
35043505
msg = extract_msg.Message(str(msg_path))
35053506
html_body = msg.htmlBody
@@ -3509,7 +3510,6 @@ def _convert_msg_to_pdf(self, msg_path: Path) -> Optional[Path]:
35093510
html_body = html_body.decode("utf-8", errors="replace")
35103511

35113512
if not html_body:
3512-
msg.close()
35133513
return None
35143514

35153515
pdf_path = msg_path.with_suffix(".pdf")
@@ -3518,16 +3518,19 @@ def _convert_msg_to_pdf(self, msg_path: Path) -> Optional[Path]:
35183518
pisa.CreatePDF(html_body, dest=f)
35193519
if hasattr(self, 'log_output'):
35203520
self.log_output.appendPlainText(f"[MSG] Konvertiert: {msg_path.name} -> {pdf_path.name}")
3521-
msg.close()
35223521
return pdf_path
3523-
else:
3524-
msg.close()
3525-
return None
3522+
return None
35263523

35273524
except Exception as e:
35283525
if hasattr(self, 'log_output'):
35293526
self.log_output.appendPlainText(f"[MSG] Fehler bei {msg_path.name}: {e}")
35303527
return None
3528+
finally:
3529+
if msg is not None:
3530+
try:
3531+
msg.close()
3532+
except Exception:
3533+
pass
35313534

35323535
def scan_folders_for_new_files(self) -> int:
35333536
"""

test_helpers.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,5 +685,63 @@ def _save_attachment_invoice(self, **kwargs):
685685
self.assertNotIn("< 50", body, "Raw < must not appear in IMAP merge body_html")
686686

687687

688+
class TestMsgConvertClosesMessage(unittest.TestCase):
689+
"""_convert_msg_to_pdf() muss extract_msg.Message auch im Fehlerfall schliessen."""
690+
691+
def _make_mock_msg(self, plain_body: str = "Test"):
692+
mock_msg = MagicMock()
693+
mock_msg.htmlBody = None
694+
mock_msg.body = plain_body
695+
return mock_msg
696+
697+
def test_msg_closed_on_pisa_exception(self):
698+
from UniversalInvoiceMail import MainWindow
699+
700+
mock_msg = self._make_mock_msg()
701+
mock_extract_msg = MagicMock()
702+
mock_extract_msg.Message.return_value = mock_msg
703+
704+
with tempfile.TemporaryDirectory() as tmpdir:
705+
msg_path = Path(tmpdir) / "test.msg"
706+
msg_path.write_bytes(b"dummy")
707+
708+
win = MainWindow.__new__(MainWindow)
709+
710+
with patch.dict("sys.modules", {"extract_msg": mock_extract_msg}), \
711+
patch("UniversalInvoiceMail.XHTML2PDF_AVAILABLE", True), \
712+
patch("UniversalInvoiceMail.pisa") as mock_pisa:
713+
mock_pisa.CreatePDF.side_effect = RuntimeError("pisa crash")
714+
result = win._convert_msg_to_pdf(msg_path)
715+
716+
self.assertIsNone(result)
717+
mock_msg.close.assert_called_once()
718+
719+
def test_msg_closed_on_success(self):
720+
from UniversalInvoiceMail import MainWindow
721+
722+
mock_msg = self._make_mock_msg()
723+
mock_extract_msg = MagicMock()
724+
mock_extract_msg.Message.return_value = mock_msg
725+
726+
with tempfile.TemporaryDirectory() as tmpdir:
727+
msg_path = Path(tmpdir) / "test.msg"
728+
msg_path.write_bytes(b"dummy")
729+
730+
win = MainWindow.__new__(MainWindow)
731+
732+
def fake_create_pdf(html, dest, **kwargs):
733+
dest.write(b"%PDF-1.4 fake")
734+
return MagicMock(err=0)
735+
736+
with patch.dict("sys.modules", {"extract_msg": mock_extract_msg}), \
737+
patch("UniversalInvoiceMail.XHTML2PDF_AVAILABLE", True), \
738+
patch("UniversalInvoiceMail.pisa") as mock_pisa:
739+
mock_pisa.CreatePDF.side_effect = fake_create_pdf
740+
result = win._convert_msg_to_pdf(msg_path)
741+
742+
mock_msg.close.assert_called_once()
743+
self.assertIsNotNone(result)
744+
745+
688746
if __name__ == "__main__":
689747
unittest.main()

0 commit comments

Comments
 (0)