izteamslots 1.1.0

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,221 @@
1
+ """IMAP mail provider.
2
+
3
+ Connects to any IMAP server to read mail. Useful when you have
4
+ your own domain or existing email accounts.
5
+
6
+ Configuration (env vars or constructor kwargs):
7
+ IMAP_HOST - IMAP server hostname (required)
8
+ IMAP_PORT - port (default: 993 for SSL, 143 for plain)
9
+ IMAP_SSL - "1" for SSL, "0" for STARTTLS/plain (default: "1")
10
+ IMAP_FOLDER - mailbox folder to read (default: "INBOX")
11
+ IMAP_MAX_MESSAGES - max messages to fetch (default: 50, newest first)
12
+
13
+ Usage::
14
+
15
+ provider = create_provider("imap", host="imap.example.com")
16
+ inbox = provider.inbox(Mailbox(email="user@example.com", password="pass"))
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import email as email_lib
21
+ import imaplib
22
+ import os
23
+ import re
24
+ from email.header import decode_header as _decode_header
25
+ from typing import Any
26
+
27
+ from .base import (
28
+ Inbox,
29
+ Mail,
30
+ MailAuthError,
31
+ Mailbox,
32
+ MailError,
33
+ MailProvider,
34
+ MailServiceUnavailable,
35
+ )
36
+
37
+ _HTML_TAG_RE = re.compile(r"<[^>]+>")
38
+ _MULTI_SPACE_RE = re.compile(r"[ \t]+")
39
+ _MULTI_NL_RE = re.compile(r"\n{3,}")
40
+
41
+
42
+ def _html_to_text(html: str) -> str:
43
+ """Rough HTML → plain text."""
44
+ text = html.replace("<br>", "\n").replace("<br/>", "\n").replace("<br />", "\n")
45
+ text = text.replace("</p>", "\n\n").replace("</div>", "\n")
46
+ text = _HTML_TAG_RE.sub("", text)
47
+ text = _MULTI_SPACE_RE.sub(" ", text)
48
+ text = _MULTI_NL_RE.sub("\n\n", text)
49
+ return text.strip()
50
+
51
+
52
+ def _decode_header_value(raw: str | None) -> str:
53
+ if not raw:
54
+ return ""
55
+ parts = _decode_header(raw)
56
+ decoded: list[str] = []
57
+ for part, charset in parts:
58
+ if isinstance(part, bytes):
59
+ decoded.append(part.decode(charset or "utf-8", errors="replace"))
60
+ else:
61
+ decoded.append(part)
62
+ return "".join(decoded)
63
+
64
+
65
+ def _extract_body(msg: email_lib.message.Message) -> str: # type: ignore[type-arg]
66
+ """Extract text body from an email message, preferring plain text."""
67
+ if not msg.is_multipart():
68
+ payload = msg.get_payload(decode=True)
69
+ if not payload:
70
+ return ""
71
+ text = payload.decode(msg.get_content_charset() or "utf-8", errors="replace")
72
+ if msg.get_content_type() == "text/html":
73
+ return _html_to_text(text)
74
+ return text
75
+
76
+ plain_parts: list[str] = []
77
+ html_parts: list[str] = []
78
+
79
+ for part in msg.walk():
80
+ ct = part.get_content_type()
81
+ if ct == "multipart/alternative" or ct.startswith("multipart/"):
82
+ continue
83
+ payload = part.get_payload(decode=True)
84
+ if not payload:
85
+ continue
86
+ charset = part.get_content_charset() or "utf-8"
87
+ text = payload.decode(charset, errors="replace")
88
+ if ct == "text/plain":
89
+ plain_parts.append(text)
90
+ elif ct == "text/html":
91
+ html_parts.append(text)
92
+
93
+ if plain_parts:
94
+ return "\n".join(plain_parts)
95
+ if html_parts:
96
+ return _html_to_text("\n".join(html_parts))
97
+ return ""
98
+
99
+
100
+ class IMAPProvider(MailProvider):
101
+ """IMAP mail provider — reads mail from any IMAP server."""
102
+
103
+ name = "imap"
104
+
105
+ def __init__(
106
+ self,
107
+ host: str | None = None,
108
+ port: int | None = None,
109
+ use_ssl: bool | None = None,
110
+ folder: str | None = None,
111
+ max_messages: int | None = None,
112
+ timeout: int = 30,
113
+ **_kwargs: Any,
114
+ ) -> None:
115
+ self.host = host or os.environ.get("IMAP_HOST", "")
116
+ if not self.host:
117
+ raise MailError("IMAP_HOST is required (env var or host= kwarg)")
118
+
119
+ ssl_env = os.environ.get("IMAP_SSL", "1")
120
+ self.use_ssl = use_ssl if use_ssl is not None else (ssl_env == "1")
121
+
122
+ default_port = 993 if self.use_ssl else 143
123
+ self.port = port or int(os.environ.get("IMAP_PORT", str(default_port)))
124
+
125
+ self.folder = folder or os.environ.get("IMAP_FOLDER", "INBOX")
126
+ self.max_messages = max_messages or int(os.environ.get("IMAP_MAX_MESSAGES", "50"))
127
+ self.timeout = timeout
128
+
129
+ def _connect(self, mailbox: Mailbox) -> imaplib.IMAP4_SSL | imaplib.IMAP4:
130
+ """Open an IMAP connection and authenticate."""
131
+ try:
132
+ if self.use_ssl:
133
+ conn = imaplib.IMAP4_SSL(self.host, self.port, timeout=self.timeout)
134
+ else:
135
+ conn = imaplib.IMAP4(self.host, self.port, timeout=self.timeout)
136
+ except (OSError, imaplib.IMAP4.error) as e:
137
+ raise MailServiceUnavailable(f"Cannot connect to {self.host}:{self.port}: {e}") from e
138
+
139
+ try:
140
+ conn.login(mailbox.email, mailbox.password)
141
+ except imaplib.IMAP4.error as e:
142
+ err_msg = str(e)
143
+ conn.logout()
144
+ raise MailAuthError(f"IMAP login failed for {mailbox.email}: {err_msg}") from e
145
+
146
+ return conn
147
+
148
+ def generate(self) -> Mailbox:
149
+ """IMAP cannot create mailboxes automatically.
150
+
151
+ If you need auto-generation, consider subclassing and
152
+ providing a pool of pre-created accounts.
153
+ """
154
+ raise NotImplementedError(
155
+ "IMAP provider does not support generate(). "
156
+ "Create mailboxes externally and use Mailbox(email=..., password=...)."
157
+ )
158
+
159
+ def inbox(self, mailbox: Mailbox) -> Inbox:
160
+ """Fetch messages from IMAP server."""
161
+ conn = self._connect(mailbox)
162
+ messages: list[Mail] = []
163
+
164
+ try:
165
+ status, _ = conn.select(self.folder, readonly=True)
166
+ if status != "OK":
167
+ raise MailError(f"Cannot select folder {self.folder!r}")
168
+
169
+ _, msg_nums_raw = conn.search(None, "ALL")
170
+ all_nums = msg_nums_raw[0].split() if msg_nums_raw[0] else []
171
+
172
+ # Newest first, limited
173
+ fetch_nums = all_nums[-self.max_messages:]
174
+ fetch_nums.reverse()
175
+
176
+ if not fetch_nums:
177
+ return Inbox(email=mailbox.email, messages=[])
178
+
179
+ # Batch fetch for efficiency
180
+ num_range = ",".join(n.decode() for n in fetch_nums)
181
+ _, fetch_data = conn.fetch(num_range, "(RFC822)")
182
+
183
+ for item in fetch_data:
184
+ if not isinstance(item, tuple) or len(item) < 2:
185
+ continue
186
+
187
+ raw_bytes = item[1]
188
+ if not isinstance(raw_bytes, bytes):
189
+ continue
190
+
191
+ msg = email_lib.message_from_bytes(raw_bytes)
192
+
193
+ msg_id = msg.get("Message-ID", "")
194
+ if not msg_id:
195
+ # Use IMAP sequence number from the fetch response
196
+ header = item[0]
197
+ if isinstance(header, bytes):
198
+ seq_match = re.match(rb"(\d+)", header)
199
+ msg_id = seq_match.group(1).decode() if seq_match else ""
200
+
201
+ messages.append(Mail(
202
+ id=msg_id,
203
+ sender=_decode_header_value(msg.get("From", "")),
204
+ subject=_decode_header_value(msg.get("Subject", "")),
205
+ body=_extract_body(msg),
206
+ date=msg.get("Date", ""),
207
+ ))
208
+
209
+ except (MailError, MailAuthError):
210
+ raise
211
+ except imaplib.IMAP4.error as e:
212
+ raise MailError(f"IMAP error: {e}") from e
213
+ except Exception as e:
214
+ raise MailError(f"Failed to read mail: {e}") from e
215
+ finally:
216
+ try:
217
+ conn.logout()
218
+ except Exception:
219
+ pass
220
+
221
+ return Inbox(email=mailbox.email, messages=messages)
@@ -0,0 +1,121 @@
1
+ """Mail provider: trickadsagencyltd.com temporary email service."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from typing import Any
7
+
8
+ import requests
9
+
10
+ from .base import (
11
+ Inbox,
12
+ Mail,
13
+ MailAuthError,
14
+ Mailbox,
15
+ MailError,
16
+ MailProvider,
17
+ MailServiceUnavailable,
18
+ )
19
+
20
+ BASE_URL = "https://www.trickadsagencyltd.com"
21
+
22
+ HEADERS = {
23
+ "accept": "*/*",
24
+ "cache-control": "no-cache",
25
+ "pragma": "no-cache",
26
+ "origin": BASE_URL,
27
+ "referer": f"{BASE_URL}/tepmail",
28
+ "user-agent": (
29
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
30
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
31
+ "Chrome/142.0.0.0 Safari/537.36"
32
+ ),
33
+ }
34
+
35
+
36
+ def _extract_html_comment(text: str) -> str:
37
+ m = re.search(r"<!--\s*([\s\S]*?)\s*-->", text)
38
+ if not m:
39
+ return ""
40
+ return m.group(1).strip().splitlines()[0][:300]
41
+
42
+
43
+ def _extract_error_summary(text: str) -> str:
44
+ candidate = _extract_html_comment(text)
45
+ if candidate:
46
+ return candidate
47
+ try:
48
+ payload = json.loads(text)
49
+ if isinstance(payload, dict):
50
+ for key in ("message", "error", "detail"):
51
+ value = payload.get(key)
52
+ if isinstance(value, str) and value.strip():
53
+ return value.strip()[:300]
54
+ except Exception:
55
+ pass
56
+ return text.strip().replace("\n", " ")[:300]
57
+
58
+
59
+ class TrickAdsProvider(MailProvider):
60
+ """Temporary email via trickadsagencyltd.com/tepmail."""
61
+
62
+ name = "trickads"
63
+
64
+ def __init__(self, session: requests.Session | None = None) -> None:
65
+ self._external = session is not None
66
+ self._session = session or requests.Session()
67
+ self._session.headers.update(HEADERS)
68
+
69
+ def _request(
70
+ self,
71
+ endpoint: str,
72
+ json_body: dict[str, Any] | None = None,
73
+ ) -> dict[str, Any]:
74
+ try:
75
+ resp = self._session.post(f"{BASE_URL}{endpoint}", json=json_body)
76
+ if resp.status_code != 200:
77
+ body = resp.text or ""
78
+ summary = _extract_error_summary(body)
79
+ if resp.status_code == 401:
80
+ raise MailAuthError(f"[{resp.status_code}] {summary}")
81
+ if resp.status_code >= 500:
82
+ raise MailServiceUnavailable(f"[{resp.status_code}] {summary}")
83
+ raise MailError(f"[{resp.status_code}] {summary}")
84
+ data: dict[str, Any] = resp.json()
85
+ except requests.RequestException as e:
86
+ raise MailServiceUnavailable(f"Connection error: {e}") from e
87
+
88
+ if data.get("status") != "success":
89
+ msg = data.get("message", "")
90
+ code = data.get("code", 0)
91
+ if code == 401 or "password" in msg.lower() or "unauthorized" in str(data.get("status", "")).lower():
92
+ raise MailAuthError(f"API: {data}")
93
+ raise MailError(f"API: {data}")
94
+ return data
95
+
96
+ def generate(self) -> Mailbox:
97
+ data = self._request("/tepmail/generate")
98
+ return Mailbox(email=data["email"], password=data["password"])
99
+
100
+ def inbox(self, mailbox: Mailbox) -> Inbox:
101
+ data = self._request(
102
+ "/tepmail/inbox",
103
+ json_body={"email": mailbox.email, "password": mailbox.password},
104
+ )
105
+ return Inbox(
106
+ email=data["email"],
107
+ messages=[
108
+ Mail(
109
+ id=m["id"],
110
+ sender=m.get("from", ""),
111
+ subject=m.get("subject", ""),
112
+ body=m.get("body", ""),
113
+ date=m.get("date", ""),
114
+ )
115
+ for m in data.get("messages", [])
116
+ ],
117
+ )
118
+
119
+ def close(self) -> None:
120
+ if not self._external:
121
+ self._session.close()