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.
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PKG-INFO +3 -2
- pyemailerajm-1.9.4/PyEmailerAJM/_version.py +1 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/the_sandman.py +55 -19
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/continuous_monitor_base.py +19 -4
- pyemailerajm-1.9.4/PyEmailerAJM/continuous_monitor/backend/email_state.py +153 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/continuous_monitor.py +14 -2
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/continuous_monitor_alert_send.py +1 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/py_emailer_ajm.py +21 -7
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/PKG-INFO +3 -2
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/requires.txt +1 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/setup.py +2 -1
- pyemailerajm-1.9.3.2/PyEmailerAJM/_version.py +0 -1
- pyemailerajm-1.9.3.2/PyEmailerAJM/continuous_monitor/backend/email_state.py +0 -127
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/LICENSE.txt +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/__init__.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/__init__.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/enums.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/errs.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/backend/logger.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/__init__.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/__init__.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/continuous_colorizer.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/snooze_tracking.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/msg/__init__.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/msg/alert_messages.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/msg/factory.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/msg/msg.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/searchers/__init__.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/searchers/factory.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/searchers/searchers.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/SOURCES.txt +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/dependency_links.txt +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM.egg-info/top_level.txt +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/README.md +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/setup.cfg +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_PyEmailerAJM.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_continuous_monitor_base.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_email_signature.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_logger.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_msg_properties.py +0 -0
- {pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/tests/test_searcher_factory.py +0 -0
- {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
|
+
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.
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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)
|
{pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/continuous_monitor.py
RENAMED
|
@@ -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',
|
|
@@ -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',
|
|
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"
|
|
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"
|
|
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
|
+
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.
|
|
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
|
|
@@ -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',
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyemailerajm-1.9.3.2 → pyemailerajm-1.9.4}/PyEmailerAJM/continuous_monitor/backend/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|