PyEmailerAJM 1.6__tar.gz → 1.7__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.
@@ -1,15 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyEmailerAJM
3
- Version: 1.6
3
+ Version: 1.7
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.tar.gz
6
+ Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.7.tar.gz
7
7
  Author: Amcsparron
8
8
  Author-email: amcsparron@albanyny.gov
9
9
  License: MIT License
10
10
  Keywords: Outlook,Email,Automation
11
11
  License-File: LICENSE.txt
12
12
  Requires-Dist: pywin32
13
+ Requires-Dist: extract_msg
14
+ Requires-Dist: email_validator
15
+ Requires-Dist: questionary
13
16
  Dynamic: author
14
17
  Dynamic: author-email
15
18
  Dynamic: download-url
@@ -0,0 +1,8 @@
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']
@@ -0,0 +1 @@
1
+ __version__ = '1.7'
@@ -5,11 +5,9 @@ py_emailer_ajm.py
5
5
  install win32 with pip install pywin32
6
6
  """
7
7
  # imports
8
- from abc import abstractmethod
9
8
  from os import environ
10
9
  from os.path import isfile, join, isdir
11
10
  from tempfile import gettempdir
12
- from typing import List
13
11
 
14
12
  # install win32 with pip install pywin32
15
13
  import win32com.client as win32
@@ -23,11 +21,12 @@ import questionary
23
21
  # this is usually thrown when questionary is used in the dev/Non Win32 environment
24
22
  # noinspection PyProtectedMember
25
23
  from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
26
- from win32com.client import CDispatch
27
24
 
28
- from PyEmailerAJM import EmailerNotSetupError, DisplayManualQuit
29
- from PyEmailerAJM import BasicEmailFolderChoices, deprecated
30
- from PyEmailerAJM import Msg, FailedMsg
25
+ from PyEmailerAJM import (EmailerNotSetupError, DisplayManualQuit,
26
+ deprecated,
27
+ Msg, FailedMsg)
28
+ from PyEmailerAJM.backend import BasicEmailFolderChoices
29
+ from PyEmailerAJM.searchers import SubjectSearcher
31
30
 
32
31
 
33
32
  class EmailerInitializer:
@@ -52,6 +51,7 @@ class EmailerInitializer:
52
51
  self.auto_send = auto_send
53
52
  self.send_emails = send_emails
54
53
 
54
+ # TODO: replace me with EasyLoggerAJM
55
55
  def _initialize_logger(self, logger=None, **kwargs):
56
56
  if logger:
57
57
  self._logger = logger
@@ -121,74 +121,7 @@ class EmailerInitializer:
121
121
  return self.email_app, self.namespace
122
122
 
123
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):
124
+ class PyEmailer(EmailerInitializer, SubjectSearcher):
192
125
  # the email tab_char
193
126
  tab_char = ' '
194
127
  signature_dir_path = join((environ['USERPROFILE']),
@@ -496,12 +429,14 @@ if __name__ == "__main__":
496
429
  module_name = __file__.split('\\')[-1].split('.py')[0]
497
430
 
498
431
  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)
432
+ m = em.find_messages_by_subject('Andrew', partial_match_ok=True)
500
433
  print([type(x) for x in m])
501
434
  # __setup_and_send_test(em)
502
435
  # __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])
436
+ x = em.find_messages_by_subject("GIS Request", partial_match_ok=True)
437
+ # [x.name for x in m.ItemProperties]
438
+ print([(m.__class__, m.sender, m.sender_email_type, m.subject)
439
+ for m in [Msg(y) for y in x]]) # for m in x])
505
440
  # property_accessor = x[0].PropertyAccessor
506
441
  # print(x[0].Sender.GetExchangeUser().PrimarySmtpAddress)
507
442
  # print(property_accessor.GetProperty("PR_EMAIL_ADDRESS"))
@@ -1,15 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyEmailerAJM
3
- Version: 1.6
3
+ Version: 1.7
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.tar.gz
6
+ Download-URL: https://github.com/amcsparron2793-Water/PyEmailer/archive/refs/tags/1.7.tar.gz
7
7
  Author: Amcsparron
8
8
  Author-email: amcsparron@albanyny.gov
9
9
  License: MIT License
10
10
  Keywords: Outlook,Email,Automation
11
11
  License-File: LICENSE.txt
12
12
  Requires-Dist: pywin32
13
+ Requires-Dist: extract_msg
14
+ Requires-Dist: email_validator
15
+ Requires-Dist: questionary
13
16
  Dynamic: author
14
17
  Dynamic: author-email
15
18
  Dynamic: download-url
@@ -4,9 +4,7 @@ setup.cfg
4
4
  setup.py
5
5
  PyEmailerAJM/__init__.py
6
6
  PyEmailerAJM/_version.py
7
- PyEmailerAJM/errs.py
8
7
  PyEmailerAJM/helpers.py
9
- PyEmailerAJM/msg.py
10
8
  PyEmailerAJM/py_emailer_ajm.py
11
9
  PyEmailerAJM.egg-info/PKG-INFO
12
10
  PyEmailerAJM.egg-info/SOURCES.txt
@@ -0,0 +1,4 @@
1
+ pywin32
2
+ extract_msg
3
+ email_validator
4
+ questionary
@@ -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'],
20
+ install_requires=['pywin32', 'extract_msg', 'email_validator', 'questionary'],
21
21
  license='MIT License',
22
22
  author='Amcsparron',
23
23
  author_email='amcsparron@albanyny.gov',
@@ -1,8 +0,0 @@
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']
@@ -1 +0,0 @@
1
- __version__ = '1.6'
@@ -1,6 +0,0 @@
1
- class EmailerNotSetupError(Exception):
2
- ...
3
-
4
-
5
- class DisplayManualQuit(Exception):
6
- ...
@@ -1,230 +0,0 @@
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
@@ -1 +0,0 @@
1
- pywin32
File without changes
File without changes
File without changes