PyEmailerAJM 1.9.3.2__tar.gz → 1.9.4__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 (42) hide show
  1. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PKG-INFO +3 -2
  2. pyemailerajm-1.9.4/PyEmailerAJM/_version.py +1 -0
  3. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/the_sandman.py +55 -19
  4. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/continuous_monitor_base.py +19 -4
  5. pyemailerajm-1.9.4/PyEmailerAJM/continuous_monitor/backend/email_state.py +153 -0
  6. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/continuous_monitor.py +14 -2
  7. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/continuous_monitor_alert_send.py +1 -0
  8. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/py_emailer_ajm.py +21 -7
  9. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/PKG-INFO +3 -2
  10. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/requires.txt +1 -0
  11. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/setup.py +2 -1
  12. pyemailerajm-1.9.3.2/PyEmailerAJM/_version.py +0 -1
  13. pyemailerajm-1.9.3.2/PyEmailerAJM/continuous_monitor/backend/email_state.py +0 -127
  14. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/LICENSE.txt +0 -0
  15. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/__init__.py +0 -0
  16. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/__init__.py +0 -0
  17. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/enums.py +0 -0
  18. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/errs.py +0 -0
  19. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/logger.py +0 -0
  20. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/__init__.py +0 -0
  21. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/__init__.py +0 -0
  22. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/continuous_colorizer.py +0 -0
  23. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/snooze_tracking.py +0 -0
  24. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/msg/__init__.py +0 -0
  25. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/msg/alert_messages.py +0 -0
  26. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/msg/factory.py +0 -0
  27. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/msg/msg.py +0 -0
  28. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/searchers/__init__.py +0 -0
  29. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/searchers/factory.py +0 -0
  30. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/searchers/searchers.py +0 -0
  31. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/SOURCES.txt +0 -0
  32. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/dependency_links.txt +0 -0
  33. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/top_level.txt +0 -0
  34. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/README.md +0 -0
  35. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/setup.cfg +0 -0
  36. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_PyEmailerAJM.py +0 -0
  37. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_continuous_monitor_base.py +0 -0
  38. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_email_signature.py +0 -0
  39. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_logger.py +0 -0
  40. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_msg_properties.py +0 -0
  41. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_searcher_factory.py +0 -0
  42. {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_snooze_tracking.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyEmailerAJM
3
- Version: 1.9.3.2
3
+ Version: 1.9.4
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.9.3.2.tar.gz
6
+ Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.9.4.tar.gz
7
7
  Author: Amcsparron
8
8
  Author-email: amcsparron@albanyny.gov
9
9
  License: MIT License
@@ -15,6 +15,7 @@ Requires-Dist: email_validator
15
15
  Requires-Dist: questionary
16
16
  Requires-Dist: EasyLoggerAJM
17
17
  Requires-Dist: ColorizerAJM
18
+ Requires-Dist: tdqm
18
19
  Dynamic: author
19
20
  Dynamic: author-email
20
21
  Dynamic: download-url
@@ -0,0 +1 @@
1
+ __version__ = '1.9.4'
@@ -1,9 +1,12 @@
1
1
  from datetime import datetime
2
- from logging import getLogger
2
+ from logging import getLogger, Logger
3
3
  from time import sleep
4
4
  from typing import Union, Optional
5
5
 
6
+ from tqdm import tqdm
6
7
 
8
+
9
+ # TODO: extract into separate project and make it a dependency
7
10
  class TheSandman:
8
11
  """
9
12
  A utility class to facilitate and log time delays with custom sleep durations.
@@ -25,14 +28,19 @@ class TheSandman:
25
28
  DEFAULT_SLEEP_TIME_SECONDS = 600
26
29
  DEFAULT_SNOOZE_EXPIRATION_LIMIT_HOURS = 24
27
30
  SECONDS_IN_HOUR = 3600
31
+ DEFAULT_USE_VISUAL_SLEEP = True
28
32
 
29
33
  def __init__(self, sleep_time_seconds=None, **kwargs):
34
+ self.sleep_time_start = None
35
+ self.use_visual_sleep = kwargs.get('use_visual_sleep', self.__class__.DEFAULT_USE_VISUAL_SLEEP)
36
+
30
37
  self.snooze_expiration_limit_hours = kwargs.get('snooze_expiration_limit_hours',
31
38
  self.__class__.DEFAULT_SNOOZE_EXPIRATION_LIMIT_HOURS)
32
39
  self.sleep_time: int = sleep_time_seconds or self.__class__.DEFAULT_SLEEP_TIME_SECONDS
33
40
  self._is_time_remaining = False
34
41
  self._sleep_time_string = None
35
- self.logger: getLogger = kwargs.get('logger', getLogger(__name__))
42
+
43
+ self.logger: Logger = kwargs.get('logger', getLogger(__name__))
36
44
  self.sleep_time_string = self.sleep_time
37
45
  self.logger.info(f'TheSandman initialized - sleep time set as {self.sleep_time_string}')
38
46
 
@@ -51,6 +59,42 @@ class TheSandman:
51
59
  else:
52
60
  self._sleep_time_string = f'sleeping for {value} {more} second(s)'
53
61
 
62
+ str_parts = [self._sleep_time_string, f'(started at {self.sleep_time_start})']
63
+ self._sleep_time_string = ' '.join(str_parts)
64
+
65
+ def _setup_sleep_in_rounds(self, **kwargs):
66
+ self.sleep_time_start = datetime.now().strftime('%m/%d/%Y %H:%M')
67
+ if self.use_visual_sleep:
68
+ kwargs['print_msg'] = False
69
+ self._is_time_remaining = False
70
+ return kwargs
71
+
72
+ def _sleep_round(self, curr_sleep_round: int, total_rounds: int, print_msg: bool, **kwargs):
73
+ if curr_sleep_round == total_rounds - 1:
74
+ self._is_time_remaining = True
75
+ sleep_time_seconds = (self.sleep_time // total_rounds)
76
+ self.sleep(sleep_time_seconds, print_msg=print_msg, **kwargs)
77
+
78
+ def sleep_in_rounds(self, rounds=2, **kwargs):
79
+ kwargs = self._setup_sleep_in_rounds(**kwargs)
80
+
81
+ for sleep_round in range(rounds):
82
+ self._sleep_round(sleep_round, rounds, **kwargs)
83
+
84
+ def visual_sleep(self, sleep_time_seconds: int) -> None:
85
+ try:
86
+ for _ in tqdm(range(sleep_time_seconds),
87
+ desc=f"{self.sleep_time_string}",
88
+ unit="second"):
89
+ sleep(1)
90
+ except Exception as e:
91
+ if e.__class__.__name__ != 'KeyboardInterrupt':
92
+ self.logger.error(f"visual_sleep failed: {e}, turning off visual sleep and trying again...")
93
+ self.use_visual_sleep = False
94
+ self.sleep(sleep_time_seconds)
95
+ else:
96
+ raise
97
+
54
98
  def sleep(self, sleep_time_seconds: int, **kwargs):
55
99
  """
56
100
  :param sleep_time_seconds: The number of seconds the function should pause execution.
@@ -61,23 +105,10 @@ class TheSandman:
61
105
 
62
106
  self.sleep_time_string = self.sleep_time if not self._is_time_remaining else sleep_time_seconds
63
107
  self.logger.info(self.sleep_time_string, **kwargs)
64
- sleep(sleep_time_seconds)
65
-
66
- def sleep_in_rounds(self, rounds=2, **kwargs):
67
- """
68
- :param rounds: Number of intervals in which the total sleep time is divided. Defaults to 2.
69
- :type rounds: int
70
- :return: None
71
- :rtype: None
72
-
73
- """
74
- dev_mode = kwargs.pop('dev_mode', True)
75
- print_msg = kwargs.pop('print_msg', True)
76
- self._is_time_remaining = False
77
- for sr in range(rounds):
78
- if sr == rounds - 1:
79
- self._is_time_remaining = True
80
- self.sleep((self.sleep_time // rounds), print_msg=print_msg, **kwargs)
108
+ if self.use_visual_sleep:
109
+ self.visual_sleep(sleep_time_seconds)
110
+ else:
111
+ sleep(sleep_time_seconds)
81
112
 
82
113
  @classmethod
83
114
  def is_snooze_expired(cls, snoozed_at: datetime, snooze_expiration_limit_hours: Optional[int] = None):
@@ -89,3 +120,8 @@ class TheSandman:
89
120
  #print('msg_snoozed expired! Unsnoozing now!')
90
121
  return True
91
122
  return False
123
+
124
+
125
+ if __name__ == '__main__':
126
+ ts = TheSandman(sleep_time_seconds=30)
127
+ ts.sleep_in_rounds(rounds=3)
@@ -1,6 +1,7 @@
1
1
  from abc import abstractmethod
2
+ from os import getenv
2
3
  from pathlib import Path
3
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING, Optional, List
4
5
 
5
6
  from PyEmailerAJM import PyEmailer, is_instance_of_dynamic
6
7
  from PyEmailerAJM.backend import TheSandman
@@ -46,9 +47,9 @@ class ContinuousMonitorBase(PyEmailer, EmailState):
46
47
  Configures the email handler unless running in development mode. Provides appropriate logging
47
48
  based on the current mode.
48
49
  """
49
- ADMIN_EMAIL_LOGGER = []
50
- ADMIN_EMAIL = []
51
- ATTRS_TO_CHECK = []
50
+ ADMIN_EMAIL_LOGGER: List[str] = []
51
+ ADMIN_EMAIL: List[str] = []
52
+ ATTRS_TO_CHECK: List[str] = []
52
53
 
53
54
  def __init__(self, display_window: bool, send_emails: bool, **kwargs):
54
55
  # Let EmailerInitializer handle logger factory vs instance normalization
@@ -182,3 +183,17 @@ class ContinuousMonitorBase(PyEmailer, EmailState):
182
183
  @abstractmethod
183
184
  def _postprocess_alert(self, alert_level: Optional['AlertTypes'] = None, **kwargs):
184
185
  ...
186
+
187
+ def _GetReadFolder(self, email_dir_index: int = None, **kwargs):
188
+ """
189
+ :param email_dir_index: Specifies the email directory index to be accessed. Defaults to None.
190
+ :type email_dir_index: int, optional
191
+ :param kwargs: Additional optional arguments that may be passed. Can include `subfolder_name` to specify a subfolder name.
192
+ :type kwargs: dict
193
+ :return: The folder specified either by the email directory index or the default folder along with the subfolder if applicable.
194
+ :rtype: object
195
+ """
196
+ kwargs.setdefault('subfolder_name', self.__class__.DEFAULT_SUBFOLDER_NAME)
197
+ if not email_dir_index:
198
+ email_dir_index = self.__class__.DEFAULT_READ_FOLDER_NAME
199
+ return super()._GetReadFolder(email_dir_index, **kwargs)
@@ -0,0 +1,153 @@
1
+ from abc import abstractmethod, ABCMeta
2
+ from logging import Logger
3
+ from typing import Optional
4
+ from enum import Enum as _Enum
5
+ from PyEmailerAJM.backend import AlertTypes
6
+ from PyEmailerAJM.backend import NoMessagesFetched
7
+
8
+
9
+ class BaseEmailState(metaclass=ABCMeta):
10
+ """
11
+ Provides a base class for defining and managing the state of an email system.
12
+
13
+ This class is intended to be subclassed to define specific email state behaviors.
14
+ Subclasses should provide implementations for abstract methods and define the
15
+ required class-level variables to ensure proper functionality.
16
+
17
+ :ivar ALERT_ENUM: Enum to use for alert comparisons. Subclasses must define this.
18
+ :type ALERT_ENUM: Enum
19
+ :ivar ALERT_CRITICAL_MEMBERS: Tuple of enum member names that are critical and must exist
20
+ in the ALERT_ENUM. This must be defined by subclasses.
21
+ :type ALERT_CRITICAL_MEMBERS: tuple of str
22
+ """
23
+ # Enum to use for alert comparisons; can be overridden by subclasses
24
+ ALERT_ENUM = None
25
+ ALERT_CRITICAL_MEMBERS = ()
26
+
27
+ def __init_subclass__(cls, **kwargs):
28
+ super().__init_subclass__(**kwargs)
29
+ enum_cls = cls._validate_alert_enum()
30
+ cls._check_for_missing(enum_cls)
31
+
32
+ @classmethod
33
+ def _check_for_missing(cls, enum_cls):
34
+ critical_members: tuple[str, ...] = getattr(cls, 'ALERT_CRITICAL_MEMBERS', ())
35
+ missing = [m for m in critical_members if not hasattr(enum_cls, m)]
36
+ if missing:
37
+ raise AttributeError(
38
+ f"ALERT_ENUM must define members: {', '.join(critical_members)} Missing: {', '.join(missing)}"
39
+ )
40
+
41
+ @classmethod
42
+ def _validate_alert_enum(cls):
43
+ # Validate ALERT_ENUM is an Enum subclass and has required members
44
+ enum_cls = getattr(cls, 'ALERT_ENUM', None)
45
+ if enum_cls is None:
46
+ raise AttributeError("Subclasses of EmailState must define ALERT_ENUM.")
47
+ if not isinstance(enum_cls, type) or not issubclass(enum_cls, _Enum):
48
+ raise TypeError("ALERT_ENUM must be an Enum subclass.")
49
+ return enum_cls
50
+
51
+ def __init__(self):
52
+ self.logger: Optional[Logger] = None
53
+ self.all_messages = None
54
+ self._was_refreshed = False
55
+
56
+ @abstractmethod
57
+ def GetMessages(self):
58
+ """
59
+ Retrieve messages from the implemented source.
60
+
61
+ :return: Messages retrieved from the source
62
+ :rtype: list
63
+ """
64
+ ...
65
+
66
+ @abstractmethod
67
+ def SetupEmail(self):
68
+ ...
69
+
70
+ def _raise_no_messages(self):
71
+ """
72
+ Raises a NoMessagesFetched exception, indicating that the `all_messages` attribute has not been populated.
73
+ This suggests that the method `refresh_messages` should be executed to fetch and populate messages.
74
+
75
+ :raises NoMessagesFetched: Exception raised when no messages have been fetched.
76
+ """
77
+ raise NoMessagesFetched("all_messages has not been populated, run self.refresh_messages() first.")
78
+
79
+ def refresh_messages(self):
80
+ """
81
+ Refreshes the messages by retrieving them from the email folder.
82
+
83
+ :return: None
84
+ :rtype: None
85
+ """
86
+ self.logger.info("Refreshing messages from email folder...")
87
+ self.all_messages = self.GetMessages()
88
+ self._was_refreshed = True
89
+ self.logger.info("Successfully refreshed messages from email folder.")
90
+
91
+ def _has_alert_level(self, alert_level: _Enum) -> Optional[bool]:
92
+ """
93
+ Generic method to check if any messages in all_messages match the specified alert level.
94
+
95
+ :param alert_level: The enum member to check for.
96
+ :return: True if at least one message matches, False if none match, None if not refreshed.
97
+ """
98
+ if self.all_messages:
99
+ return any(x.__class__.ALERT_LEVEL == alert_level for x in self.all_messages)
100
+ if not self._was_refreshed:
101
+ self._raise_no_messages()
102
+ return False
103
+
104
+
105
+ class EmailState(BaseEmailState, metaclass=ABCMeta):
106
+ """
107
+ Represents an abstract base class for managing the state of email alerts and their associated levels.
108
+
109
+ The class provides properties to evaluate the presence of specific alert levels (e.g., overdue, critical warnings,
110
+ warnings) across the managed messages. Subclasses can customize the alert handling by overriding the
111
+ ALERT_ENUM attribute. This class is intended to be extended for more specific implementations of email
112
+ state management.
113
+
114
+ :ivar ALERT_ENUM: Enum to use for alert comparisons; can be overridden by subclasses.
115
+ :type ALERT_ENUM: type
116
+ :ivar ALERT_CRITICAL_MEMBERS: A tuple consisting of alert levels considered critical.
117
+ :type ALERT_CRITICAL_MEMBERS: tuple[str, ...]
118
+ """
119
+
120
+ # Enum to use for alert comparisons; can be overridden by subclasses
121
+ ALERT_ENUM: AlertTypes = AlertTypes
122
+ ALERT_CRITICAL_MEMBERS: tuple[str, ...] = ("WARNING", "CRITICAL_WARNING", "OVERDUE")
123
+
124
+ @property
125
+ def has_overdue(self):
126
+ """
127
+ Checks if there are any overdue messages among all messages. A message is considered overdue if its alert level
128
+ matches the AlertTypes.OVERDUE constant. If no messages have been fetched and the flag _was_refreshed is False,
129
+ it raises an exception indicating no messages are available.
130
+
131
+ :return: True if there are overdue messages, False otherwise.
132
+ :rtype: bool
133
+ """
134
+ return self._has_alert_level(self.__class__.ALERT_ENUM.OVERDUE)
135
+
136
+ @property
137
+ def has_critical_warning(self):
138
+ """
139
+ Checks if there are any messages with a critical warning alert level.
140
+
141
+ :return: A boolean indicating whether there is at least one message with a critical warning alert level
142
+ :rtype: bool
143
+
144
+ """
145
+ return self._has_alert_level(self.__class__.ALERT_ENUM.CRITICAL_WARNING)
146
+
147
+ @property
148
+ def has_warning(self):
149
+ """
150
+ :return: Indicates whether there are any messages of warning level present.
151
+ :rtype: bool
152
+ """
153
+ return self._has_alert_level(self.__class__.ALERT_ENUM.WARNING)
@@ -37,9 +37,21 @@ class ContinuousMonitor(ContinuousMonitorBase):
37
37
  self.auto_send = False
38
38
  self.display_window = False
39
39
 
40
- @abstractmethod
41
40
  def _postprocess_alert(self, alert_level=None, **kwargs):
42
- ...
41
+ """
42
+ Processes the alert after it has been generated, enabling customization or
43
+ additional handling based on the alert level and other contextual keyword arguments.
44
+
45
+ :param alert_level: The severity level of the alert, intended to define
46
+ the gravity or importance of the alert being processed.
47
+ :type alert_level: Optional
48
+ :param kwargs: Additional keyword arguments that can be provided for
49
+ further customization or handling during alert processing.
50
+ :type kwargs: dict
51
+ :return: The processed alert result.
52
+ :rtype: None
53
+ """
54
+ return
43
55
 
44
56
  def _process_no_alert(self, **kwargs):
45
57
  no_alert_str = kwargs.get('no_alert_string',
@@ -10,6 +10,7 @@ from pythoncom import com_error
10
10
  NO_COLORIZER = False
11
11
 
12
12
 
13
+ # TODO: create a version that does not monitor the inbox and only sends emails when triggered
13
14
  class ContinuousMonitorAlertSend(ContinuousMonitor):
14
15
  ADMIN_EMAIL_LOGGER = []
15
16
  ADMIN_EMAIL = []
@@ -5,7 +5,7 @@ py_emailer_ajm.py
5
5
  install win32 with pip install pywin32
6
6
  """
7
7
  # imports
8
- from os import environ
8
+ from os import environ, getenv
9
9
  from os.path import isfile, join, isdir
10
10
  from tempfile import gettempdir
11
11
  from typing import Optional
@@ -151,6 +151,10 @@ class PyEmailer(EmailerInitializer):
151
151
  DEFAULT_TEMP_SAVE_PATH = gettempdir()
152
152
  VALID_EMAIL_FOLDER_CHOICES = [x for x in BasicEmailFolderChoices]
153
153
 
154
+ # TODO: validate this works
155
+ DEFAULT_READ_FOLDER_NAME = getenv('READ_EMAIL_FOLDER', None)
156
+ DEFAULT_SUBFOLDER_NAME = getenv('READ_EMAIL_SUBFOLDER', 'Inbox')
157
+
154
158
  def __init__(self, display_window: bool, send_emails: bool, logger: Logger = None, email_sig_filename: str = None,
155
159
  auto_send: bool = False, email_app_name: str = EmailerInitializer.DEFAULT_EMAIL_APP_NAME,
156
160
  namespace_name: str = EmailerInitializer.DEFAULT_NAMESPACE_NAME, **kwargs):
@@ -172,11 +176,18 @@ class PyEmailer(EmailerInitializer):
172
176
  **kwargs)
173
177
  self.logger.info(f"searcher {self.searcher.__class__.__name__} initialized.")
174
178
 
179
+ @property
180
+ def current_session_exchange_user_email(self):
181
+ """Returns the primary SMTP address of the current user's Exchange account."""
182
+ session_current_user = self.namespace.Application.Session.CurrentUser
183
+ exchange_current_user = session_current_user.AddressEntry.GetExchangeUser()
184
+
185
+ return exchange_current_user.PrimarySmtpAddress
186
+
175
187
  @property
176
188
  def current_user_email(self):
177
189
  if self.email_app_name.lower().startswith('outlook'):
178
- self._current_user_email = (
179
- self.namespace.Application.Session.CurrentUser.AddressEntry.GetExchangeUser().PrimarySmtpAddress)
190
+ self._current_user_email = self.current_session_exchange_user_email
180
191
  return self._current_user_email
181
192
 
182
193
  @current_user_email.setter
@@ -307,13 +318,16 @@ class PyEmailer(EmailerInitializer):
307
318
  :return: The folder specified either by the email directory index or the default folder along with the subfolder if applicable.
308
319
  :rtype: object
309
320
  """
310
- subfolder_name = kwargs.get('subfolder_name', 'Inbox')
321
+ subfolder_name = kwargs.get('subfolder_name', self.__class__.DEFAULT_SUBFOLDER_NAME)
322
+ if not email_dir_index:
323
+ email_dir_index = self.__class__.DEFAULT_READ_FOLDER_NAME
324
+
311
325
  if not email_dir_index:
312
326
  email_dir_index = BasicEmailFolderChoices.INBOX
313
- self.logger.debug(f">>> email_dir_index not specified, defaulting to '{email_dir_index}' folder. <<<")
327
+ self.logger.debug(f"email_dir_index not specified, defaulting to '{email_dir_index}' folder.")
314
328
  if not isinstance(email_dir_index, int):
315
- self.logger.debug(f">>> email_dir_index is not an int, "
316
- f"defaulting to {email_dir_index} folder and {subfolder_name} subfolder. <<<")
329
+ self.logger.debug(f"email_dir_index is not an int, "
330
+ f"defaulting to {email_dir_index} folder and {subfolder_name} subfolder.")
317
331
  return self.namespace.Folders[email_dir_index].Folders[subfolder_name]
318
332
 
319
333
  else:
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyEmailerAJM
3
- Version: 1.9.3.2
3
+ Version: 1.9.4
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.9.3.2.tar.gz
6
+ Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.9.4.tar.gz
7
7
  Author: Amcsparron
8
8
  Author-email: amcsparron@albanyny.gov
9
9
  License: MIT License
@@ -15,6 +15,7 @@ Requires-Dist: email_validator
15
15
  Requires-Dist: questionary
16
16
  Requires-Dist: EasyLoggerAJM
17
17
  Requires-Dist: ColorizerAJM
18
+ Requires-Dist: tdqm
18
19
  Dynamic: author
19
20
  Dynamic: author-email
20
21
  Dynamic: download-url
@@ -4,3 +4,4 @@ email_validator
4
4
  questionary
5
5
  EasyLoggerAJM
6
6
  ColorizerAJM
7
+ tdqm
@@ -20,7 +20,8 @@ setup(
20
20
  url='https://github.com/amcsparron2793-Water/PyEmailer',
21
21
  download_url=f'https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/{get_property("__version__", project_name)}.tar.gz',
22
22
  keywords=["Outlook", "Email", "Automation"],
23
- install_requires=['pywin32', 'extract_msg', 'email_validator', 'questionary', 'EasyLoggerAJM', 'ColorizerAJM'],
23
+ install_requires=['pywin32', 'extract_msg', 'email_validator', 'questionary',
24
+ 'EasyLoggerAJM', 'ColorizerAJM', 'tdqm'],
24
25
  license='MIT License',
25
26
  author='Amcsparron',
26
27
  author_email='amcsparron@albanyny.gov',
@@ -1 +0,0 @@
1
- __version__ = '1.9.3.2'
@@ -1,127 +0,0 @@
1
- from abc import abstractmethod
2
- from logging import Logger
3
- from typing import Optional
4
- from PyEmailerAJM.backend import AlertTypes
5
- from PyEmailerAJM.backend import NoMessagesFetched
6
-
7
-
8
- class EmailState:
9
- """
10
- Represents the state and behavior associated with processing email messages
11
- and evaluating their alert levels (Overdue, Critical Warning, Warning).
12
-
13
- Attributes:
14
- logger: A logger object used to log messages and events during message processing.
15
- all_messages: A collection of all retrieved email messages.
16
- _was_refreshed: A boolean indicating whether the messages have been refreshed.
17
-
18
- Methods:
19
- __init__:
20
- Initializes the EmailState instance with default values.
21
-
22
- GetMessages:
23
- An abstract method to be implemented by subclasses for retrieving email messages.
24
-
25
- _raise_no_messages:
26
- Raises a NoMessagesFetched exception if email messages have not been populated.
27
-
28
- refresh_messages:
29
- Populates the `all_messages` attribute by fetching the latest messages
30
- using the `GetMessages` method and updates `_was_refreshed` to True.
31
-
32
- Properties:
33
- has_overdue:
34
- Indicates if there are any messages with an alert level of Overdue.
35
- Raises NoMessagesFetched if messages have not been refreshed.
36
-
37
- has_critical_warning:
38
- Indicates if there are any messages with an alert level of Critical Warning.
39
- Raises NoMessagesFetched if messages have not been refreshed.
40
-
41
- has_warning:
42
- Indicates if there are any messages with an alert level of Warning.
43
- Raises NoMessagesFetched if messages have not been refreshed.
44
- """
45
-
46
- def __init__(self):
47
- self.logger: Optional[Logger] = None
48
- self.all_messages = None
49
- self._was_refreshed = False
50
-
51
- @abstractmethod
52
- def GetMessages(self):
53
- """
54
- Retrieve messages from the implemented source.
55
-
56
- :return: Messages retrieved from the source
57
- :rtype: list
58
- """
59
- ...
60
-
61
- @abstractmethod
62
- def SetupEmail(self):
63
- ...
64
-
65
- def _raise_no_messages(self):
66
- """
67
- Raises a NoMessagesFetched exception, indicating that the `all_messages` attribute has not been populated.
68
- This suggests that the method `refresh_messages` should be executed to fetch and populate messages.
69
-
70
- :raises NoMessagesFetched: Exception raised when no messages have been fetched.
71
- """
72
- raise NoMessagesFetched("all_messages has not been populated, run self.refresh_messages() first.")
73
-
74
- def refresh_messages(self):
75
- """
76
- Refreshes the messages by retrieving them from the email folder.
77
-
78
- :return: None
79
- :rtype: None
80
- """
81
- self.logger.info("Refreshing messages from email folder...")
82
- self.all_messages = self.GetMessages()
83
- self._was_refreshed = True
84
- self.logger.info("Successfully refreshed messages from email folder.")
85
-
86
- @property
87
- def has_overdue(self):
88
- """
89
- Checks if there are any overdue messages among all messages. A message is considered overdue if its alert level
90
- matches the AlertTypes.OVERDUE constant. If no messages have been fetched and the flag _was_refreshed is False,
91
- it raises an exception indicating no messages are available.
92
-
93
- :return: True if there are overdue messages, False otherwise.
94
- :rtype: bool
95
- """
96
- if self.all_messages:
97
- return any([x for x in self.all_messages
98
- if x.__class__.ALERT_LEVEL == AlertTypes.OVERDUE])
99
- if not self._was_refreshed:
100
- self._raise_no_messages()
101
-
102
- @property
103
- def has_critical_warning(self):
104
- """
105
- Checks if there are any messages with a critical warning alert level.
106
-
107
- :return: A boolean indicating whether there is at least one message with a critical warning alert level
108
- :rtype: bool
109
-
110
- """
111
- if self.all_messages:
112
- return any([x for x in self.all_messages
113
- if x.__class__.ALERT_LEVEL == AlertTypes.CRITICAL_WARNING])
114
- elif not self._was_refreshed:
115
- self._raise_no_messages()
116
-
117
- @property
118
- def has_warning(self):
119
- """
120
- :return: Indicates whether there are any messages of warning level present.
121
- :rtype: bool
122
- """
123
- if self.all_messages:
124
- return any([x for x in self.all_messages
125
- if x.__class__.ALERT_LEVEL == AlertTypes.WARNING])
126
- elif not self._was_refreshed:
127
- self._raise_no_messages()
File without changes
File without changes
File without changes