webtoolkit 0.1.64__tar.gz → 0.1.67__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/PKG-INFO +2 -2
  2. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/pyproject.toml +2 -2
  3. webtoolkit-0.1.67/webtoolkit/utils/__init__.py +1 -0
  4. webtoolkit-0.1.67/webtoolkit/utils/emailreader.py +239 -0
  5. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/LICENSE +0 -0
  6. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/README.md +0 -0
  7. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/__init__.py +0 -0
  8. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/baseurl.py +0 -0
  9. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/contentinterface.py +0 -0
  10. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/contentlinkparser.py +0 -0
  11. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/contentmoderation.py +0 -0
  12. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/contenttext.py +0 -0
  13. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/crawlers/__init__.py +0 -0
  14. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/crawlers/crawlerinterface.py +0 -0
  15. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/crawlers/requestscrawler.py +0 -0
  16. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/domaincache.py +0 -0
  17. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/__init__.py +0 -0
  18. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/defaulturlhandler.py +0 -0
  19. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlerchannelodysee.py +0 -0
  20. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlerchannelyoutube.py +0 -0
  21. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlerhttppage.py +0 -0
  22. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlerinterface.py +0 -0
  23. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlers.py +0 -0
  24. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlervideoodysee.py +0 -0
  25. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlervideoyoutube.py +0 -0
  26. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/pages.py +0 -0
  27. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/remoteserver.py +0 -0
  28. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/remoteurl.py +0 -0
  29. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/request.py +0 -0
  30. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/response.py +0 -0
  31. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/statuses.py +0 -0
  32. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/__init__.py +0 -0
  33. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/__init__.py +0 -0
  34. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/codeproject.py +0 -0
  35. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/firebog.py +0 -0
  36. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/geekwirecom.py +0 -0
  37. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/githubcom.py +0 -0
  38. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/hackernews.py +0 -0
  39. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/instance.py +0 -0
  40. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/opmlfile.py +0 -0
  41. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/reddit.py +0 -0
  42. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/returndislike.py +0 -0
  43. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/robotstxtcom.py +0 -0
  44. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/thehill.py +0 -0
  45. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/warhammercommunity.py +0 -0
  46. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/youtube.py +0 -0
  47. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fakeinternet.py +0 -0
  48. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fakeinternetcontents.py +0 -0
  49. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fakeresponse.py +0 -0
  50. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/mocks.py +0 -0
  51. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/urllocation.py +0 -0
  52. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/utils/dateutils.py +0 -0
  53. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/utils/logger.py +0 -0
  54. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/utils/memorychecker.py +0 -0
  55. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/webconfig.py +0 -0
  56. {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/webtools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: webtoolkit
3
- Version: 0.1.64
3
+ Version: 0.1.67
4
4
  Summary: Web tools and interfaces for Internet data processing.
5
5
  License: GPL3
6
6
  Author: Iwan Grozny
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Requires-Dist: beautifulsoup4 (>=4.13.5,<5.0.0)
17
- Requires-Dist: brutefeedparser (>=0.10.5,<0.11.0)
17
+ Requires-Dist: brutefeedparser (>=1.0.0,<2.0.0)
18
18
  Requires-Dist: lxml (>=5.4.0,<6.0.0)
19
19
  Requires-Dist: psutil
20
20
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
@@ -3,7 +3,7 @@
3
3
 
4
4
  [tool.poetry]
5
5
  name = "webtoolkit"
6
- version = "0.1.64"
6
+ version = "0.1.67"
7
7
  description = "Web tools and interfaces for Internet data processing."
8
8
  authors = ["Iwan Grozny <renegat@renegat0x0.ddns.net>"]
9
9
  license = "GPL3"
@@ -15,7 +15,7 @@ python-dateutil = "^2.8.2"
15
15
  tldextract = "^5.1.2"
16
16
  beautifulsoup4 = "^4.13.5"
17
17
  lxml = "^5.4.0"
18
- brutefeedparser="^0.10.5"
18
+ brutefeedparser="^1.0.0"
19
19
  pytz = "^2024.2"
20
20
  psutil="*"
21
21
  url-cleaner="*"
@@ -0,0 +1,239 @@
1
+ """
2
+ em = EmailReader("server")
3
+ em.connect("username", "password")
4
+
5
+ for email_data in em.get_emails():
6
+ print("Subject:", email_data.title)
7
+ print("From:", email_data.author)
8
+ #print("Body:", email_data.body)
9
+
10
+ em.close()
11
+ """
12
+
13
+ import imaplib
14
+ import email
15
+ from email.header import decode_header
16
+ from email.utils import parsedate_to_datetime
17
+ from email.utils import parseaddr
18
+
19
+ from utils.dateutils import DateUtils
20
+
21
+
22
+ class Email(object):
23
+ """
24
+ Provides fields similar to ContentInterface
25
+ author - sender
26
+ """
27
+ def __init__(self):
28
+ self.title = None
29
+ self.body = None
30
+ self.date_published = None
31
+ self.author = None
32
+ self.id = None
33
+
34
+ def __str__(self):
35
+ return "{}/{}/{}\n{}".format(
36
+ self.title, self.date_published, self.author, self.body
37
+ )
38
+
39
+
40
+ class EmailReader(object):
41
+ def __init__(self, server, time_limit=None, creds=None):
42
+ """
43
+ Specify time limit, to search only mails newer than time limit
44
+ Note: creds is unused
45
+ """
46
+ self.server = server
47
+ self.time_limit = time_limit
48
+ self.method = "Simple"
49
+ self.creds = creds
50
+
51
+ def connect(self, username, password):
52
+ """
53
+ Connects using username and password
54
+ """
55
+ self.imap = imaplib.IMAP4_SSL(self.server)
56
+ if self.method == "Simple":
57
+ return self.imap.login(username, password)
58
+ elif self.method == "XOAUTH2":
59
+ auth_string = self.generate_xoauth2_token(creds)
60
+ return self.imap.authenticate("XOAUTH2", lambda x: auth_string)
61
+
62
+ def generate_xoauth2_token(self, creds):
63
+ return f"user={creds.id_token['email']}\1auth=Bearer {creds.token}\1\1"
64
+
65
+ def get_emails(self):
66
+ """
67
+ Yields emails with Email type
68
+ """
69
+ # Select the mailbox you want to read (in this case, the inbox)
70
+ self.imap.select("inbox")
71
+
72
+ # Search for all emails
73
+ status, messages = self.imap.search(None, "ALL")
74
+ email_ids = messages[0].split()
75
+ email_ids.reverse()
76
+
77
+ for email_data in self.read_emails_impl(email_ids):
78
+ yield email_data
79
+
80
+ def read_emails_impl(self, email_ids):
81
+ """
82
+ Reads all emails within time limit
83
+ Note: generator
84
+ """
85
+ for email_id in email_ids:
86
+ mail = self.read_email_impl(email_id)
87
+ if self.time_limit:
88
+ if mail.date_published < self.time_limit:
89
+ break
90
+ else:
91
+ yield mail
92
+ else:
93
+ yield mail
94
+
95
+ def read_email_impl(self, email_id) -> Email:
96
+ """
97
+ Read email into email structure
98
+ @return email
99
+ """
100
+ # Fetch the email by ID
101
+ res, msg_data = self.imap.fetch(email_id, "(RFC822)")
102
+ raw_email = msg_data[0][1]
103
+
104
+ msg = email.message_from_bytes(raw_email)
105
+
106
+ mail = Email()
107
+ mail.id = msg["Message-ID"]
108
+ mail.title = self.decode_mime_words(msg["Subject"])
109
+ mail.body = self.extract_email_body(msg)
110
+ mail.date_published = self.extract_email_date(msg)
111
+ mail.author = self.extract_email_author(msg)
112
+
113
+ return mail
114
+
115
+ def extract_email_author(self, msg):
116
+ # Try From
117
+ from_header = msg.get("From")
118
+ if from_header:
119
+ name, addr = parseaddr(from_header)
120
+ name = self.decode_mime_words(name)
121
+ if addr:
122
+ return f"{name} <{addr}>" if name else addr
123
+
124
+ # Fallback to Sender
125
+ sender = msg.get("Sender")
126
+ if sender:
127
+ name, addr = parseaddr(sender)
128
+ name = self.decode_mime_words(name)
129
+ if addr:
130
+ return f"{name} <{addr}>" if name else addr
131
+
132
+ # Fallback to Reply-To
133
+ reply_to = msg.get("Reply-To")
134
+ if reply_to:
135
+ name, addr = parseaddr(reply_to)
136
+ name = self.decode_mime_words(name)
137
+ if addr:
138
+ return f"{name} <{addr}>" if name else addr
139
+
140
+ # Last resort: return decoded name only
141
+ return self.decode_mime_words(from_header) if from_header else None
142
+
143
+ def extract_email_date(self, msg):
144
+ # Extract and parse the sent date
145
+ date_header = msg["Date"]
146
+ if date_header:
147
+ try:
148
+ date = parsedate_to_datetime(date_header)
149
+ date = DateUtils.to_utc_date(date)
150
+ return date
151
+ except Exception as e:
152
+ print(f"Failed to parse email date: {e}")
153
+
154
+ def extract_email_body(self, msg):
155
+ """
156
+ Extracts the email body from an email.message.Message object.
157
+ Prefers 'text/plain', falls back to 'text/html' if needed.
158
+ Handles different character encodings gracefully.
159
+
160
+ @return body, or None
161
+ """
162
+ body = None
163
+
164
+ if msg.is_multipart():
165
+ # Try to get the plain text part first
166
+ for part in msg.walk():
167
+ content_type = part.get_content_type()
168
+ content_disposition = str(part.get("Content-Disposition"))
169
+ if (
170
+ content_type == "text/plain"
171
+ and "attachment" not in content_disposition
172
+ ):
173
+ charset = part.get_content_charset() or "utf-8"
174
+ try:
175
+ body = part.get_payload(decode=True).decode(
176
+ charset, errors="replace"
177
+ )
178
+ return body # Prefer plain text; return early
179
+ except Exception as e:
180
+ print(f"Failed to decode plain text part: {e}")
181
+
182
+ # Fallback to HTML part if no plain text found
183
+ for part in msg.walk():
184
+ content_type = part.get_content_type()
185
+ content_disposition = str(part.get("Content-Disposition"))
186
+ if (
187
+ content_type == "text/html"
188
+ and "attachment" not in content_disposition
189
+ ):
190
+ charset = part.get_content_charset() or "utf-8"
191
+ try:
192
+ body = part.get_payload(decode=True).decode(
193
+ charset, errors="replace"
194
+ )
195
+ return body
196
+ except Exception as e:
197
+ print(f"Failed to decode HTML part: {e}")
198
+ else:
199
+ # Not multipart — just decode the single payload
200
+ charset = msg.get_content_charset() or "utf-8"
201
+ try:
202
+ body = msg.get_payload(decode=True).decode(charset, errors="replace")
203
+ except Exception as e:
204
+ print(f"Failed to decode body: {e}")
205
+
206
+ return body
207
+
208
+ def decode_mime_words(self, header_value):
209
+ if header_value is None:
210
+ return ""
211
+ decoded_fragments = decode_header(header_value)
212
+ decoded_string = ""
213
+ for fragment, encoding in decoded_fragments:
214
+ if isinstance(fragment, bytes):
215
+ try:
216
+ decoded_string += fragment.decode(
217
+ encoding or "utf-8", errors="replace"
218
+ )
219
+ except Exception as e:
220
+ decoded_string += fragment.decode("utf-8", errors="replace")
221
+ else:
222
+ decoded_string += fragment
223
+ return decoded_string
224
+
225
+ def close(self):
226
+ self.imap.logout()
227
+
228
+
229
+ if __name__ == "__main__":
230
+ em = EmailReader("imap.poczta.onet.pl")
231
+ em.connect("myemail@op.pl", "xxx")
232
+
233
+ for email_data in em.get_emails():
234
+ print("Subject:", email_data.title)
235
+ print("From:", email_data.author)
236
+ print("Date published:", email_data.date_published)
237
+ # print("Body:", email_data[2])
238
+
239
+ em.close()
File without changes
File without changes