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.
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/PKG-INFO +2 -2
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/pyproject.toml +2 -2
- webtoolkit-0.1.67/webtoolkit/utils/__init__.py +1 -0
- webtoolkit-0.1.67/webtoolkit/utils/emailreader.py +239 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/LICENSE +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/README.md +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/__init__.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/baseurl.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/contentinterface.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/contentlinkparser.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/contentmoderation.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/contenttext.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/crawlers/__init__.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/crawlers/crawlerinterface.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/crawlers/requestscrawler.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/domaincache.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/__init__.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/defaulturlhandler.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlerchannelodysee.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlerchannelyoutube.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlerhttppage.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlerinterface.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlers.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlervideoodysee.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/handlers/handlervideoyoutube.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/pages.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/remoteserver.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/remoteurl.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/request.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/response.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/statuses.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/__init__.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/__init__.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/codeproject.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/firebog.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/geekwirecom.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/githubcom.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/hackernews.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/instance.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/opmlfile.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/reddit.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/returndislike.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/robotstxtcom.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/thehill.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/warhammercommunity.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fake/youtube.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fakeinternet.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fakeinternetcontents.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/fakeresponse.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/tests/mocks.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/urllocation.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/utils/dateutils.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/utils/logger.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/utils/memorychecker.py +0 -0
- {webtoolkit-0.1.64 → webtoolkit-0.1.67}/webtoolkit/webconfig.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
18
|
+
brutefeedparser="^1.0.0"
|
|
19
19
|
pytz = "^2024.2"
|
|
20
20
|
psutil="*"
|
|
21
21
|
url-cleaner="*"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|