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.
Files changed (44) hide show
  1. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PKG-INFO +2 -2
  2. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/__init__.py +2 -2
  3. pyemailerajm-1.9/PyEmailerAJM/_version.py +1 -0
  4. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/__init__.py +2 -2
  5. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/enums.py +10 -1
  6. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/continuous_monitor_base.py +31 -14
  7. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/continuous_monitor.py +14 -4
  8. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/continuous_monitor_alert_send.py +24 -4
  9. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/msg/alert_messages.py +71 -8
  10. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/msg/msg.py +22 -1
  11. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/py_emailer_ajm.py +75 -38
  12. pyemailerajm-1.9/PyEmailerAJM/searchers/__init__.py +29 -0
  13. pyemailerajm-1.9/PyEmailerAJM/searchers/factory.py +68 -0
  14. pyemailerajm-1.9/PyEmailerAJM/searchers/searchers.py +395 -0
  15. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/PKG-INFO +2 -2
  16. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/SOURCES.txt +5 -0
  17. pyemailerajm-1.9/README.md +143 -0
  18. pyemailerajm-1.9/tests/test_continuous_monitor_base.py +104 -0
  19. pyemailerajm-1.9/tests/test_email_signature.py +43 -0
  20. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/tests/test_logger.py +9 -0
  21. pyemailerajm-1.9/tests/test_msg_properties.py +72 -0
  22. pyemailerajm-1.9/tests/test_searcher_factory.py +117 -0
  23. pyemailerajm-1.8.4/PyEmailerAJM/_version.py +0 -1
  24. pyemailerajm-1.8.4/PyEmailerAJM/searchers/__init__.py +0 -3
  25. pyemailerajm-1.8.4/PyEmailerAJM/searchers/searchers.py +0 -139
  26. pyemailerajm-1.8.4/README.md +0 -15
  27. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/LICENSE.txt +0 -0
  28. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/errs.py +0 -0
  29. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/logger.py +0 -0
  30. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/backend/the_sandman.py +0 -0
  31. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/__init__.py +0 -0
  32. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/__init__.py +0 -0
  33. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/continuous_colorizer.py +0 -0
  34. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/email_state.py +0 -0
  35. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/continuous_monitor/backend/snooze_tracking.py +0 -0
  36. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/msg/__init__.py +0 -0
  37. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM/msg/factory.py +0 -0
  38. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/dependency_links.txt +0 -0
  39. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/requires.txt +0 -0
  40. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/PyEmailerAJM.egg-info/top_level.txt +0 -0
  41. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/setup.cfg +0 -0
  42. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/setup.py +0 -0
  43. {pyemailerajm-1.8.4 → pyemailerajm-1.9}/tests/test_PyEmailerAJM.py +0 -0
  44. {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.8.4
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.8.4.tar.gz
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 BaseSearcher, SubjectSearcher
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
- 'BaseSearcher', 'SubjectSearcher', 'ContinuousMonitor',
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(Path(kwargs.get('file_name', './snooze_tracker.json')),
72
- logger=kwargs.get('logger', self.logger))
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
- if self.dev_mode:
86
- self.logger.warning("email handler disabled for dev mode")
87
- elif (not type(self).__name__ == "ContinuousMonitorAlertSend"
88
- and not is_instance_of_dynamic(self, "__main__.ContinuousMonitorAlertSend")):
89
- self.logger.warning(
90
- f"email handler not initialized because this is not a ContinuousMonitorAlertSend subclass"
91
- )
92
- else:
93
- self._elog.setup_email_handler(email_msg=self.email,
94
- logger_admins=self.__class__.ADMIN_EMAIL_LOGGER)
95
- self.email = self.initialize_new_email()
96
- self.logger.info("email handler initialized, initialized a new email object for use by monitor")
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
  """
@@ -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 = [self.__class__.MSG_FACTORY_CLASS.get_msg(x, logger=self.logger, snooze_checker=self.snooze_tracker) for x in 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
- def check_for_alerts(self):
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
- self.logger.info("\nChecking for emails with an alert...", print_msg=True)
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
- self.logger.info(f"No emails with an alert detected in {self.read_folder}", print_msg=True)
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, and display_window set to True for endless_watch()")
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 _AlertMsgBase(Msg):
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 if the message subject contains any predefined alert keywords.
183
-
184
- :return: True if the subject contains any alert keywords, False otherwise
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
- if any((x for x in getattr(cls, 'ALERT_SUBJECT_KEYWORDS')
188
- if x.lower() in msg.subject.lower())):
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, warning, info
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, basicConfig, StreamHandler, FileHandler, getLogger
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 SubjectSearcher
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
- if logger:
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, SubjectSearcher):
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, auto_send, email_app_name, namespace_name, **kwargs)
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
- with open(signature_full_path, 'r', encoding='utf-16') as f:
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
- f"defaulting to {email_dir_index} folder and {subfolder_name} subfolder. <<<")
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
- include_re=reply_msg_match,
307
- partial_match_ok=partial_match_ok)
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
- for x in self.logger.handlers])):
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
- em = PyEmailer(display_window=False, send_emails=True, auto_send=False, use_default_logger=False)
455
- m = em.find_messages_by_subject('Andrew', partial_match_ok=True)
456
- print([type(x) for x in m])
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.find_messages_by_subject("GIS Request", partial_match_ok=True)
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])