PyEmailerAJM 1.7__tar.gz → 1.8__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.7 → pyemailerajm-1.8}/PKG-INFO +4 -2
- pyemailerajm-1.8/PyEmailerAJM/__init__.py +33 -0
- pyemailerajm-1.8/PyEmailerAJM/_version.py +1 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8}/PyEmailerAJM/py_emailer_ajm.py +66 -86
- {pyemailerajm-1.7 → pyemailerajm-1.8}/PyEmailerAJM.egg-info/PKG-INFO +4 -2
- {pyemailerajm-1.7 → pyemailerajm-1.8}/PyEmailerAJM.egg-info/SOURCES.txt +3 -2
- {pyemailerajm-1.7 → pyemailerajm-1.8}/PyEmailerAJM.egg-info/requires.txt +2 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8}/setup.py +1 -1
- {pyemailerajm-1.7 → pyemailerajm-1.8}/tests/test_PyEmailerAJM.py +2 -0
- pyemailerajm-1.8/tests/test_logger.py +83 -0
- pyemailerajm-1.8/tests/test_snooze_tracking.py +93 -0
- pyemailerajm-1.7/PyEmailerAJM/__init__.py +0 -8
- pyemailerajm-1.7/PyEmailerAJM/_version.py +0 -1
- pyemailerajm-1.7/PyEmailerAJM/helpers.py +0 -40
- {pyemailerajm-1.7 → pyemailerajm-1.8}/LICENSE.txt +0 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8}/PyEmailerAJM.egg-info/dependency_links.txt +0 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8}/PyEmailerAJM.egg-info/top_level.txt +0 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8}/README.md +0 -0
- {pyemailerajm-1.7 → pyemailerajm-1.8}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyEmailerAJM
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8
|
|
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.8.tar.gz
|
|
7
7
|
Author: Amcsparron
|
|
8
8
|
Author-email: amcsparron@albanyny.gov
|
|
9
9
|
License: MIT License
|
|
@@ -13,6 +13,8 @@ Requires-Dist: pywin32
|
|
|
13
13
|
Requires-Dist: extract_msg
|
|
14
14
|
Requires-Dist: email_validator
|
|
15
15
|
Requires-Dist: questionary
|
|
16
|
+
Requires-Dist: EasyLoggerAJM
|
|
17
|
+
Requires-Dist: ColorizerAJM
|
|
16
18
|
Dynamic: author
|
|
17
19
|
Dynamic: author-email
|
|
18
20
|
Dynamic: download-url
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
# TODO: add these to other projects? and or EasyLogger?
|
|
4
|
+
__project_root__ = Path(__file__).parent.parent
|
|
5
|
+
__project_name__ = __project_root__.name
|
|
6
|
+
print(f'logs for {__project_name__} found in {__project_root__ / "logs"}')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_instance_of_dynamic(obj: object, base_class_path: str) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Check if an object is an instance of a class or its subclass specified by its module path.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
module_path, class_name = base_class_path.rsplit('.', 1)
|
|
15
|
+
module = import_module(module_path)
|
|
16
|
+
base_class = getattr(module, class_name)
|
|
17
|
+
return isinstance(obj, base_class)
|
|
18
|
+
except (ImportError, AttributeError):
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from PyEmailerAJM.backend import deprecated
|
|
23
|
+
from PyEmailerAJM.backend.errs import EmailerNotSetupError, DisplayManualQuit
|
|
24
|
+
from PyEmailerAJM.msg import Msg, FailedMsg
|
|
25
|
+
from PyEmailerAJM.searchers import BaseSearcher, SubjectSearcher
|
|
26
|
+
from PyEmailerAJM.py_emailer_ajm import PyEmailer, EmailerInitializer
|
|
27
|
+
from PyEmailerAJM.continuous_monitor.continuous_monitor import ContinuousMonitor
|
|
28
|
+
|
|
29
|
+
__all__ = ['EmailerNotSetupError', 'DisplayManualQuit', 'deprecated',
|
|
30
|
+
'Msg', 'FailedMsg', 'PyEmailer', 'EmailerInitializer',
|
|
31
|
+
'BaseSearcher', 'SubjectSearcher', 'ContinuousMonitor',
|
|
32
|
+
'__project_root__', '__project_name__', 'is_instance_of_dynamic']
|
|
33
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.8'
|
|
@@ -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
|
|
18
|
+
from logging import Logger, basicConfig, StreamHandler, FileHandler, getLogger
|
|
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
|
|
@@ -25,7 +25,7 @@ from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
|
|
|
25
25
|
from PyEmailerAJM import (EmailerNotSetupError, DisplayManualQuit,
|
|
26
26
|
deprecated,
|
|
27
27
|
Msg, FailedMsg)
|
|
28
|
-
from PyEmailerAJM.backend import BasicEmailFolderChoices
|
|
28
|
+
from PyEmailerAJM.backend import BasicEmailFolderChoices, PyEmailerLogger
|
|
29
29
|
from PyEmailerAJM.searchers import SubjectSearcher
|
|
30
30
|
|
|
31
31
|
|
|
@@ -38,8 +38,11 @@ class EmailerInitializer:
|
|
|
38
38
|
auto_send: bool = False,
|
|
39
39
|
email_app_name: str = DEFAULT_EMAIL_APP_NAME,
|
|
40
40
|
namespace_name: str = DEFAULT_NAMESPACE_NAME, **kwargs):
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
if logger:
|
|
42
|
+
self.logger = logger
|
|
43
|
+
else:
|
|
44
|
+
self._elog = PyEmailerLogger(**kwargs)
|
|
45
|
+
self.logger = self._elog()
|
|
43
46
|
# print("Dummy logger in use!")
|
|
44
47
|
|
|
45
48
|
self.email_app_name = email_app_name
|
|
@@ -51,54 +54,9 @@ class EmailerInitializer:
|
|
|
51
54
|
self.auto_send = auto_send
|
|
52
55
|
self.send_emails = send_emails
|
|
53
56
|
|
|
54
|
-
# TODO: replace me with EasyLoggerAJM
|
|
55
|
-
def _initialize_logger(self, logger=None, **kwargs):
|
|
56
|
-
if logger:
|
|
57
|
-
self._logger = logger
|
|
58
|
-
return self._logger
|
|
59
|
-
else:
|
|
60
|
-
self._logger = Logger(__name__)
|
|
61
|
-
|
|
62
|
-
if self._logger.hasHandlers():
|
|
63
|
-
return self._logger
|
|
64
|
-
if not kwargs.get('use_default_logger', True):
|
|
65
|
-
print("not using default logger")
|
|
66
|
-
return self._logger
|
|
67
|
-
return self._initialize_default_logger()
|
|
68
|
-
|
|
69
|
-
def _initialize_default_logger(self, **kwargs):
|
|
70
|
-
def init_handlers():
|
|
71
|
-
sh = StreamHandler()
|
|
72
|
-
sh.set_name('StreamHandler')
|
|
73
|
-
fh = FileHandler(kwargs.get('log_file_path', join('./', 'PyEmailer.log')))
|
|
74
|
-
fh.set_name('FileHandler')
|
|
75
|
-
return sh, fh
|
|
76
|
-
|
|
77
|
-
def set_handler_levels(**kw):
|
|
78
|
-
fh_level = kw.get('FileHandler_level', 'DEBUG')
|
|
79
|
-
sh_level = kw.get('StreamHandler_level', 'INFO')
|
|
80
|
-
for h in self._logger.handlers:
|
|
81
|
-
if isinstance(h, FileHandler):
|
|
82
|
-
h.setLevel(fh_level)
|
|
83
|
-
elif isinstance(h, StreamHandler):
|
|
84
|
-
h.setLevel(sh_level)
|
|
85
|
-
else:
|
|
86
|
-
h.setLevel(kw.get('handler_default_level', 'DEBUG'))
|
|
87
|
-
|
|
88
|
-
stream_handle, file_handle = init_handlers()
|
|
89
|
-
|
|
90
|
-
if kwargs.get('log_to_stdout', True):
|
|
91
|
-
self._logger.addHandler(stream_handle)
|
|
92
|
-
self._logger.addHandler(file_handle)
|
|
93
|
-
set_handler_levels(**kwargs)
|
|
94
|
-
|
|
95
|
-
basicConfig(level='INFO', handlers=self._logger.handlers)
|
|
96
|
-
self._logger.info("basic logger initialized.")
|
|
97
|
-
return self._logger
|
|
98
|
-
|
|
99
57
|
def initialize_new_email(self):
|
|
100
58
|
if hasattr(self, 'email_app') and self.email_app is not None:
|
|
101
|
-
self.email = Msg(self.email_app.CreateItem(0), logger=self.
|
|
59
|
+
self.email = Msg(self.email_app.CreateItem(0), logger=self.logger)
|
|
102
60
|
return self.email
|
|
103
61
|
raise AttributeError("email_app is not defined. Run 'initialize_email_item_app_and_namespace' first")
|
|
104
62
|
|
|
@@ -107,17 +65,17 @@ class EmailerInitializer:
|
|
|
107
65
|
email_app, namespace = self._setup_email_app_and_namespace()
|
|
108
66
|
email = self.initialize_new_email()
|
|
109
67
|
except com_error as e:
|
|
110
|
-
self.
|
|
68
|
+
self.logger.error(e, exc_info=True)
|
|
111
69
|
raise e
|
|
112
70
|
return email_app, namespace, email
|
|
113
71
|
|
|
114
72
|
def _setup_email_app_and_namespace(self):
|
|
115
73
|
self.email_app = win32.Dispatch(self.email_app_name)
|
|
116
74
|
|
|
117
|
-
self.
|
|
75
|
+
self.logger.debug(f"{self.email_app_name} app in use.")
|
|
118
76
|
self.namespace = self.email_app.GetNamespace(self.namespace_name)
|
|
119
77
|
|
|
120
|
-
self.
|
|
78
|
+
self.logger.debug(f"{self.namespace_name} namespace in use.")
|
|
121
79
|
return self.email_app, self.namespace
|
|
122
80
|
|
|
123
81
|
|
|
@@ -160,7 +118,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
160
118
|
if validate_email(value, check_deliverability=False):
|
|
161
119
|
self._current_user_email = value
|
|
162
120
|
except EmailNotValidError as e:
|
|
163
|
-
self.
|
|
121
|
+
self.logger.error(e, exc_info=True)
|
|
164
122
|
value = None
|
|
165
123
|
self._current_user_email = value
|
|
166
124
|
|
|
@@ -178,7 +136,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
178
136
|
try:
|
|
179
137
|
raise NotADirectoryError(f"{self.signature_dir_path} does not exist.")
|
|
180
138
|
except NotADirectoryError as e:
|
|
181
|
-
self.
|
|
139
|
+
self.logger.warning(e)
|
|
182
140
|
self._email_signature = None
|
|
183
141
|
|
|
184
142
|
if isfile(signature_full_path):
|
|
@@ -188,10 +146,11 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
188
146
|
try:
|
|
189
147
|
raise FileNotFoundError(f"{signature_full_path} does not exist.")
|
|
190
148
|
except FileNotFoundError as e:
|
|
191
|
-
self.
|
|
149
|
+
self.logger.warning(e)
|
|
192
150
|
self._email_signature = None
|
|
193
151
|
else:
|
|
194
152
|
self._email_signature = None
|
|
153
|
+
self.logger.info("email_sig_filename not specified, no email signature will be attached.")
|
|
195
154
|
|
|
196
155
|
return self._email_signature
|
|
197
156
|
|
|
@@ -212,12 +171,12 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
212
171
|
except Exception as e:
|
|
213
172
|
# TODO: slated for removal
|
|
214
173
|
# this is here purely as a compatibility thing, to be taken out later.
|
|
215
|
-
self.
|
|
216
|
-
self.
|
|
174
|
+
self.logger.warning(e)
|
|
175
|
+
self.logger.warning("Defaulting to basic y/n prompt.")
|
|
217
176
|
while True:
|
|
218
177
|
q = input(f"{self.DisplayEmailSendTrackingWarning}. Do you understand? (y/n): ").lower().strip()
|
|
219
178
|
if q == 'y':
|
|
220
|
-
self.
|
|
179
|
+
self.logger.warning(self.DisplayEmailSendTrackingWarning)
|
|
221
180
|
return True
|
|
222
181
|
elif q == 'n':
|
|
223
182
|
return False
|
|
@@ -233,10 +192,10 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
233
192
|
try:
|
|
234
193
|
raise DisplayManualQuit("User cancelled operation due to DisplayTrackingWarning.")
|
|
235
194
|
except DisplayManualQuit as e:
|
|
236
|
-
self.
|
|
195
|
+
self.logger.error(e, exc_info=True)
|
|
237
196
|
raise e
|
|
238
197
|
|
|
239
|
-
def
|
|
198
|
+
def _get_default_folder_for_email_dir(self, email_dir_index: int = None, **kwargs):
|
|
240
199
|
# 6 = inbox
|
|
241
200
|
if email_dir_index in self.__class__.VALID_EMAIL_FOLDER_CHOICES:
|
|
242
201
|
self.read_folder = self.namespace.GetDefaultFolder(email_dir_index)
|
|
@@ -245,9 +204,30 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
245
204
|
try:
|
|
246
205
|
raise ValueError(f"email_dir_index must be one of {self.__class__.VALID_EMAIL_FOLDER_CHOICES}")
|
|
247
206
|
except ValueError as e:
|
|
248
|
-
self.
|
|
207
|
+
self.logger.error(e, exc_info=True)
|
|
249
208
|
raise e
|
|
250
209
|
|
|
210
|
+
def _GetReadFolder(self, email_dir_index: int = None, **kwargs):
|
|
211
|
+
"""
|
|
212
|
+
:param email_dir_index: Specifies the email directory index to be accessed. Defaults to None.
|
|
213
|
+
:type email_dir_index: int, optional
|
|
214
|
+
:param kwargs: Additional optional arguments that may be passed. Can include `subfolder_name` to specify a subfolder name, defaulting to 'Inbox'.
|
|
215
|
+
:type kwargs: dict
|
|
216
|
+
:return: The folder specified either by the email directory index or the default folder along with the subfolder if applicable.
|
|
217
|
+
:rtype: object
|
|
218
|
+
"""
|
|
219
|
+
subfolder_name = kwargs.get('subfolder_name', 'Inbox')
|
|
220
|
+
if not email_dir_index:
|
|
221
|
+
email_dir_index = BasicEmailFolderChoices.INBOX
|
|
222
|
+
self.logger.debug(f">>> email_dir_index not specified, defaulting to '{email_dir_index}' folder. <<<")
|
|
223
|
+
if not isinstance(email_dir_index, int):
|
|
224
|
+
self.logger.debug(f">>> email_dir_index is not an int, "
|
|
225
|
+
f"defaulting to {email_dir_index} folder and {subfolder_name} subfolder. <<<")
|
|
226
|
+
return self.namespace.Folders[email_dir_index].Folders[subfolder_name]
|
|
227
|
+
|
|
228
|
+
else:
|
|
229
|
+
return self._get_default_folder_for_email_dir(email_dir_index)
|
|
230
|
+
|
|
251
231
|
def GetMessages(self, folder_index=None):
|
|
252
232
|
if isinstance(folder_index, int):
|
|
253
233
|
self.read_folder = self._GetReadFolder(folder_index)
|
|
@@ -259,9 +239,9 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
259
239
|
try:
|
|
260
240
|
raise TypeError("folder_index must be an integer or self.read_folder must be defined")
|
|
261
241
|
except TypeError as e:
|
|
262
|
-
self.
|
|
242
|
+
self.logger.error(e, exc_info=True)
|
|
263
243
|
raise e
|
|
264
|
-
return [Msg(m, logger=self.
|
|
244
|
+
return [Msg(m, logger=self.logger) for m in self.read_folder.Items]
|
|
265
245
|
|
|
266
246
|
@deprecated("use Msg classes body attribute instead")
|
|
267
247
|
def GetEmailMessageBody(self, msg):
|
|
@@ -273,7 +253,7 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
273
253
|
try:
|
|
274
254
|
raise ValueError("This message has no body.")
|
|
275
255
|
except ValueError as e:
|
|
276
|
-
self.
|
|
256
|
+
self.logger.error(e, exc_info=True)
|
|
277
257
|
raise e
|
|
278
258
|
|
|
279
259
|
@deprecated("use find_messages_by_subject instead")
|
|
@@ -289,15 +269,15 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
289
269
|
full_save_path = join(save_dir_path, str(attachment))
|
|
290
270
|
try:
|
|
291
271
|
attachment.SaveAsFile(full_save_path)
|
|
292
|
-
self.
|
|
272
|
+
self.logger.debug(f"{full_save_path} saved from email with subject {msg.subject}")
|
|
293
273
|
except Exception as e:
|
|
294
|
-
self.
|
|
274
|
+
self.logger.error(e, exc_info=True)
|
|
295
275
|
raise e
|
|
296
276
|
|
|
297
277
|
def SetupEmail(self, recipient: str, subject: str, text: str, attachments: list = None, **kwargs):
|
|
298
278
|
self.email = self.email.SetupMsg(sender=self.current_user_email, email_item=self.email(),
|
|
299
279
|
recipient=recipient, subject=subject, body=text, attachments=attachments,
|
|
300
|
-
logger=self.
|
|
280
|
+
logger=self.logger, **kwargs)
|
|
301
281
|
self._setup_was_run = True
|
|
302
282
|
return self.email
|
|
303
283
|
|
|
@@ -308,34 +288,34 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
308
288
|
self.email.send()
|
|
309
289
|
return
|
|
310
290
|
elif not send:
|
|
311
|
-
self.
|
|
291
|
+
self.logger.info(f"Mail not sent to {self.email.to}")
|
|
312
292
|
print(f"Mail not sent to {self.email.to}")
|
|
313
293
|
q = questionary.confirm("do you want to quit early?", default=False).ask()
|
|
314
294
|
if q:
|
|
315
295
|
print("ok quitting!")
|
|
316
|
-
self.
|
|
296
|
+
self.logger.warning("Quitting early due to user input.")
|
|
317
297
|
exit(-1)
|
|
318
298
|
else:
|
|
319
299
|
return
|
|
320
300
|
except com_error as e:
|
|
321
|
-
self.
|
|
301
|
+
self.logger.error(e, exc_info=True)
|
|
322
302
|
except NoConsoleScreenBufferError as e:
|
|
323
303
|
# TODO: slated for removal
|
|
324
304
|
# this is here purely as a compatibility thing, to be taken out later.
|
|
325
|
-
self.
|
|
326
|
-
self.
|
|
305
|
+
self.logger.warning(e)
|
|
306
|
+
self.logger.warning("defaulting to basic input style...")
|
|
327
307
|
while True:
|
|
328
308
|
yn = input("Send Mail? (y/n/q): ").lower()
|
|
329
309
|
if yn == 'y':
|
|
330
310
|
self.email.send()
|
|
331
311
|
break
|
|
332
312
|
elif yn == 'n':
|
|
333
|
-
self.
|
|
313
|
+
self.logger.info(f"Mail not sent to {self.email.to}")
|
|
334
314
|
print(f"Mail not sent to {self.email.to}")
|
|
335
315
|
break
|
|
336
316
|
elif yn == 'q':
|
|
337
317
|
print("ok quitting!")
|
|
338
|
-
self.
|
|
318
|
+
self.logger.warning("Quitting early due to user input.")
|
|
339
319
|
exit(-1)
|
|
340
320
|
else:
|
|
341
321
|
print("Please choose \'y\', \'n\' or \'q\'")
|
|
@@ -344,18 +324,18 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
344
324
|
if self._setup_was_run:
|
|
345
325
|
if print_ready_msg:
|
|
346
326
|
print(f"Ready to send/display mail to/for {self.email.to}...")
|
|
347
|
-
self.
|
|
327
|
+
self.logger.info(f"Ready to send/display mail to/for {self.email.to}...")
|
|
348
328
|
if self.send_emails and self.display_window:
|
|
349
329
|
send_and_display_warning = ("Sending email while also displaying the email "
|
|
350
330
|
"in the app is not possible. Defaulting to Display only")
|
|
351
331
|
# print(send_and_display_warning)
|
|
352
|
-
self.
|
|
332
|
+
self.logger.warning(send_and_display_warning)
|
|
353
333
|
self.send_emails = False
|
|
354
334
|
self.display_window = True
|
|
355
335
|
|
|
356
336
|
if self.send_emails:
|
|
357
337
|
if self.auto_send:
|
|
358
|
-
self.
|
|
338
|
+
self.logger.info("Sending emails with auto_send...")
|
|
359
339
|
self.email.send()
|
|
360
340
|
|
|
361
341
|
else:
|
|
@@ -366,13 +346,13 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
366
346
|
else:
|
|
367
347
|
both_disabled_warning = ("Both sending and displaying the email are disabled. "
|
|
368
348
|
"No errors were encountered.")
|
|
369
|
-
self.
|
|
349
|
+
self.logger.warning(both_disabled_warning)
|
|
370
350
|
# print(both_disabled_warning)
|
|
371
351
|
else:
|
|
372
352
|
try:
|
|
373
353
|
raise EmailerNotSetupError("Setup has not been run, sending or displaying an email cannot occur.")
|
|
374
354
|
except EmailerNotSetupError as e:
|
|
375
|
-
self.
|
|
355
|
+
self.logger.error(e, exc_info=True)
|
|
376
356
|
raise e
|
|
377
357
|
|
|
378
358
|
@staticmethod
|
|
@@ -389,8 +369,8 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
389
369
|
|
|
390
370
|
if msg_candidates:
|
|
391
371
|
msg_candidates = [FailedMsg(m) for m in msg_candidates]
|
|
392
|
-
self.
|
|
393
|
-
self.
|
|
372
|
+
self.logger.info(f"{len(msg_candidates)} 'failed send' candidates found.")
|
|
373
|
+
self.logger.info("mutating msg_candidates (Msg instances) into FailedMsg instances.")
|
|
394
374
|
|
|
395
375
|
for m in msg_candidates:
|
|
396
376
|
failed_info = m.process_failed_msg(m(), recent_days_cap=recent_days_cap)
|
|
@@ -402,10 +382,10 @@ class PyEmailer(EmailerInitializer, SubjectSearcher):
|
|
|
402
382
|
'err_info': failed_info})
|
|
403
383
|
results_string = self.__class__.FAILED_SEND_LOGGER_STRING.format(num=len(failed_sends),
|
|
404
384
|
recent_days_cap=recent_days_cap)
|
|
405
|
-
if (not self.
|
|
406
|
-
for x in self.
|
|
385
|
+
if (not self.logger.hasHandlers() or not any([isinstance(x, StreamHandler)
|
|
386
|
+
for x in self.logger.handlers])):
|
|
407
387
|
print(results_string)
|
|
408
|
-
self.
|
|
388
|
+
self.logger.info(results_string)
|
|
409
389
|
return failed_sends
|
|
410
390
|
|
|
411
391
|
|
|
@@ -428,7 +408,7 @@ def __setup_and_send_test(emailer):
|
|
|
428
408
|
if __name__ == "__main__":
|
|
429
409
|
module_name = __file__.split('\\')[-1].split('.py')[0]
|
|
430
410
|
|
|
431
|
-
em = PyEmailer(display_window=False, send_emails=True, auto_send=False, use_default_logger=
|
|
411
|
+
em = PyEmailer(display_window=False, send_emails=True, auto_send=False, use_default_logger=False)
|
|
432
412
|
m = em.find_messages_by_subject('Andrew', partial_match_ok=True)
|
|
433
413
|
print([type(x) for x in m])
|
|
434
414
|
# __setup_and_send_test(em)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyEmailerAJM
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8
|
|
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.8.tar.gz
|
|
7
7
|
Author: Amcsparron
|
|
8
8
|
Author-email: amcsparron@albanyny.gov
|
|
9
9
|
License: MIT License
|
|
@@ -13,6 +13,8 @@ Requires-Dist: pywin32
|
|
|
13
13
|
Requires-Dist: extract_msg
|
|
14
14
|
Requires-Dist: email_validator
|
|
15
15
|
Requires-Dist: questionary
|
|
16
|
+
Requires-Dist: EasyLoggerAJM
|
|
17
|
+
Requires-Dist: ColorizerAJM
|
|
16
18
|
Dynamic: author
|
|
17
19
|
Dynamic: author-email
|
|
18
20
|
Dynamic: download-url
|
|
@@ -4,11 +4,12 @@ setup.cfg
|
|
|
4
4
|
setup.py
|
|
5
5
|
PyEmailerAJM/__init__.py
|
|
6
6
|
PyEmailerAJM/_version.py
|
|
7
|
-
PyEmailerAJM/helpers.py
|
|
8
7
|
PyEmailerAJM/py_emailer_ajm.py
|
|
9
8
|
PyEmailerAJM.egg-info/PKG-INFO
|
|
10
9
|
PyEmailerAJM.egg-info/SOURCES.txt
|
|
11
10
|
PyEmailerAJM.egg-info/dependency_links.txt
|
|
12
11
|
PyEmailerAJM.egg-info/requires.txt
|
|
13
12
|
PyEmailerAJM.egg-info/top_level.txt
|
|
14
|
-
tests/test_PyEmailerAJM.py
|
|
13
|
+
tests/test_PyEmailerAJM.py
|
|
14
|
+
tests/test_logger.py
|
|
15
|
+
tests/test_snooze_tracking.py
|
|
@@ -17,7 +17,7 @@ setup(
|
|
|
17
17
|
url='https://github.com/amcsparron2793-Water/PyEmailer',
|
|
18
18
|
download_url=f'https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/{get_property("__version__", project_name)}.tar.gz',
|
|
19
19
|
keywords=["Outlook", "Email", "Automation"],
|
|
20
|
-
install_requires=['pywin32', 'extract_msg', 'email_validator', 'questionary'],
|
|
20
|
+
install_requires=['pywin32', 'extract_msg', 'email_validator', 'questionary','EasyLoggerAJM', 'ColorizerAJM'],
|
|
21
21
|
license='MIT License',
|
|
22
22
|
author='Amcsparron',
|
|
23
23
|
author_email='amcsparron@albanyny.gov',
|
|
@@ -7,8 +7,10 @@ from PyEmailerAJM import PyEmailer, Msg
|
|
|
7
7
|
|
|
8
8
|
class TestPyEmailer(unittest.TestCase):
|
|
9
9
|
TEST_ATTACHMENT_NAMES = ['attachment1', 'attachment2']
|
|
10
|
+
TEST_ADMIN_EMAIL = ['example@example.com']
|
|
10
11
|
|
|
11
12
|
def setUp(self):
|
|
13
|
+
PyEmailer.ADMIN_EMAIL = TestPyEmailer.TEST_ADMIN_EMAIL
|
|
12
14
|
self.emailer = PyEmailer(False, False)
|
|
13
15
|
|
|
14
16
|
@classmethod
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from PyEmailerAJM.backend.logger import PyEmailerLogger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# FIXME: some of these tests are flakey
|
|
9
|
+
class TestPyEmailerLogger(unittest.TestCase):
|
|
10
|
+
def setUp(self) -> None:
|
|
11
|
+
self.logger = PyEmailerLogger()
|
|
12
|
+
|
|
13
|
+
def test_call(self):
|
|
14
|
+
self.assertIs(self.logger(), self.logger.logger)
|
|
15
|
+
|
|
16
|
+
@patch('PyEmailerAJM.backend.logger.DupeDebugFilter')
|
|
17
|
+
def test_add_dupe_debug_to_handler(self, mock_dupe_debug_filter):
|
|
18
|
+
handler = MagicMock()
|
|
19
|
+
|
|
20
|
+
# FIX: Ensure handler.level is an integer
|
|
21
|
+
handler.level = logging.WARNING # Set a valid logging level
|
|
22
|
+
|
|
23
|
+
self.logger._add_dupe_debug_to_handler(handler)
|
|
24
|
+
mock_dupe_debug_filter.assert_called_once()
|
|
25
|
+
handler.addFilter.assert_called_once_with(mock_dupe_debug_filter.return_value)
|
|
26
|
+
|
|
27
|
+
@unittest.skip("Skipping this test because it's under development")
|
|
28
|
+
def test_set_logger_class(self):
|
|
29
|
+
result = self.logger._set_logger_class()
|
|
30
|
+
self.assertIs(result, self.logger)
|
|
31
|
+
|
|
32
|
+
@patch('PyEmailerAJM.backend.logger.Logger')
|
|
33
|
+
def test_initialize_logger(self, mock_logger):
|
|
34
|
+
self.logger.initialize_logger(logger=mock_logger)
|
|
35
|
+
self.assertFalse(mock_logger.propagate)
|
|
36
|
+
|
|
37
|
+
@patch('PyEmailerAJM.backend.logger.OutlookEmailHandler')
|
|
38
|
+
def test_setup_email_handler(self, mock_email_handler):
|
|
39
|
+
mock_email_handler_instance = mock_email_handler.return_value
|
|
40
|
+
mock_email_handler_instance.level = logging.ERROR
|
|
41
|
+
|
|
42
|
+
with patch.object(self.logger.logger, 'addHandler', autospec=True) as mock_add_handler:
|
|
43
|
+
self.logger.setup_email_handler(email_msg='Test email', logger_admins=['admin1@gmail.com'])
|
|
44
|
+
|
|
45
|
+
self.assertEqual(mock_email_handler_instance.level, logging.ERROR)
|
|
46
|
+
mock_email_handler.assert_called_once_with(
|
|
47
|
+
email_msg='Test email',
|
|
48
|
+
project_name=self.logger.project_name,
|
|
49
|
+
logger_dir_path=self.logger.log_location,
|
|
50
|
+
recipient=['admin1@gmail.com']
|
|
51
|
+
)
|
|
52
|
+
mock_email_handler_instance.setLevel.assert_called_once_with(logging.ERROR)
|
|
53
|
+
mock_email_handler_instance.setFormatter.assert_called_once_with(
|
|
54
|
+
self.logger.formatter)
|
|
55
|
+
mock_add_handler.assert_called_once_with(mock_email_handler_instance)
|
|
56
|
+
|
|
57
|
+
@patch('PyEmailerAJM.backend.logger.FileHandler')
|
|
58
|
+
def test_add_filter_to_file_handler(self, mock_file_handler):
|
|
59
|
+
mock_file_handler.level = logging.INFO # FIX: Set a valid level
|
|
60
|
+
self.logger._add_filter_to_file_handler(mock_file_handler)
|
|
61
|
+
mock_file_handler.addFilter.assert_called_once()
|
|
62
|
+
|
|
63
|
+
@patch('PyEmailerAJM.backend.logger.StreamHandler')
|
|
64
|
+
def test_add_filter_to_stream_handler(self, mock_stream_handler):
|
|
65
|
+
mock_stream_handler.level = logging.DEBUG # FIX: Set a valid level
|
|
66
|
+
self.logger._add_filter_to_stream_handler(mock_stream_handler)
|
|
67
|
+
mock_stream_handler.addFilter.assert_called_once()
|
|
68
|
+
|
|
69
|
+
def test_project_name_getter(self):
|
|
70
|
+
self.assertEqual(self.logger.project_name, self.logger.project_name)
|
|
71
|
+
|
|
72
|
+
def test_project_name_setter(self):
|
|
73
|
+
self.logger.project_name = 'NewProject'
|
|
74
|
+
self.assertEqual(self.logger.project_name, 'NewProject')
|
|
75
|
+
|
|
76
|
+
@patch('PyEmailerAJM.backend.logger.StreamHandlerIgnoreExecInfo')
|
|
77
|
+
def test_create_stream_handler(self, mock_stream_handler):
|
|
78
|
+
self.logger.create_stream_handler(log_level_to_stream=logging.WARNING)
|
|
79
|
+
mock_stream_handler.assert_called_once()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == '__main__':
|
|
83
|
+
unittest.main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import unittest
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch, Mock
|
|
6
|
+
|
|
7
|
+
from PyEmailerAJM.continuous_monitor.backend import SnoozeTracking
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestSnoozeTracking(unittest.TestCase):
|
|
11
|
+
TEST_JSON_PATH = Path("./test.json")
|
|
12
|
+
|
|
13
|
+
def setUp(self):
|
|
14
|
+
self.file_path = self.__class__.TEST_JSON_PATH
|
|
15
|
+
|
|
16
|
+
self.logger = logging.getLogger('test_logger')
|
|
17
|
+
self.subject = "Test Email Subject"
|
|
18
|
+
self.snooze_time = datetime.now() + timedelta(days=2)
|
|
19
|
+
self.email_entries = {
|
|
20
|
+
self.subject: self.snooze_time.isoformat()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
self.snooze_tracking = SnoozeTracking(self.file_path, logger=self.logger)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def tearDownClass(cls):
|
|
27
|
+
cls.TEST_JSON_PATH.unlink()
|
|
28
|
+
|
|
29
|
+
@unittest.skip("this test only works if the file "
|
|
30
|
+
"is NOT deleted by tearDownClass, "
|
|
31
|
+
"then the test is run again")
|
|
32
|
+
@patch('json.load')
|
|
33
|
+
@patch('builtins.open')
|
|
34
|
+
def test_json_loaded(self, mock_open, mock_json_load):
|
|
35
|
+
mock_json_load.return_value = self.email_entries
|
|
36
|
+
loaded_json = self.snooze_tracking.json_loaded
|
|
37
|
+
mock_open.assert_called_once_with(self.file_path, 'r')
|
|
38
|
+
self.assertEqual(loaded_json, self.email_entries, 'Should load JSON data from file')
|
|
39
|
+
|
|
40
|
+
@patch('json.load')
|
|
41
|
+
@patch('builtins.open')
|
|
42
|
+
def test_write_entry(self, mock_open, mock_json_load):
|
|
43
|
+
mock_json_load.return_value = {}
|
|
44
|
+
new_snooze_time = datetime.now() + timedelta(days=3)
|
|
45
|
+
self.snooze_tracking.write_entry(self.subject, new_snooze_time)
|
|
46
|
+
self.assertEqual(self.snooze_tracking.json_loaded[self.subject], new_snooze_time,
|
|
47
|
+
'Should write entry to json_loaded')
|
|
48
|
+
|
|
49
|
+
def test_convert_datetime(self):
|
|
50
|
+
non_dt_val = "non_datetime_value"
|
|
51
|
+
dt_val = datetime.now()
|
|
52
|
+
self.assertEqual(self.snooze_tracking._convert_datetime(non_dt_val), non_dt_val,
|
|
53
|
+
'Non datetime value should remain unchanged')
|
|
54
|
+
self.assertEqual(self.snooze_tracking._convert_datetime(dt_val), dt_val.isoformat(),
|
|
55
|
+
'Datetime value should be iso formatted')
|
|
56
|
+
|
|
57
|
+
@patch('json.dump')
|
|
58
|
+
@patch('builtins.open')
|
|
59
|
+
def test_save_json(self, mock_open, mock_json_dump):
|
|
60
|
+
with patch.object(self.logger, 'info') as mock_info: # Mock the logger.info method
|
|
61
|
+
self.snooze_tracking._json_loaded = self.email_entries
|
|
62
|
+
self.snooze_tracking.save_json()
|
|
63
|
+
|
|
64
|
+
# Use the mocked `info` method with assert_called.
|
|
65
|
+
mock_info.assert_called_with(f"json saved to {self.file_path}")
|
|
66
|
+
|
|
67
|
+
mock_open.assert_called_once_with(self.file_path, 'w')
|
|
68
|
+
mock_json_dump.assert_called_once()
|
|
69
|
+
|
|
70
|
+
def test_read_entry(self):
|
|
71
|
+
self.snooze_tracking._json_loaded = self.email_entries
|
|
72
|
+
output_snooze_time = self.snooze_tracking.read_entry(self.subject)
|
|
73
|
+
self.assertEqual(output_snooze_time, self.snooze_time, 'Should correctly read snooze time from json_loaded')
|
|
74
|
+
|
|
75
|
+
def test_snooze_msgs(self):
|
|
76
|
+
mock_msg_1 = Mock()
|
|
77
|
+
mock_msg_1.subject = "msg_1"
|
|
78
|
+
mock_msg_1.msg_snoozed = False
|
|
79
|
+
mock_msg_1.msg_snoozed_time = datetime.now() + timedelta(days=3)
|
|
80
|
+
|
|
81
|
+
mock_msg_2 = Mock()
|
|
82
|
+
mock_msg_2.subject = "msg_2"
|
|
83
|
+
mock_msg_2.msg_snoozed = True
|
|
84
|
+
mock_msg_2.msg_snoozed_time = datetime.now() + timedelta(days=1)
|
|
85
|
+
|
|
86
|
+
msg_list = [mock_msg_1, mock_msg_2]
|
|
87
|
+
output_msg_list = self.snooze_tracking.snooze_msgs(msg_list)
|
|
88
|
+
self.assertEqual(output_msg_list, msg_list, 'Should return the same list')
|
|
89
|
+
self.assertTrue(output_msg_list[0].msg_snoozed, 'Non-snoozed message should be marked as snoozed')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
unittest.main()
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
from PyEmailerAJM.backend import deprecated
|
|
2
|
-
from PyEmailerAJM.backend.errs import EmailerNotSetupError, DisplayManualQuit
|
|
3
|
-
from PyEmailerAJM.msg import Msg, FailedMsg
|
|
4
|
-
from PyEmailerAJM.searchers import BaseSearcher, SubjectSearcher
|
|
5
|
-
from PyEmailerAJM.py_emailer_ajm import PyEmailer, EmailerInitializer
|
|
6
|
-
|
|
7
|
-
__all__ = ['EmailerNotSetupError', 'DisplayManualQuit', 'deprecated',
|
|
8
|
-
'Msg', 'FailedMsg', 'PyEmailer', 'EmailerInitializer']
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '1.7'
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import warnings
|
|
2
|
-
import functools
|
|
3
|
-
from enum import IntEnum
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class BasicEmailFolderChoices(IntEnum):
|
|
7
|
-
INBOX = 6
|
|
8
|
-
SENT_ITEMS = 5
|
|
9
|
-
DRAFTS = 16
|
|
10
|
-
DELETED_ITEMS = 3
|
|
11
|
-
OUTBOX = 4
|
|
12
|
-
|
|
13
|
-
def __str__(self):
|
|
14
|
-
"""Return the enum name as a string."""
|
|
15
|
-
return self.name
|
|
16
|
-
|
|
17
|
-
def __repr__(self):
|
|
18
|
-
return f"<{self.__class__.__name__}.{self.name} ({self.value})>"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def deprecated(reason: str = ""):
|
|
22
|
-
"""
|
|
23
|
-
Decorator that marks a function or method as deprecated.
|
|
24
|
-
|
|
25
|
-
:param reason: Optional message to explain what to use instead
|
|
26
|
-
or when the feature will be removed.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def decorator(func):
|
|
30
|
-
@functools.wraps(func)
|
|
31
|
-
def wrapper(*args, **kwargs):
|
|
32
|
-
message = f"Function '{func.__name__}' is deprecated."
|
|
33
|
-
if reason:
|
|
34
|
-
message += f" {reason}"
|
|
35
|
-
warnings.warn(message, category=DeprecationWarning, stacklevel=2)
|
|
36
|
-
return func(*args, **kwargs)
|
|
37
|
-
|
|
38
|
-
return wrapper
|
|
39
|
-
|
|
40
|
-
return decorator
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|