PyEmailerAJM 1.6__py3-none-any.whl

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.
@@ -0,0 +1,8 @@
1
+ from PyEmailerAJM.errs import EmailerNotSetupError, DisplayManualQuit
2
+ from PyEmailerAJM.helpers import deprecated, BasicEmailFolderChoices
3
+ from PyEmailerAJM.msg import Msg, FailedMsg
4
+ from PyEmailerAJM.py_emailer_ajm import PyEmailer, EmailerInitializer
5
+
6
+ __all__ = ['EmailerNotSetupError', 'DisplayManualQuit', 'deprecated',
7
+ 'BasicEmailFolderChoices', 'Msg', 'FailedMsg',
8
+ 'PyEmailer', 'EmailerInitializer']
@@ -0,0 +1 @@
1
+ __version__ = '1.6'
PyEmailerAJM/errs.py ADDED
@@ -0,0 +1,6 @@
1
+ class EmailerNotSetupError(Exception):
2
+ ...
3
+
4
+
5
+ class DisplayManualQuit(Exception):
6
+ ...
@@ -0,0 +1,40 @@
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
PyEmailerAJM/msg.py ADDED
@@ -0,0 +1,230 @@
1
+ from abc import abstractmethod
2
+ from os.path import isfile, isabs, abspath, join
3
+ from tempfile import gettempdir
4
+
5
+ import win32com.client as win32
6
+ import datetime
7
+ import extract_msg
8
+ from bs4 import BeautifulSoup
9
+ from logging import Logger, getLogger, warning
10
+
11
+
12
+ class _BasicMsgProperties:
13
+ def __init__(self, email_item: win32.CDispatch):
14
+ self.email_item = email_item
15
+
16
+ @classmethod
17
+ @abstractmethod
18
+ def _validate_and_add_attachments(cls, email_item: win32.CDispatch, attachment_list: list = None):
19
+ ...
20
+
21
+ @property
22
+ def sender(self):
23
+ if hasattr(self.email_item, 'SenderEmailType') and self.email_item.SenderEmailType == 'EX':
24
+ return self.email_item.Sender.GetExchangeUser().PrimarySmtpAddress
25
+ # return self.email_item.Sender if hasattr(self.email_item, 'Sender') else self.email_item.sender
26
+ else:
27
+ return self.email_item.SenderEmailAddress
28
+
29
+ @property
30
+ def sender_name(self):
31
+ return self.email_item.Sender if hasattr(self.email_item, 'Sender') else self.email_item.sender
32
+
33
+ @property
34
+ def to(self):
35
+ return self.email_item.To if hasattr(self.email_item, 'To') else self.email_item.to
36
+
37
+ def cc(self):
38
+ return self.email_item.CC if hasattr(self.email_item, 'CC') else self.email_item.cc
39
+
40
+ @property
41
+ def subject(self):
42
+ return self.email_item.Subject if hasattr(self.email_item, 'Subject') else self.email_item.subject
43
+
44
+ @property
45
+ def received_time(self):
46
+ #not_future = self.email_item.ReceivedTime.year < datetime.datetime.now().year
47
+ return self.email_item.ReceivedTime #if not_future else None
48
+
49
+ @property
50
+ def body(self):
51
+ return self.email_item.HTMLBody if hasattr(self.email_item, 'HTMLBody') else self.email_item.htmlBody
52
+
53
+ @property
54
+ def attachments(self):
55
+ return self.email_item.Attachments
56
+
57
+ @attachments.setter
58
+ def attachments(self, value: list):
59
+ self._validate_and_add_attachments(self.email_item, value)
60
+
61
+
62
+ class Msg(_BasicMsgProperties):
63
+ def __init__(self, email_item: win32.CDispatch or extract_msg.Message, **kwargs):
64
+ super().__init__(email_item)
65
+ self._logger: Logger = kwargs.get('logger', getLogger(__name__))
66
+ self.send_success = False
67
+
68
+ def __call__(self, *args, **kwargs):
69
+ return self.email_item
70
+
71
+ @classmethod
72
+ def SetupMsg(cls, sender, recipient, subject, body, email_item: win32.CDispatch, attachments: list = None, **kwargs):
73
+ email_item.To = recipient
74
+ email_item.Sender = sender
75
+ email_item.Subject = subject
76
+ email_item.HtmlBody = body
77
+ email_item.cc = kwargs.get('cc', '')
78
+ email_item.Bcc = kwargs.get('bcc', '')
79
+
80
+ cls._validate_and_add_attachments(email_item, attachments)
81
+ return cls(email_item, **kwargs)
82
+
83
+ @classmethod
84
+ def _validate_and_add_attachments(cls, email_item: win32.CDispatch, attachment_list: list = None):
85
+ """ Validate and attach files to the email_item. """
86
+ if not attachment_list:
87
+ warning("No attachments detected")
88
+ return
89
+
90
+ if not isinstance(attachment_list, list):
91
+ raise TypeError("Attachments must be provided as a list")
92
+
93
+ def _absolute_file_path(file_path):
94
+ """Returns absolute path if valid; raises FileNotFoundError otherwise."""
95
+ if not isabs(file_path):
96
+ file_path = abspath(file_path)
97
+ if not isfile(file_path):
98
+ raise FileNotFoundError(f"File {file_path} could not be attached.")
99
+ return file_path
100
+
101
+ for attachment in attachment_list:
102
+ email_item.attachments.Add(_absolute_file_path(attachment))
103
+
104
+ def SaveAllEmailAttachments(self, save_dir_path):
105
+ all_attachment_paths = set()
106
+ for attachment in self.attachments:
107
+ full_save_path = join(save_dir_path, str(attachment))
108
+ try:
109
+ attachment.SaveAsFile(full_save_path)
110
+ all_attachment_paths.add(full_save_path)
111
+ self._logger.debug(f"{full_save_path} saved from email with subject {self.subject}")
112
+ except Exception as e:
113
+ self._logger.error(e, exc_info=True)
114
+ raise e
115
+ return all_attachment_paths
116
+
117
+ def display(self):
118
+ # print(f"Displaying the email in {self.email_app_name}, this window might open minimized.")
119
+ # self._logger.info(f"Displaying the email in {self.email_app_name}, this window might open minimized.")
120
+ try:
121
+ self().Display(True)
122
+ except Exception as e:
123
+ self._logger.error(e, exc_info=True)
124
+ raise e
125
+
126
+ def send(self):
127
+ try:
128
+ # if the send fails, self.to is NULL, so this needs to be saved in a local variable
129
+ attempted_recipient = self.to
130
+ self.send_success = False
131
+ self().Send()
132
+ # print(f"Mail sent to {self._recipient}")
133
+ self.send_success = True
134
+ self._logger.info(f"Mail successfully sent to {attempted_recipient}")
135
+ except Exception as e:
136
+ self._logger.error(e, exc_info=True)
137
+ raise e
138
+
139
+ def _ValidateResponseMsg(self):
140
+ if isinstance(self(), win32.CDispatch):
141
+ self._logger.debug("passed in msg is CDispatch instance")
142
+ if hasattr(self(), 'HtmlBody') or hasattr(self(), 'htmlBody'):
143
+ self._logger.debug("passed in msg has 'HtmlBody' or 'htmlBody' attr")
144
+
145
+ if (not isinstance(self(), win32.CDispatch)
146
+ or not hasattr(self(), ('HtmlBody' or 'htmlBody'))):
147
+ raise AttributeError("msg attr must have 'HtmlBody' attr AND be a CDispatch instance")
148
+ return self()
149
+
150
+ def _msg_is_recent(self, recent_days_cap=1):
151
+ if self.received_time is not None:
152
+ abs_diff = abs(self.received_time - datetime.datetime.now(tz=self.received_time.tzinfo))
153
+ return abs_diff <= datetime.timedelta(days=recent_days_cap)
154
+ print(f"msg with subject \'{self.email_item.Subject}\' has no received time. defaulting to false")
155
+ self._logger.debug(f"msg with subject \'{self.email_item.Subject}\' has no received time. defaulting to false")
156
+ return False
157
+
158
+ def return_as_failed_send(self):
159
+ return FailedMsg(self())
160
+
161
+
162
+ class FailedMsg(Msg):
163
+ ERR_SKIP_STRING = "err {}: skipping this message"
164
+ DEFAULT_TEMP_SAVE_PATH = gettempdir()
165
+
166
+ def _message_filter_checks(self, **kwargs) -> bool:
167
+ recent_days_cap = kwargs.get('recent_days_cap', 1)
168
+ return self._msg_is_recent(recent_days_cap)
169
+
170
+ def _fetch_failed_msg_details(self, **kwargs):
171
+ temp_attachment_save_path = kwargs.get('temp_attachment_save_path',
172
+ self.__class__.DEFAULT_TEMP_SAVE_PATH)
173
+ try:
174
+ attachment_msg_path = self.SaveAllEmailAttachments(temp_attachment_save_path)
175
+ print('saved_attachments')
176
+ except Exception as e:
177
+ self._logger.warning(self.__class__.ERR_SKIP_STRING.format(f'({e})'))
178
+ return e
179
+ if len(attachment_msg_path) == 1:
180
+ return next(iter(attachment_msg_path))
181
+ return attachment_msg_path
182
+
183
+ def process_failed_msg(self, post_master_msg, **kwargs):
184
+ recent_days_cap = kwargs.get('recent_days_cap', 1)
185
+ try:
186
+ self.email_item = post_master_msg
187
+ self._ValidateResponseMsg()
188
+ except AttributeError as e:
189
+ self._logger.warning(self.__class__.ERR_SKIP_STRING.format(f'({e})'))
190
+ return e, None, None
191
+
192
+ if self._msg_is_recent(recent_days_cap):
193
+ attachment_msg = self._fetch_failed_msg_details()
194
+ if isinstance(attachment_msg, Exception):
195
+ return attachment_msg, None, None
196
+ else:
197
+ if isinstance(attachment_msg, str):
198
+ fmd = _FailedMessageDetails.extract_msg_from_attachment(attachment_msg)
199
+ return fmd.process_failed_details_msg() #self._process_failed_details_msg(attachment_msg)
200
+ return None, None, None
201
+
202
+
203
+ class _FailedMessageDetails(FailedMsg):
204
+ @classmethod
205
+ def extract_msg_from_attachment(cls, parent_msg: str):
206
+ return cls(extract_msg.Message(parent_msg))
207
+
208
+ def _extract_from_failed_details_msg(self, para):
209
+ email_of_err = para.findNext('p').get_text().strip().split('(')[0].strip()
210
+ err_reason = para.findNext('p').findNext('p').get_text()
211
+ send_time = self().date.ctime()
212
+ failed_subject = self.subject
213
+
214
+ err_details = {'email_of_err': email_of_err, 'err_reason': err_reason,
215
+ 'send_time': send_time, 'failed_subject': failed_subject}
216
+ # print(f"Email of err: {email_of_err},\nErr reason: {err_reason}\nSend time: {send_time}")
217
+ return err_details #email_of_err, err_reason, send_time
218
+
219
+ def process_failed_details_msg(self, **kwargs):
220
+ detail_marker_string = kwargs.get('detail_marker_string',
221
+ "Delivery has failed to these recipients or groups:")
222
+
223
+ soup = BeautifulSoup(self.body, features="html.parser")
224
+
225
+ all_p = soup.find_all(name='p') # , attrs={'class': 'MsoNormal'})
226
+
227
+ for para in all_p:
228
+ if detail_marker_string in para.get_text():
229
+ return {** self._extract_from_failed_details_msg(para)}
230
+ return None, None, None
@@ -0,0 +1,518 @@
1
+ #! python3
2
+ """
3
+ py_emailer_ajm.py
4
+
5
+ install win32 with pip install pywin32
6
+ """
7
+ # imports
8
+ from abc import abstractmethod
9
+ from os import environ
10
+ from os.path import isfile, join, isdir
11
+ from tempfile import gettempdir
12
+ from typing import List
13
+
14
+ # install win32 with pip install pywin32
15
+ import win32com.client as win32
16
+
17
+ # This is installed as part of pywin32
18
+ # noinspection PyUnresolvedReferences
19
+ from pythoncom import com_error
20
+ from logging import Logger, basicConfig, StreamHandler, FileHandler
21
+ from email_validator import validate_email, EmailNotValidError
22
+ import questionary
23
+ # this is usually thrown when questionary is used in the dev/Non Win32 environment
24
+ # noinspection PyProtectedMember
25
+ from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
26
+ from win32com.client import CDispatch
27
+
28
+ from PyEmailerAJM import EmailerNotSetupError, DisplayManualQuit
29
+ from PyEmailerAJM import BasicEmailFolderChoices, deprecated
30
+ from PyEmailerAJM import Msg, FailedMsg
31
+
32
+
33
+ class EmailerInitializer:
34
+ DEFAULT_EMAIL_APP_NAME = 'outlook.application'
35
+ DEFAULT_NAMESPACE_NAME = 'MAPI'
36
+
37
+ def __init__(self, display_window: bool,
38
+ send_emails: bool, logger: Logger = None,
39
+ auto_send: bool = False,
40
+ email_app_name: str = DEFAULT_EMAIL_APP_NAME,
41
+ namespace_name: str = DEFAULT_NAMESPACE_NAME, **kwargs):
42
+
43
+ self._logger = self._initialize_logger(logger, use_default_logger=kwargs.get('use_default_logger', False))
44
+ # print("Dummy logger in use!")
45
+
46
+ self.email_app_name = email_app_name
47
+ self.namespace_name = namespace_name
48
+
49
+ self.email_app, self.namespace, self.email = self.initialize_email_item_app_and_namespace()
50
+
51
+ self.display_window = display_window
52
+ self.auto_send = auto_send
53
+ self.send_emails = send_emails
54
+
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
+ def initialize_new_email(self):
100
+ if hasattr(self, 'email_app') and self.email_app is not None:
101
+ self.email = Msg(self.email_app.CreateItem(0), logger=self._logger)
102
+ return self.email
103
+ raise AttributeError("email_app is not defined. Run 'initialize_email_item_app_and_namespace' first")
104
+
105
+ def initialize_email_item_app_and_namespace(self):
106
+ try:
107
+ email_app, namespace = self._setup_email_app_and_namespace()
108
+ email = self.initialize_new_email()
109
+ except com_error as e:
110
+ self._logger.error(e, exc_info=True)
111
+ raise e
112
+ return email_app, namespace, email
113
+
114
+ def _setup_email_app_and_namespace(self):
115
+ self.email_app = win32.Dispatch(self.email_app_name)
116
+
117
+ self._logger.debug(f"{self.email_app_name} app in use.")
118
+ self.namespace = self.email_app.GetNamespace(self.namespace_name)
119
+
120
+ self._logger.debug(f"{self.namespace_name} namespace in use.")
121
+ return self.email_app, self.namespace
122
+
123
+
124
+ class _SubjectSearcher:
125
+ # Constants for prefixes
126
+ FW_PREFIXES = ['FW:', 'FWD:']
127
+ RE_PREFIX = 'RE:'
128
+
129
+ @abstractmethod
130
+ def GetMessages(self):
131
+ ...
132
+
133
+ def find_messages_by_subject(self, search_subject: str, include_fw: bool = True, include_re: bool = True,
134
+ partial_match_ok: bool = False) -> List[CDispatch]:
135
+ """Returns a list of messages matching the given subject, ignoring prefixes based on flags."""
136
+
137
+ # Normalize search subject
138
+ normalized_subject = self._normalize_subject(search_subject)
139
+ matched_messages = []
140
+ print("partial match ok: ", partial_match_ok)
141
+
142
+ for message in self.GetMessages():
143
+ normalized_message_subject = self._normalize_subject(message.subject)
144
+
145
+ if (self._is_exact_match(normalized_message_subject, normalized_subject) or
146
+ (partial_match_ok and self._is_partial_match(normalized_message_subject,
147
+ normalized_subject))):
148
+ matched_messages.append(message)
149
+ continue
150
+
151
+ if include_fw and self._matches_prefix(normalized_message_subject,
152
+ self.__class__.FW_PREFIXES,
153
+ normalized_subject,
154
+ partial_match_ok):
155
+ matched_messages.append(message)
156
+ continue
157
+
158
+ if include_re and self._matches_prefix(normalized_message_subject,
159
+ [self.__class__.RE_PREFIX],
160
+ normalized_subject,
161
+ partial_match_ok):
162
+ matched_messages.append(message)
163
+
164
+ return [m() for m in matched_messages]
165
+
166
+ @staticmethod
167
+ def _normalize_subject(subject: str) -> str:
168
+ """Normalize the given subject by converting to lowercase and stripping whitespace."""
169
+ return subject.lower().strip()
170
+
171
+ def _matches_prefix(self, message_subject: str, prefixes: list, search_subject: str,
172
+ partial_match_ok: bool = False) -> bool:
173
+ """Checks if the message subject matches the search subject after removing a prefix."""
174
+ for prefix in prefixes:
175
+ if message_subject.startswith(prefix.lower()):
176
+ stripped_subject = message_subject.split(prefix.lower(), 1)[1].strip()
177
+ return (self._is_exact_match(stripped_subject, search_subject) if not partial_match_ok
178
+ else self._is_partial_match(stripped_subject, search_subject))
179
+ return False
180
+
181
+ @staticmethod
182
+ def _is_exact_match(message_subject: str, search_subject: str) -> bool:
183
+ """Checks if the subject matches exactly."""
184
+ return message_subject == search_subject
185
+
186
+ @staticmethod
187
+ def _is_partial_match(message_subject: str, search_subject: str) -> bool:
188
+ return search_subject in message_subject
189
+
190
+
191
+ class PyEmailer(EmailerInitializer, _SubjectSearcher):
192
+ # the email tab_char
193
+ tab_char = '&emsp;'
194
+ signature_dir_path = join((environ['USERPROFILE']),
195
+ 'AppData\\Roaming\\Microsoft\\Signatures\\')
196
+
197
+ DisplayEmailSendTrackingWarning = "THIS TYPE OF SEND CANNOT BE DETECTED FOR SEND SUCCESS AUTOMATICALLY."
198
+ FAILED_SEND_LOGGER_STRING = "{num} confirmed failed send(s) found in the last {recent_days_cap} day(s)."
199
+
200
+ DEFAULT_TEMP_SAVE_PATH = gettempdir()
201
+ VALID_EMAIL_FOLDER_CHOICES = [x for x in BasicEmailFolderChoices]
202
+
203
+ def __init__(self, display_window: bool, send_emails: bool, logger: Logger = None, email_sig_filename: str = None,
204
+ auto_send: bool = False, email_app_name: str = EmailerInitializer.DEFAULT_EMAIL_APP_NAME,
205
+ namespace_name: str = EmailerInitializer.DEFAULT_NAMESPACE_NAME, **kwargs):
206
+
207
+ super().__init__(display_window, send_emails, logger, auto_send, email_app_name, namespace_name, **kwargs)
208
+ self._setup_was_run = False
209
+ self._current_user_email = None
210
+
211
+ self.read_folder = None
212
+
213
+ self._email_signature = None
214
+ self._send_success = False
215
+ self.email_sig_filename = email_sig_filename
216
+
217
+ @property
218
+ def current_user_email(self):
219
+ if self.email_app_name.lower().startswith('outlook'):
220
+ self._current_user_email = (
221
+ self.namespace.Application.Session.CurrentUser.AddressEntry.GetExchangeUser().PrimarySmtpAddress)
222
+ return self._current_user_email
223
+
224
+ @current_user_email.setter
225
+ def current_user_email(self, value):
226
+ try:
227
+ if validate_email(value, check_deliverability=False):
228
+ self._current_user_email = value
229
+ except EmailNotValidError as e:
230
+ self._logger.error(e, exc_info=True)
231
+ value = None
232
+ self._current_user_email = value
233
+
234
+ @property
235
+ def email_signature(self):
236
+ return self._email_signature
237
+
238
+ @email_signature.getter
239
+ def email_signature(self):
240
+ if self.email_sig_filename:
241
+ signature_full_path = join(self.signature_dir_path, self.email_sig_filename)
242
+ if isdir(self.signature_dir_path):
243
+ pass
244
+ else:
245
+ try:
246
+ raise NotADirectoryError(f"{self.signature_dir_path} does not exist.")
247
+ except NotADirectoryError as e:
248
+ self._logger.warning(e)
249
+ self._email_signature = None
250
+
251
+ if isfile(signature_full_path):
252
+ with open(signature_full_path, 'r', encoding='utf-16') as f:
253
+ self._email_signature = f.read().strip()
254
+ else:
255
+ try:
256
+ raise FileNotFoundError(f"{signature_full_path} does not exist.")
257
+ except FileNotFoundError as e:
258
+ self._logger.warning(e)
259
+ self._email_signature = None
260
+ else:
261
+ self._email_signature = None
262
+
263
+ return self._email_signature
264
+
265
+ @property
266
+ def send_success(self):
267
+ return self._send_success
268
+
269
+ @send_success.setter
270
+ def send_success(self, value):
271
+ self._send_success = value
272
+
273
+ def _display_tracking_warning_confirm(self):
274
+ # noinspection PyBroadException
275
+ try:
276
+ q = questionary.confirm(f"{self.DisplayEmailSendTrackingWarning}. Do you understand?",
277
+ default=False, auto_enter=False).ask()
278
+ return q
279
+ except Exception as e:
280
+ # TODO: slated for removal
281
+ # this is here purely as a compatibility thing, to be taken out later.
282
+ self._logger.warning(e)
283
+ self._logger.warning("Defaulting to basic y/n prompt.")
284
+ while True:
285
+ q = input(f"{self.DisplayEmailSendTrackingWarning}. Do you understand? (y/n): ").lower().strip()
286
+ if q == 'y':
287
+ self._logger.warning(self.DisplayEmailSendTrackingWarning)
288
+ return True
289
+ elif q == 'n':
290
+ return False
291
+ else:
292
+ print("Please respond with 'y' or 'n'.")
293
+
294
+ def display_tracker_check(self) -> bool:
295
+ if self.display_window:
296
+ c = self._display_tracking_warning_confirm()
297
+ if c:
298
+ return c
299
+ else:
300
+ try:
301
+ raise DisplayManualQuit("User cancelled operation due to DisplayTrackingWarning.")
302
+ except DisplayManualQuit as e:
303
+ self._logger.error(e, exc_info=True)
304
+ raise e
305
+
306
+ def _GetReadFolder(self, email_dir_index: int = BasicEmailFolderChoices.INBOX):
307
+ # 6 = inbox
308
+ if email_dir_index in self.__class__.VALID_EMAIL_FOLDER_CHOICES:
309
+ self.read_folder = self.namespace.GetDefaultFolder(email_dir_index)
310
+ return self.read_folder
311
+ else:
312
+ try:
313
+ raise ValueError(f"email_dir_index must be one of {self.__class__.VALID_EMAIL_FOLDER_CHOICES}")
314
+ except ValueError as e:
315
+ self._logger.error(e, exc_info=True)
316
+ raise e
317
+
318
+ def GetMessages(self, folder_index=None):
319
+ if isinstance(folder_index, int):
320
+ self.read_folder = self._GetReadFolder(folder_index)
321
+ elif not folder_index and self.read_folder:
322
+ pass
323
+ elif not folder_index:
324
+ self.read_folder = self._GetReadFolder()
325
+ else:
326
+ try:
327
+ raise TypeError("folder_index must be an integer or self.read_folder must be defined")
328
+ except TypeError as e:
329
+ self._logger.error(e, exc_info=True)
330
+ raise e
331
+ return [Msg(m, logger=self._logger) for m in self.read_folder.Items]
332
+
333
+ @deprecated("use Msg classes body attribute instead")
334
+ def GetEmailMessageBody(self, msg):
335
+ """message = messages.GetLast()"""
336
+ body_content = msg.body
337
+ if body_content:
338
+ return body_content
339
+ else:
340
+ try:
341
+ raise ValueError("This message has no body.")
342
+ except ValueError as e:
343
+ self._logger.error(e, exc_info=True)
344
+ raise e
345
+
346
+ @deprecated("use find_messages_by_subject instead")
347
+ def FindMsgBySubject(self, subject: str, forwarded_message_match: bool = True,
348
+ reply_msg_match: bool = True, partial_match_ok: bool = False):
349
+ return self.find_messages_by_subject(subject, include_fw=forwarded_message_match,
350
+ include_re=reply_msg_match,
351
+ partial_match_ok=partial_match_ok)
352
+
353
+ def SaveAllEmailAttachments(self, msg, save_dir_path):
354
+ attachments = msg.Attachments
355
+ for attachment in attachments:
356
+ full_save_path = join(save_dir_path, str(attachment))
357
+ try:
358
+ attachment.SaveAsFile(full_save_path)
359
+ self._logger.debug(f"{full_save_path} saved from email with subject {msg.subject}")
360
+ except Exception as e:
361
+ self._logger.error(e, exc_info=True)
362
+ raise e
363
+
364
+ def SetupEmail(self, recipient: str, subject: str, text: str, attachments: list = None, **kwargs):
365
+ self.email = self.email.SetupMsg(sender=self.current_user_email, email_item=self.email(),
366
+ recipient=recipient, subject=subject, body=text, attachments=attachments,
367
+ logger=self._logger, **kwargs)
368
+ self._setup_was_run = True
369
+ return self.email
370
+
371
+ def _manual_send_loop(self):
372
+ try:
373
+ send = questionary.confirm("Send Mail?:", default=False).ask()
374
+ if send:
375
+ self.email.send()
376
+ return
377
+ elif not send:
378
+ self._logger.info(f"Mail not sent to {self.email.to}")
379
+ print(f"Mail not sent to {self.email.to}")
380
+ q = questionary.confirm("do you want to quit early?", default=False).ask()
381
+ if q:
382
+ print("ok quitting!")
383
+ self._logger.warning("Quitting early due to user input.")
384
+ exit(-1)
385
+ else:
386
+ return
387
+ except com_error as e:
388
+ self._logger.error(e, exc_info=True)
389
+ except NoConsoleScreenBufferError as e:
390
+ # TODO: slated for removal
391
+ # this is here purely as a compatibility thing, to be taken out later.
392
+ self._logger.warning(e)
393
+ self._logger.warning("defaulting to basic input style...")
394
+ while True:
395
+ yn = input("Send Mail? (y/n/q): ").lower()
396
+ if yn == 'y':
397
+ self.email.send()
398
+ break
399
+ elif yn == 'n':
400
+ self._logger.info(f"Mail not sent to {self.email.to}")
401
+ print(f"Mail not sent to {self.email.to}")
402
+ break
403
+ elif yn == 'q':
404
+ print("ok quitting!")
405
+ self._logger.warning("Quitting early due to user input.")
406
+ exit(-1)
407
+ else:
408
+ print("Please choose \'y\', \'n\' or \'q\'")
409
+
410
+ def SendOrDisplay(self, print_ready_msg: bool = False):
411
+ if self._setup_was_run:
412
+ if print_ready_msg:
413
+ print(f"Ready to send/display mail to/for {self.email.to}...")
414
+ self._logger.info(f"Ready to send/display mail to/for {self.email.to}...")
415
+ if self.send_emails and self.display_window:
416
+ send_and_display_warning = ("Sending email while also displaying the email "
417
+ "in the app is not possible. Defaulting to Display only")
418
+ # print(send_and_display_warning)
419
+ self._logger.warning(send_and_display_warning)
420
+ self.send_emails = False
421
+ self.display_window = True
422
+
423
+ if self.send_emails:
424
+ if self.auto_send:
425
+ self._logger.info("Sending emails with auto_send...")
426
+ self.email.send()
427
+
428
+ else:
429
+ self._manual_send_loop()
430
+
431
+ elif self.display_window:
432
+ self.email.display()
433
+ else:
434
+ both_disabled_warning = ("Both sending and displaying the email are disabled. "
435
+ "No errors were encountered.")
436
+ self._logger.warning(both_disabled_warning)
437
+ # print(both_disabled_warning)
438
+ else:
439
+ try:
440
+ raise EmailerNotSetupError("Setup has not been run, sending or displaying an email cannot occur.")
441
+ except EmailerNotSetupError as e:
442
+ self._logger.error(e, exc_info=True)
443
+ raise e
444
+
445
+ @staticmethod
446
+ def _fmsg_is_no_info_or_err(info):
447
+ return (any(isinstance(x, Exception) for x in info)
448
+ or all(isinstance(x, type(None)) for x in info))
449
+
450
+ def get_failed_sends(self, fail_string_marker: str = 'undeliverable', partial_match_ok: bool = True, **kwargs):
451
+ failed_sends = []
452
+ recent_days_cap = kwargs.get('recent_days_cap', 1)
453
+ self.GetMessages(BasicEmailFolderChoices.INBOX)
454
+
455
+ msg_candidates = self.FindMsgBySubject(fail_string_marker, partial_match_ok=partial_match_ok)
456
+
457
+ if msg_candidates:
458
+ msg_candidates = [FailedMsg(m) for m in msg_candidates]
459
+ self._logger.info(f"{len(msg_candidates)} 'failed send' candidates found.")
460
+ self._logger.info("mutating msg_candidates (Msg instances) into FailedMsg instances.")
461
+
462
+ for m in msg_candidates:
463
+ failed_info = m.process_failed_msg(m(), recent_days_cap=recent_days_cap)
464
+
465
+ if self._fmsg_is_no_info_or_err(failed_info):
466
+ continue
467
+ else:
468
+ failed_sends.append({'postmaster_email': m.sender,
469
+ 'err_info': failed_info})
470
+ results_string = self.__class__.FAILED_SEND_LOGGER_STRING.format(num=len(failed_sends),
471
+ recent_days_cap=recent_days_cap)
472
+ if (not self._logger.hasHandlers() or not any([isinstance(x, StreamHandler)
473
+ for x in self._logger.handlers])):
474
+ print(results_string)
475
+ self._logger.info(results_string)
476
+ return failed_sends
477
+
478
+
479
+ def __failed_sends_test(emailer):
480
+ failed_sends = emailer.get_failed_sends(recent_days_cap=1)
481
+ fs_results = ([(x.get('err_info').get('send_time'),
482
+ x.get('err_info').get('failed_subject'))
483
+ for x in failed_sends]
484
+ if failed_sends else "no failed sends found")
485
+ print(fs_results)
486
+
487
+
488
+ def __setup_and_send_test(emailer):
489
+ emailer.SetupEmail(subject="TEST: Your TEST agreement expires in 30 days or less!",
490
+ recipient='amcsparron@albanyny.gov',
491
+ text="testing to see anything works", bcc='amcsparron@albanyny.gov')
492
+ emailer.SendOrDisplay()
493
+
494
+
495
+ if __name__ == "__main__":
496
+ module_name = __file__.split('\\')[-1].split('.py')[0]
497
+
498
+ em = PyEmailer(display_window=False, send_emails=True, auto_send=False, use_default_logger=True)
499
+ m = em.find_messages_by_subject('Andrew', partial_match_ok=True, include_re=True, include_fw=True)
500
+ print([type(x) for x in m])
501
+ # __setup_and_send_test(em)
502
+ # __failed_sends_test(em)
503
+ # x = emailer.find_messages_by_subject("Timecard", partial_match_ok=False, include_re=False)
504
+ # #print([(m.SenderEmailAddress, m.SenderEmailType, [x.name for x in m.ItemProperties]) for m in x])
505
+ # property_accessor = x[0].PropertyAccessor
506
+ # print(x[0].Sender.GetExchangeUser().PrimarySmtpAddress)
507
+ # print(property_accessor.GetProperty("PR_EMAIL_ADDRESS"))
508
+
509
+ # r_dict = {
510
+ # "subject": f"TEST: Your TEST "
511
+ # f"agreement expires in 30 days or less!",
512
+ # "text": "testing to see if the attachment works",
513
+ # "recipient": 'test',
514
+ # "attachments": []
515
+ # }
516
+ # # &emsp; is the tab character for emails
517
+ # emailer.SetupEmail(**r_dict) # recipient="test", subject="test subject", text="test_body")
518
+ # emailer.SendOrDisplay()
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyEmailerAJM
3
+ Version: 1.6
4
+ Summary: Allows for automating sending Email with the Outlook Desktop client. Future releases will add more client support
5
+ Home-page: https://github.com/amcsparron2793-Water/PyEmailer
6
+ Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.6.tar.gz
7
+ Author: Amcsparron
8
+ Author-email: amcsparron@albanyny.gov
9
+ License: MIT License
10
+ Keywords: Outlook,Email,Automation
11
+ License-File: LICENSE.txt
12
+ Requires-Dist: pywin32
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: download-url
16
+ Dynamic: home-page
17
+ Dynamic: keywords
18
+ Dynamic: license
19
+ Dynamic: license-file
20
+ Dynamic: requires-dist
21
+ Dynamic: summary
@@ -0,0 +1,11 @@
1
+ PyEmailerAJM/__init__.py,sha256=bn1ONP8stlYp4ngNMRICP6AheZdZ2F9QiFs0uSmTwHA,436
2
+ PyEmailerAJM/_version.py,sha256=M1eDmUwdJ1O4qzSP864VIsLUTc686xQagwVqbieJkTU,21
3
+ PyEmailerAJM/errs.py,sha256=-_SM8q3cdTacvefJl-__qC6uIEn8uQgo1YdVXq22sjw,99
4
+ PyEmailerAJM/helpers.py,sha256=bvYvU4Dma9JgDiXE31xdeXVP6diYBU1tab1Hesxxnr0,1052
5
+ PyEmailerAJM/msg.py,sha256=jornyustxEvhi8QMupKxHGcV2xzLB3-Ftjc-cqNBvQ8,9666
6
+ PyEmailerAJM/py_emailer_ajm.py,sha256=WEtg2HzEcW2v_MiPyIb7-kHCk1k7ZelNEXx8zBGI2xY,22574
7
+ pyemailerajm-1.6.dist-info/licenses/LICENSE.txt,sha256=7TxqSLofaZz1NmKH8ljjGHxsgR1OyIeGah5ZZrKYVXI,1084
8
+ pyemailerajm-1.6.dist-info/METADATA,sha256=KhMHZ1piuEudIEV-iw7QnFbXR4rpveQOO1Sy0zE_O6M,690
9
+ pyemailerajm-1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ pyemailerajm-1.6.dist-info/top_level.txt,sha256=SCqZjBB-AUu9Qkgcrfc3KhH8VSriJqEGevQVPpIyI4Q,13
11
+ pyemailerajm-1.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,17 @@
1
+ MIT License
2
+ Copyright (c) 2018 Andrew McSparron
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+ The above copyright notice and this permission notice shall be included in all
10
+ copies or substantial portions of the Software.
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ PyEmailerAJM