PyEmailerAJM 1.8.4__tar.gz → 1.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PKG-INFO +2 -2
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/__init__.py +2 -2
- pyemailerajm-1.9/PyEmailerAJM/_version.py +1 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/__init__.py +2 -2
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/enums.py +10 -1
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/continuous_monitor_base.py +31 -14
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/continuous_monitor.py +14 -4
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/continuous_monitor_alert_send.py +24 -4
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/msg/alert_messages.py +71 -8
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/msg/msg.py +22 -1
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/py_emailer_ajm.py +75 -38
- pyemailerajm-1.9/PyEmailerAJM/searchers/__init__.py +29 -0
- pyemailerajm-1.9/PyEmailerAJM/searchers/factory.py +68 -0
- pyemailerajm-1.9/PyEmailerAJM/searchers/searchers.py +395 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/PKG-INFO +2 -2
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/SOURCES.txt +5 -0
- pyemailerajm-1.9/README.md +143 -0
- pyemailerajm-1.9/tests/test_continuous_monitor_base.py +104 -0
- pyemailerajm-1.9/tests/test_email_signature.py +43 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/tests/test_logger.py +9 -0
- pyemailerajm-1.9/tests/test_msg_properties.py +72 -0
- pyemailerajm-1.9/tests/test_searcher_factory.py +117 -0
- pyemailerajm-1.8.4/PyEmailerAJM/_version.py +0 -1
- pyemailerajm-1.8.4/PyEmailerAJM/searchers/__init__.py +0 -3
- pyemailerajm-1.8.4/PyEmailerAJM/searchers/searchers.py +0 -139
- pyemailerajm-1.8.4/README.md +0 -15
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/LICENSE.txt +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/errs.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/logger.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/the_sandman.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/__init__.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/__init__.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/continuous_colorizer.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/email_state.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/snooze_tracking.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/msg/__init__.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/msg/factory.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/dependency_links.txt +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/requires.txt +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/top_level.txt +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/setup.cfg +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/setup.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/tests/test_PyEmailerAJM.py +0 -0
- {pyemailerajm-1.8.4 → pyemailerajm-1.9}/tests/test_snooze_tracking.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyEmailerAJM
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9
|
|
4
4
|
Summary: Allows for automating sending Email with the Outlook Desktop client. Future releases will add more client support
|
|
5
5
|
Home-page: https://github.com/amcsparron2793-Water/PyEmailer
|
|
6
|
-
Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.
|
|
6
|
+
Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.9.tar.gz
|
|
7
7
|
Author: Amcsparron
|
|
8
8
|
Author-email: amcsparron@albanyny.gov
|
|
9
9
|
License: MIT License
|
|
@@ -17,12 +17,12 @@ def is_instance_of_dynamic(obj: object, base_class_path: str) -> bool:
|
|
|
17
17
|
from PyEmailerAJM.backend import deprecated
|
|
18
18
|
from PyEmailerAJM.backend.errs import EmailerNotSetupError, DisplayManualQuit
|
|
19
19
|
from PyEmailerAJM.msg import Msg, FailedMsg
|
|
20
|
-
from PyEmailerAJM.searchers import
|
|
20
|
+
from PyEmailerAJM.searchers import SearcherFactory
|
|
21
21
|
from PyEmailerAJM.py_emailer_ajm import PyEmailer, EmailerInitializer
|
|
22
22
|
from PyEmailerAJM.continuous_monitor.continuous_monitor import ContinuousMonitor
|
|
23
23
|
|
|
24
24
|
__all__ = ['EmailerNotSetupError', 'DisplayManualQuit', 'deprecated',
|
|
25
25
|
'Msg', 'FailedMsg', 'PyEmailer', 'EmailerInitializer',
|
|
26
|
-
'
|
|
26
|
+
'SearcherFactory', 'ContinuousMonitor',
|
|
27
27
|
'is_instance_of_dynamic']
|
|
28
28
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.9'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from PyEmailerAJM.backend.errs import *
|
|
2
|
-
from PyEmailerAJM.backend.enums import BasicEmailFolderChoices, AlertTypes
|
|
2
|
+
from PyEmailerAJM.backend.enums import BasicEmailFolderChoices, AlertTypes, EmailMsgImportanceLevel
|
|
3
3
|
from PyEmailerAJM.backend.the_sandman import TheSandman
|
|
4
4
|
from PyEmailerAJM.backend.logger import PyEmailerLogger
|
|
5
5
|
import warnings
|
|
@@ -31,4 +31,4 @@ def deprecated(reason: str = ""):
|
|
|
31
31
|
__all__ = ['deprecated', 'EmailerNotSetupError', 'InvalidAlertLevel',
|
|
32
32
|
'DisplayManualQuit', 'NoMessagesFetched',
|
|
33
33
|
'UnrecognizedEmailError', 'BasicEmailFolderChoices',
|
|
34
|
-
'AlertTypes', 'TheSandman', 'PyEmailerLogger']
|
|
34
|
+
'AlertTypes', 'EmailMsgImportanceLevel','TheSandman', 'PyEmailerLogger']
|
|
@@ -33,4 +33,13 @@ class BasicEmailFolderChoices(IntEnum):
|
|
|
33
33
|
return self.name
|
|
34
34
|
|
|
35
35
|
def __repr__(self):
|
|
36
|
-
return f"<{self.__class__.__name__}.{self.name} ({self.value})>"
|
|
36
|
+
return f"<{self.__class__.__name__}.{self.name} ({self.value})>"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EmailMsgImportanceLevel(IntEnum):
|
|
40
|
+
LOW = 0
|
|
41
|
+
NORMAL = 1
|
|
42
|
+
HIGH = 2
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
return self.name
|
|
@@ -51,6 +51,7 @@ class ContinuousMonitorBase(PyEmailer, EmailState):
|
|
|
51
51
|
ATTRS_TO_CHECK = []
|
|
52
52
|
|
|
53
53
|
def __init__(self, display_window: bool, send_emails: bool, **kwargs):
|
|
54
|
+
# Let EmailerInitializer handle logger factory vs instance normalization
|
|
54
55
|
super().__init__(display_window, send_emails, **kwargs)
|
|
55
56
|
|
|
56
57
|
self.dev_mode = kwargs.get('dev_mode', False)
|
|
@@ -59,6 +60,14 @@ class ContinuousMonitorBase(PyEmailer, EmailState):
|
|
|
59
60
|
self.log_dev_mode_warnings()
|
|
60
61
|
self.email_handler_init()
|
|
61
62
|
|
|
63
|
+
@property
|
|
64
|
+
def num_snoozed_msgs(self):
|
|
65
|
+
if (self.snooze_tracker.json_loaded and
|
|
66
|
+
hasattr(self.snooze_tracker.json_loaded, '__len__')):
|
|
67
|
+
return len(self.snooze_tracker.json_loaded)
|
|
68
|
+
else:
|
|
69
|
+
return 0
|
|
70
|
+
|
|
62
71
|
@classmethod
|
|
63
72
|
def check_for_class_attrs(cls, class_attrs_to_check):
|
|
64
73
|
for c in class_attrs_to_check:
|
|
@@ -68,8 +77,10 @@ class ContinuousMonitorBase(PyEmailer, EmailState):
|
|
|
68
77
|
|
|
69
78
|
def initialize_helper_classes(self, **kwargs):
|
|
70
79
|
colorizer = ContinuousColorizer(logger=self.logger)
|
|
71
|
-
snooze_tracker = SnoozeTracking(
|
|
72
|
-
|
|
80
|
+
snooze_tracker = SnoozeTracking(
|
|
81
|
+
Path(kwargs.get('file_name', './snooze_tracker.json')),
|
|
82
|
+
logger=self.logger,
|
|
83
|
+
)
|
|
73
84
|
sleep_timer = TheSandman(sleep_time_seconds=kwargs.get('sleep_time_seconds', None), logger=self.logger)
|
|
74
85
|
return colorizer, snooze_tracker, sleep_timer
|
|
75
86
|
|
|
@@ -81,19 +92,25 @@ class ContinuousMonitorBase(PyEmailer, EmailState):
|
|
|
81
92
|
f" it will mock send emails but not actually send them to {self.__class__.ADMIN_EMAIL}"
|
|
82
93
|
)
|
|
83
94
|
|
|
95
|
+
# Issue with PyEmailer 1.8.5 causes the base version to disable email handler
|
|
96
|
+
# (issue with check for setup_email_handler attr) - below is a functional work around
|
|
84
97
|
def email_handler_init(self):
|
|
85
|
-
|
|
86
|
-
self.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
try:
|
|
99
|
+
if self.dev_mode:
|
|
100
|
+
self.logger.warning("email handler disabled for dev mode")
|
|
101
|
+
elif (not type(self).__name__ == "ContinuousMonitorAlertSend"
|
|
102
|
+
and not is_instance_of_dynamic(self, "__main__.ContinuousMonitorAlertSend")):
|
|
103
|
+
self.logger.warning(
|
|
104
|
+
f"email handler not initialized because this is not a ContinuousMonitorAlertSend subclass"
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
self.logger_class.setup_email_handler(email_msg=self.email,
|
|
108
|
+
logger_admins=self.__class__.ADMIN_EMAIL_LOGGER)
|
|
109
|
+
self.email = self.initialize_new_email()
|
|
110
|
+
self.logger.info("email handler initialized, initialized a new email object for use by monitor")
|
|
111
|
+
except AttributeError as e:
|
|
112
|
+
self.logger.error(f"email handler not initialized because {e}")
|
|
113
|
+
pass
|
|
97
114
|
|
|
98
115
|
def _print_and_postprocess(self, alert_level):
|
|
99
116
|
"""
|
{pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/continuous_monitor.py
RENAMED
|
@@ -9,6 +9,8 @@ from PyEmailerAJM.msg import MsgFactory
|
|
|
9
9
|
class ContinuousMonitor(ContinuousMonitorBase):
|
|
10
10
|
TITLE_STRING = " Watching for emails with alerts in {} folder ".center(100, '*')
|
|
11
11
|
MSG_FACTORY_CLASS: MsgFactory = MsgFactory
|
|
12
|
+
ALERT_CHECK_STR = "Checking for emails with an alert..."
|
|
13
|
+
NO_ALERTS_STR = "No emails with an alert detected in {read_folder} ({num_snoozed} snoozed)."
|
|
12
14
|
|
|
13
15
|
def GetMessages(self, folder_index=None):
|
|
14
16
|
"""
|
|
@@ -18,7 +20,9 @@ class ContinuousMonitor(ContinuousMonitorBase):
|
|
|
18
20
|
:rtype: list
|
|
19
21
|
"""
|
|
20
22
|
msgs = super().GetMessages(folder_index)
|
|
21
|
-
sorted_msgs = [
|
|
23
|
+
sorted_msgs = [
|
|
24
|
+
self.__class__.MSG_FACTORY_CLASS.get_msg(x, logger=self.logger, snooze_checker=self.snooze_tracker) for x in
|
|
25
|
+
msgs]
|
|
22
26
|
alert_messages = [x for x in sorted_msgs if x is not None and x.msg_alert]
|
|
23
27
|
return alert_messages
|
|
24
28
|
|
|
@@ -37,7 +41,8 @@ class ContinuousMonitor(ContinuousMonitorBase):
|
|
|
37
41
|
def _postprocess_alert(self, alert_level=None, **kwargs):
|
|
38
42
|
...
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
# TODO: make no_alerts_string property so there is more flexibility with format
|
|
45
|
+
def check_for_alerts(self, **kwargs):
|
|
41
46
|
"""
|
|
42
47
|
Checks for emails in the specified folder and identifies if there are any alerts. Alerts,
|
|
43
48
|
if present, are categorized as overdue, warning, or critical warning, and are processed accordingly.
|
|
@@ -47,8 +52,10 @@ class ContinuousMonitor(ContinuousMonitorBase):
|
|
|
47
52
|
:rtype: None
|
|
48
53
|
|
|
49
54
|
"""
|
|
50
|
-
|
|
55
|
+
alert_check_string = kwargs.get('alert_check_string', self.__class__.ALERT_CHECK_STR)
|
|
56
|
+
self.logger.info(alert_check_string, print_msg=True)
|
|
51
57
|
self.refresh_messages()
|
|
58
|
+
|
|
52
59
|
if self.has_overdue:
|
|
53
60
|
self._print_and_postprocess(AlertTypes.OVERDUE)
|
|
54
61
|
|
|
@@ -59,7 +66,10 @@ class ContinuousMonitor(ContinuousMonitorBase):
|
|
|
59
66
|
self._print_and_postprocess(AlertTypes.CRITICAL_WARNING)
|
|
60
67
|
|
|
61
68
|
else:
|
|
62
|
-
|
|
69
|
+
no_alert_str = kwargs.get('no_alert_string',
|
|
70
|
+
self.__class__.NO_ALERTS_STR.format(read_folder=self.read_folder,
|
|
71
|
+
num_snoozed=self.num_snoozed_msgs))
|
|
72
|
+
self.logger.info(no_alert_str, print_msg=True)
|
|
63
73
|
|
|
64
74
|
self.snooze_tracker.snooze_msgs(self.all_messages)
|
|
65
75
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from PyEmailerAJM.continuous_monitor import ContinuousMonitor
|
|
4
|
+
from PyEmailerAJM.backend import EmailMsgImportanceLevel
|
|
5
|
+
|
|
6
|
+
# This is installed as part of pywin32
|
|
7
|
+
# noinspection PyUnresolvedReferences
|
|
8
|
+
from pythoncom import com_error
|
|
4
9
|
|
|
5
10
|
NO_COLORIZER = False
|
|
6
11
|
|
|
@@ -14,6 +19,8 @@ class ContinuousMonitorAlertSend(ContinuousMonitor):
|
|
|
14
19
|
"Thanks,\n"
|
|
15
20
|
"{email_sender}")
|
|
16
21
|
ATTRS_TO_CHECK = ['ADMIN_EMAIL', 'ADMIN_EMAIL_LOGGER']
|
|
22
|
+
ALERT_EMAIL_IMPORTANCE = EmailMsgImportanceLevel.HIGH
|
|
23
|
+
DEFAULT_EMAIL_IMPORTANCE = EmailMsgImportanceLevel.NORMAL
|
|
17
24
|
|
|
18
25
|
def __init__(self, display_window: bool, send_emails: bool, **kwargs):
|
|
19
26
|
|
|
@@ -25,14 +32,11 @@ class ContinuousMonitorAlertSend(ContinuousMonitor):
|
|
|
25
32
|
self.logger.warning(f"IS DEV MODE - NOT checking for class attributes "
|
|
26
33
|
f"({', '.join(self.__class__.ATTRS_TO_CHECK)}) for ContinuousMonitorAlertSend")
|
|
27
34
|
|
|
28
|
-
def __init_subclass__(cls, **kwargs):
|
|
29
|
-
cls.check_for_class_attrs(cls.ATTRS_TO_CHECK)
|
|
30
|
-
|
|
31
35
|
def _set_args_for_endless_watch(self):
|
|
32
36
|
self.send_emails = True
|
|
33
37
|
self.auto_send = True
|
|
34
38
|
self.display_window = False
|
|
35
|
-
self.logger.debug("send_emails, auto_send,
|
|
39
|
+
self.logger.debug("Configured endless_watch: send_emails=True, auto_send=True, display_window=False")
|
|
36
40
|
|
|
37
41
|
def SetupEmail(self, recipient: Optional[str] = None, subject: str = DEFAULT_SUBJECT,
|
|
38
42
|
text: str = None, attachments: list = None, **kwargs):
|
|
@@ -102,10 +106,26 @@ class ContinuousMonitorAlertSend(ContinuousMonitor):
|
|
|
102
106
|
).replace('\n', '<br>')
|
|
103
107
|
return formatted_full_body
|
|
104
108
|
|
|
109
|
+
def _set_email_importance(self, importance_level=None, **kwargs):
|
|
110
|
+
default_importance = kwargs.get('default_importance', self.__class__.DEFAULT_EMAIL_IMPORTANCE)
|
|
111
|
+
try:
|
|
112
|
+
if importance_level is None:
|
|
113
|
+
self.email.importance = self.__class__.ALERT_EMAIL_IMPORTANCE
|
|
114
|
+
else:
|
|
115
|
+
self.email.importance = importance_level
|
|
116
|
+
except (com_error, TypeError) as e:
|
|
117
|
+
self.logger.warning(f"Invalid Importance level ({importance_level}) for email,"
|
|
118
|
+
f" setting to {default_importance}")
|
|
119
|
+
self.email.importance = default_importance
|
|
120
|
+
return self.email
|
|
121
|
+
return self.email
|
|
122
|
+
|
|
105
123
|
def _postprocess_alert(self, alert_level=None, **kwargs):
|
|
124
|
+
self._set_email_importance(**kwargs)
|
|
106
125
|
self.SendOrDisplay(**kwargs)
|
|
107
126
|
|
|
108
127
|
def refresh_messages(self):
|
|
128
|
+
self.email = self.initialize_new_email()
|
|
109
129
|
self.SetupEmail()
|
|
110
130
|
super().refresh_messages()
|
|
111
131
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import inspect
|
|
2
4
|
|
|
3
5
|
import win32com.client as win32
|
|
4
6
|
# pylint: disable=import-error
|
|
@@ -7,7 +9,58 @@ from PyEmailerAJM.backend import TheSandman
|
|
|
7
9
|
from PyEmailerAJM.backend import AlertTypes
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
class
|
|
12
|
+
class _AlertCheckMethods:
|
|
13
|
+
"""
|
|
14
|
+
Provides utility methods for checking specific keywords in various parts of a message,
|
|
15
|
+
such as the subject, body, or attachment names.
|
|
16
|
+
|
|
17
|
+
This class contains several class methods designed to verify whether a candidate string,
|
|
18
|
+
message subject, body, or attachment names include any predefined alert keywords.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
DEFAULT_ALERT_CHECK_METHOD_NAMES = ['_check_subject_for_keys',
|
|
23
|
+
'_check_body_for_keys',
|
|
24
|
+
'_check_attachment_name_for_keys']
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def _validate_alert_check_methods(cls, alert_check_methods: list = None):
|
|
28
|
+
if not alert_check_methods:
|
|
29
|
+
alert_check_methods = cls.DEFAULT_ALERT_CHECK_METHOD_NAMES
|
|
30
|
+
if all([callable(getattr(cls, x)) for x in alert_check_methods]):
|
|
31
|
+
alert_check_methods = [getattr(cls, x) for x in alert_check_methods]
|
|
32
|
+
else:
|
|
33
|
+
raise AttributeError('alert_check_methods must be a list of callable objects '
|
|
34
|
+
'that accept a single Msg argument', name=None)
|
|
35
|
+
return alert_check_methods
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _check_string_for_keys(cls, candidate_string: str):
|
|
39
|
+
if any((x for x in getattr(cls, 'ALERT_SUBJECT_KEYWORDS')
|
|
40
|
+
if x.lower() in candidate_string.lower())):
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def _check_subject_for_keys(cls, msg: Msg):
|
|
46
|
+
return cls._check_string_for_keys(msg.subject)
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def _check_body_for_keys(cls, msg: Msg):
|
|
50
|
+
return cls._check_string_for_keys(msg.body)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def _check_attachment_name_for_keys(cls, msg: Msg):
|
|
54
|
+
for a in msg.attachments:
|
|
55
|
+
try:
|
|
56
|
+
if cls._check_string_for_keys(Path(a).resolve().stem):
|
|
57
|
+
return True
|
|
58
|
+
except (ValueError, Exception):
|
|
59
|
+
continue
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _AlertMsgBase(Msg, _AlertCheckMethods):
|
|
11
64
|
"""
|
|
12
65
|
A base class for alert message handling that inherits from ``Msg``.
|
|
13
66
|
This class is designed to evaluate whether a message meets specific
|
|
@@ -74,7 +127,6 @@ class _AlertMsgBase(Msg):
|
|
|
74
127
|
|
|
75
128
|
@classmethod
|
|
76
129
|
def AlertMsgBaseCheckClsAttrs(cls):
|
|
77
|
-
# FIXME: this should go with each of the alert_msgs??
|
|
78
130
|
if issubclass(cls, _AlertMsgBase):
|
|
79
131
|
cls.check_for_class_attrs(cls.ATTRS_TO_CHECK)
|
|
80
132
|
|
|
@@ -89,6 +141,8 @@ class _AlertMsgBase(Msg):
|
|
|
89
141
|
snooze_checker_entry = self.snooze_checker.read_entry(self.subject)
|
|
90
142
|
|
|
91
143
|
if not snooze_checker_entry and self.msg_snoozed_time:
|
|
144
|
+
# FIXME: is this the cause of the "\snooze_tracking.py", line 102, in write_entry
|
|
145
|
+
# TypeError: fromisoformat: argument must be str
|
|
92
146
|
snooze_expired = TheSandman.is_snooze_expired(self.msg_snoozed_time)
|
|
93
147
|
elif not snooze_checker_entry and not self.msg_snoozed_time:
|
|
94
148
|
snooze_expired = True
|
|
@@ -177,15 +231,24 @@ class _AlertMsgBase(Msg):
|
|
|
177
231
|
return super()._msg_is_recent(recent_days_cap=days_limit)
|
|
178
232
|
|
|
179
233
|
@classmethod
|
|
180
|
-
def msg_is_alert(cls, msg: Msg):
|
|
234
|
+
def msg_is_alert(cls, msg: Msg, **kwargs):
|
|
181
235
|
"""
|
|
182
|
-
Checks
|
|
183
|
-
|
|
184
|
-
|
|
236
|
+
Checks whether a given message qualifies as an alert based on specific
|
|
237
|
+
validation methods.
|
|
238
|
+
|
|
239
|
+
This method iterates over a set of alert validation methods and applies
|
|
240
|
+
them to the provided message. If any of the methods validate the message
|
|
241
|
+
as an alert, the method returns True. Otherwise, it returns False.
|
|
242
|
+
|
|
243
|
+
:param msg: The message object to determine if it is an alert.
|
|
244
|
+
:type msg: Msg
|
|
245
|
+
:param kwargs: Additional parameters passed to alert validation methods.
|
|
246
|
+
:type kwargs: dict
|
|
247
|
+
:return: True if the message is an alert, False otherwise.
|
|
185
248
|
:rtype: bool
|
|
186
249
|
"""
|
|
187
|
-
|
|
188
|
-
|
|
250
|
+
alert_check_methods = [x(msg) for x in cls._validate_alert_check_methods(**kwargs)]
|
|
251
|
+
if any(alert_check_methods):
|
|
189
252
|
return True
|
|
190
253
|
return False
|
|
191
254
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
1
3
|
from ..backend.errs import UnrecognizedEmailError
|
|
4
|
+
from ..backend.enums import EmailMsgImportanceLevel
|
|
2
5
|
from abc import abstractmethod
|
|
3
6
|
from os.path import isfile, isabs, abspath, join
|
|
4
7
|
from tempfile import gettempdir
|
|
@@ -9,7 +12,7 @@ from pywintypes import com_error
|
|
|
9
12
|
import datetime
|
|
10
13
|
import extract_msg
|
|
11
14
|
from bs4 import BeautifulSoup
|
|
12
|
-
from logging import Logger, getLogger,
|
|
15
|
+
from logging import Logger, getLogger, info
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class _BasicMsgProperties:
|
|
@@ -46,6 +49,7 @@ class _BasicMsgProperties:
|
|
|
46
49
|
def to(self):
|
|
47
50
|
return self.email_item.To if hasattr(self.email_item, 'To') else self.email_item.to
|
|
48
51
|
|
|
52
|
+
@property
|
|
49
53
|
def cc(self):
|
|
50
54
|
return self.email_item.CC if hasattr(self.email_item, 'CC') else self.email_item.cc
|
|
51
55
|
|
|
@@ -70,6 +74,23 @@ class _BasicMsgProperties:
|
|
|
70
74
|
def attachments(self, value: list):
|
|
71
75
|
self._validate_and_add_attachments(self.email_item, value)
|
|
72
76
|
|
|
77
|
+
@property
|
|
78
|
+
def importance(self):
|
|
79
|
+
return self.email_item.Importance
|
|
80
|
+
|
|
81
|
+
@importance.setter
|
|
82
|
+
def importance(self, value: Union[EmailMsgImportanceLevel, str, int]):
|
|
83
|
+
if value in EmailMsgImportanceLevel or value in [x.name for x in EmailMsgImportanceLevel]:
|
|
84
|
+
if isinstance(value, str):
|
|
85
|
+
value = EmailMsgImportanceLevel[value].value
|
|
86
|
+
elif isinstance(value, EmailMsgImportanceLevel):
|
|
87
|
+
value = value.value
|
|
88
|
+
elif isinstance(value, int):
|
|
89
|
+
pass
|
|
90
|
+
self.email_item.Importance = value
|
|
91
|
+
else:
|
|
92
|
+
raise TypeError(f"Invalid importance level: {value}")
|
|
93
|
+
|
|
73
94
|
|
|
74
95
|
class Msg(_BasicMsgProperties):
|
|
75
96
|
def __init__(self, email_item: win32.CDispatch or extract_msg.Message, **kwargs):
|
|
@@ -15,7 +15,7 @@ import win32com.client as win32
|
|
|
15
15
|
# This is installed as part of pywin32
|
|
16
16
|
# noinspection PyUnresolvedReferences
|
|
17
17
|
from pythoncom import com_error
|
|
18
|
-
from logging import Logger,
|
|
18
|
+
from logging import Logger, StreamHandler
|
|
19
19
|
from email_validator import validate_email, EmailNotValidError
|
|
20
20
|
import questionary
|
|
21
21
|
# this is usually thrown when questionary is used in the dev/Non Win32 environment
|
|
@@ -26,7 +26,7 @@ from PyEmailerAJM import (EmailerNotSetupError, DisplayManualQuit,
|
|
|
26
26
|
deprecated,
|
|
27
27
|
Msg, FailedMsg)
|
|
28
28
|
from PyEmailerAJM.backend import BasicEmailFolderChoices, PyEmailerLogger
|
|
29
|
-
from PyEmailerAJM.searchers import
|
|
29
|
+
from PyEmailerAJM.searchers import SearcherFactory
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class EmailerInitializer:
|
|
@@ -47,11 +47,7 @@ class EmailerInitializer:
|
|
|
47
47
|
auto_send: bool = False,
|
|
48
48
|
email_app_name: str = DEFAULT_EMAIL_APP_NAME,
|
|
49
49
|
namespace_name: str = DEFAULT_NAMESPACE_NAME, **kwargs):
|
|
50
|
-
|
|
51
|
-
self.logger = logger
|
|
52
|
-
else:
|
|
53
|
-
self._elog = PyEmailerLogger(**kwargs)
|
|
54
|
-
self.logger = self._elog()
|
|
50
|
+
self.logger, self.logger_class = self.initialize_emailer_logger(logger, **kwargs)
|
|
55
51
|
# print("Dummy logger in use!")
|
|
56
52
|
|
|
57
53
|
self.email_app_name = email_app_name
|
|
@@ -63,6 +59,26 @@ class EmailerInitializer:
|
|
|
63
59
|
self.auto_send = auto_send
|
|
64
60
|
self.send_emails = send_emails
|
|
65
61
|
|
|
62
|
+
def initialize_emailer_logger(self, logger: Logger = None, **kwargs):
|
|
63
|
+
if logger:
|
|
64
|
+
# If a real logger instance was provided (has .info), use it directly
|
|
65
|
+
if hasattr(logger, 'info') and hasattr(logger, 'warning'):
|
|
66
|
+
self.logger = logger
|
|
67
|
+
self.logger_class = logger.__class__
|
|
68
|
+
# If a callable/factory was provided, call it to get the logger instance
|
|
69
|
+
elif callable(logger):
|
|
70
|
+
self.logger_class = logger
|
|
71
|
+
self.logger = self.logger_class()
|
|
72
|
+
else:
|
|
73
|
+
# Fallback: treat as an instance but avoid calling missing methods here
|
|
74
|
+
self.logger = logger
|
|
75
|
+
# Derive a class reference best-effort
|
|
76
|
+
self.logger_class = getattr(logger, '__class__', type(logger))
|
|
77
|
+
else:
|
|
78
|
+
self.logger_class = PyEmailerLogger(**kwargs)
|
|
79
|
+
self.logger = self.logger_class()
|
|
80
|
+
return self.logger, self.logger_class
|
|
81
|
+
|
|
66
82
|
def initialize_new_email(self):
|
|
67
83
|
if hasattr(self, 'email_app') and self.email_app is not None:
|
|
68
84
|
self.email = Msg(self.email_app.CreateItem(0), logger=self.logger)
|
|
@@ -88,7 +104,7 @@ class EmailerInitializer:
|
|
|
88
104
|
return self.email_app, self.namespace
|
|
89
105
|
|
|
90
106
|
|
|
91
|
-
class PyEmailer(EmailerInitializer
|
|
107
|
+
class PyEmailer(EmailerInitializer):
|
|
92
108
|
"""
|
|
93
109
|
The `PyEmailer` class is designed for managing and handling email-related operations.
|
|
94
110
|
It initializes email client settings, manages email folders, handles email messages,
|
|
@@ -138,7 +154,9 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
138
154
|
auto_send: bool = False, email_app_name: str = EmailerInitializer.DEFAULT_EMAIL_APP_NAME,
|
|
139
155
|
namespace_name: str = EmailerInitializer.DEFAULT_NAMESPACE_NAME, **kwargs):
|
|
140
156
|
|
|
141
|
-
super().__init__(display_window, send_emails, logger,
|
|
157
|
+
super().__init__(display_window, send_emails, logger,
|
|
158
|
+
auto_send, email_app_name, namespace_name,
|
|
159
|
+
**kwargs)
|
|
142
160
|
self._setup_was_run = False
|
|
143
161
|
self._current_user_email = None
|
|
144
162
|
|
|
@@ -147,6 +165,11 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
147
165
|
self._email_signature = None
|
|
148
166
|
self._send_success = False
|
|
149
167
|
self.email_sig_filename = email_sig_filename
|
|
168
|
+
self.searcher = SearcherFactory().get_searcher(search_type=kwargs.pop('search_type', 'subject'),
|
|
169
|
+
get_messages=kwargs.pop('get_messages', self.GetMessages),
|
|
170
|
+
logger=self.logger,
|
|
171
|
+
**kwargs)
|
|
172
|
+
self.logger.info(f"searcher {self.searcher.__class__.__name__} initialized.")
|
|
150
173
|
|
|
151
174
|
@property
|
|
152
175
|
def current_user_email(self):
|
|
@@ -169,6 +192,30 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
169
192
|
def email_signature(self):
|
|
170
193
|
return self._email_signature
|
|
171
194
|
|
|
195
|
+
def _read_email_sig_file(self, sig_full_path: str):
|
|
196
|
+
"""
|
|
197
|
+
Reads the content of an email signature file from the specified path. The method
|
|
198
|
+
attempts to decode the file using multiple encodings to ensure compatibility
|
|
199
|
+
with common formats, particularly those used by Outlook for .txt signature files.
|
|
200
|
+
|
|
201
|
+
:param sig_full_path: Path to the email signature file to be read.
|
|
202
|
+
:type sig_full_path: str
|
|
203
|
+
:return: Content of the email signature if successfully read; otherwise, None.
|
|
204
|
+
:rtype: Optional[str]
|
|
205
|
+
"""
|
|
206
|
+
# Try common encodings for Outlook signature .txt files
|
|
207
|
+
try:
|
|
208
|
+
with open(sig_full_path, 'r', encoding='utf-16') as f:
|
|
209
|
+
return f.read().strip()
|
|
210
|
+
except UnicodeError:
|
|
211
|
+
# Fallback to UTF-8 with BOM or plain UTF-8
|
|
212
|
+
try:
|
|
213
|
+
with open(sig_full_path, 'r', encoding='utf-8-sig') as f:
|
|
214
|
+
return f.read().strip()
|
|
215
|
+
except Exception as e:
|
|
216
|
+
self.logger.warning(e)
|
|
217
|
+
return None
|
|
218
|
+
|
|
172
219
|
@email_signature.getter
|
|
173
220
|
def email_signature(self):
|
|
174
221
|
if self.email_sig_filename:
|
|
@@ -183,8 +230,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
183
230
|
self._email_signature = None
|
|
184
231
|
|
|
185
232
|
if isfile(signature_full_path):
|
|
186
|
-
|
|
187
|
-
self._email_signature = f.read().strip()
|
|
233
|
+
self._email_signature = self._read_email_sig_file(signature_full_path)
|
|
188
234
|
else:
|
|
189
235
|
try:
|
|
190
236
|
raise FileNotFoundError(f"{signature_full_path} does not exist.")
|
|
@@ -226,7 +272,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
226
272
|
else:
|
|
227
273
|
print("Please respond with 'y' or 'n'.")
|
|
228
274
|
|
|
229
|
-
def display_tracker_check(self) -> bool:
|
|
275
|
+
def display_tracker_check(self) -> bool | None:
|
|
230
276
|
if self.display_window:
|
|
231
277
|
c = self._display_tracking_warning_confirm()
|
|
232
278
|
if c:
|
|
@@ -237,6 +283,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
237
283
|
except DisplayManualQuit as e:
|
|
238
284
|
self.logger.error(e, exc_info=True)
|
|
239
285
|
raise e
|
|
286
|
+
return None
|
|
240
287
|
|
|
241
288
|
def _get_default_folder_for_email_dir(self, email_dir_index: int = None, **kwargs):
|
|
242
289
|
# 6 = inbox
|
|
@@ -265,7 +312,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
265
312
|
self.logger.debug(f">>> email_dir_index not specified, defaulting to '{email_dir_index}' folder. <<<")
|
|
266
313
|
if not isinstance(email_dir_index, int):
|
|
267
314
|
self.logger.debug(f">>> email_dir_index is not an int, "
|
|
268
|
-
|
|
315
|
+
f"defaulting to {email_dir_index} folder and {subfolder_name} subfolder. <<<")
|
|
269
316
|
return self.namespace.Folders[email_dir_index].Folders[subfolder_name]
|
|
270
317
|
|
|
271
318
|
else:
|
|
@@ -284,6 +331,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
284
331
|
except TypeError as e:
|
|
285
332
|
self.logger.error(e, exc_info=True)
|
|
286
333
|
raise e
|
|
334
|
+
# noinspection PyUnresolvedReferences
|
|
287
335
|
return [Msg(m, logger=self.logger) for m in self.read_folder.Items]
|
|
288
336
|
|
|
289
337
|
@deprecated("use Msg classes body attribute instead")
|
|
@@ -299,12 +347,13 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
299
347
|
self.logger.error(e, exc_info=True)
|
|
300
348
|
raise e
|
|
301
349
|
|
|
350
|
+
# FIXME: this should be rewritten to use the searcher factory etc
|
|
302
351
|
@deprecated("use find_messages_by_subject instead")
|
|
303
352
|
def FindMsgBySubject(self, subject: str, forwarded_message_match: bool = True,
|
|
304
353
|
reply_msg_match: bool = True, partial_match_ok: bool = False):
|
|
305
|
-
return self.find_messages_by_subject(subject, include_fw=forwarded_message_match,
|
|
306
|
-
|
|
307
|
-
|
|
354
|
+
return self.searcher.find_messages_by_subject(subject, include_fw=forwarded_message_match,
|
|
355
|
+
include_re=reply_msg_match,
|
|
356
|
+
partial_match_ok=partial_match_ok)
|
|
308
357
|
|
|
309
358
|
def SaveAllEmailAttachments(self, msg, save_dir_path):
|
|
310
359
|
attachments = msg.Attachments
|
|
@@ -426,37 +475,25 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
426
475
|
results_string = self.__class__.FAILED_SEND_LOGGER_STRING.format(num=len(failed_sends),
|
|
427
476
|
recent_days_cap=recent_days_cap)
|
|
428
477
|
if (not self.logger.hasHandlers() or not any([isinstance(x, StreamHandler)
|
|
429
|
-
|
|
478
|
+
for x in self.logger.handlers])):
|
|
430
479
|
print(results_string)
|
|
431
480
|
self.logger.info(results_string)
|
|
432
481
|
return failed_sends
|
|
433
482
|
|
|
434
483
|
|
|
435
|
-
def __failed_sends_test(emailer):
|
|
436
|
-
failed_sends = emailer.get_failed_sends(recent_days_cap=1)
|
|
437
|
-
fs_results = ([(x.get('err_info').get('send_time'),
|
|
438
|
-
x.get('err_info').get('failed_subject'))
|
|
439
|
-
for x in failed_sends]
|
|
440
|
-
if failed_sends else "no failed sends found")
|
|
441
|
-
print(fs_results)
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def __setup_and_send_test(emailer):
|
|
445
|
-
emailer.SetupEmail(subject="TEST: Your TEST agreement expires in 30 days or less!",
|
|
446
|
-
recipient='amcsparron@albanyny.gov',
|
|
447
|
-
text="testing to see anything works", bcc='amcsparron@albanyny.gov')
|
|
448
|
-
emailer.SendOrDisplay()
|
|
449
|
-
|
|
450
|
-
|
|
451
484
|
if __name__ == "__main__":
|
|
452
485
|
module_name = __file__.split('\\')[-1].split('.py')[0]
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
m = em.
|
|
456
|
-
print([
|
|
486
|
+
em = PyEmailer(display_window=False, send_emails=True, auto_send=False, use_default_logger=False,
|
|
487
|
+
show_warning_logs_in_console=True)
|
|
488
|
+
m = em.searcher.find_messages_by_attribute('Andrew', partial_match_ok=True, no_fastpath_search=True)
|
|
489
|
+
print([(m.__class__, m.sender, m.sender_email_type, m.subject)
|
|
490
|
+
for m in [Msg(y) for y in m]])
|
|
457
491
|
# __setup_and_send_test(em)
|
|
458
492
|
# __failed_sends_test(em)
|
|
459
|
-
x = em.
|
|
493
|
+
x = em.searcher.find_messages_by_attribute("Exchange St. Site",
|
|
494
|
+
no_fastpath_search=True,
|
|
495
|
+
partial_match_ok=True)
|
|
496
|
+
print(type(x[0]))
|
|
460
497
|
# [x.name for x in m.ItemProperties]
|
|
461
498
|
print([(m.__class__, m.sender, m.sender_email_type, m.subject)
|
|
462
499
|
for m in [Msg(y) for y in x]]) # for m in x])
|