tmail-cli 0.1.0__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.
- tmail/__init__.py +0 -0
- tmail/__main__.py +3 -0
- tmail/api.py +36 -0
- tmail/cli.py +546 -0
- tmail/storage.py +149 -0
- tmail/ui.py +193 -0
- tmail_cli-0.1.0.dist-info/METADATA +66 -0
- tmail_cli-0.1.0.dist-info/RECORD +11 -0
- tmail_cli-0.1.0.dist-info/WHEEL +4 -0
- tmail_cli-0.1.0.dist-info/entry_points.txt +2 -0
- tmail_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
tmail/__init__.py
ADDED
|
File without changes
|
tmail/__main__.py
ADDED
tmail/api.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
MAIL_ENDPOINT = "https://api.internal.temp-mail.io/api/v3/email/new"
|
|
4
|
+
INBOX_ENDPOINT = "https://api.internal.temp-mail.io/api/v3/email/{email}/messages"
|
|
5
|
+
ATTACHMENT_ENDPOINT = "https://api.internal.temp-mail.io/api/v3/attachment/{id}?download=1"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_random_email():
|
|
9
|
+
"""Generates a new random email address."""
|
|
10
|
+
json_data = {"min_name_length": 10, "max_name_length": 10}
|
|
11
|
+
response = requests.post(MAIL_ENDPOINT, json=json_data)
|
|
12
|
+
response.raise_for_status()
|
|
13
|
+
return response.json()["email"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def fetch_messages(email):
|
|
17
|
+
"""Fetches messages for a given email address."""
|
|
18
|
+
url = INBOX_ENDPOINT.format(email=email)
|
|
19
|
+
response = requests.get(url)
|
|
20
|
+
response.raise_for_status()
|
|
21
|
+
return response.json()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_attachment_url(att_id):
|
|
25
|
+
"""Returns the download URL for an attachment."""
|
|
26
|
+
return ATTACHMENT_ENDPOINT.format(id=att_id)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def download_attachment(att_id, save_path):
|
|
30
|
+
"""Downloads an attachment to the given file path."""
|
|
31
|
+
url = ATTACHMENT_ENDPOINT.format(id=att_id)
|
|
32
|
+
response = requests.get(url, stream=True)
|
|
33
|
+
response.raise_for_status()
|
|
34
|
+
with open(save_path, "wb") as f:
|
|
35
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
36
|
+
f.write(chunk)
|
tmail/cli.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from rich.live import Live
|
|
8
|
+
from rich.prompt import Prompt
|
|
9
|
+
|
|
10
|
+
from .api import get_random_email, fetch_messages, get_attachment_url, download_attachment
|
|
11
|
+
from .storage import (
|
|
12
|
+
save_email,
|
|
13
|
+
get_old_emails,
|
|
14
|
+
delete_email,
|
|
15
|
+
load_seen_mail_ids,
|
|
16
|
+
save_seen_mail_ids,
|
|
17
|
+
save_unread_count,
|
|
18
|
+
get_unread_counts,
|
|
19
|
+
clear_unread_count,
|
|
20
|
+
remove_unread_entry,
|
|
21
|
+
load_config,
|
|
22
|
+
save_config,
|
|
23
|
+
remove_all_data,
|
|
24
|
+
ATTACHMENTS_DIR,
|
|
25
|
+
)
|
|
26
|
+
from .ui import (
|
|
27
|
+
show_logo,
|
|
28
|
+
get_main_menu_panel,
|
|
29
|
+
get_email_actions_panel,
|
|
30
|
+
get_settings_panel,
|
|
31
|
+
show_emails_with_unread,
|
|
32
|
+
show_inbox_header,
|
|
33
|
+
build_inbox_table,
|
|
34
|
+
show_full_message,
|
|
35
|
+
show_messages_selection,
|
|
36
|
+
ask_choice,
|
|
37
|
+
wait_for_enter,
|
|
38
|
+
show_error,
|
|
39
|
+
show_success,
|
|
40
|
+
show_info,
|
|
41
|
+
console,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── helpers for non-interactive (CLI flag) use ──────────────────────
|
|
46
|
+
|
|
47
|
+
def _print_messages_plain(messages):
|
|
48
|
+
"""Print messages as plain text (for --inbox / --watch)."""
|
|
49
|
+
if not messages:
|
|
50
|
+
print("No messages.")
|
|
51
|
+
return
|
|
52
|
+
for i, msg in enumerate(messages, 1):
|
|
53
|
+
print(f"--- Message {i} ---")
|
|
54
|
+
print(f"From: {msg['from']}")
|
|
55
|
+
print(f"To: {msg['to']}")
|
|
56
|
+
print(f"Subject: {msg['subject']}")
|
|
57
|
+
print(f"Body: {msg['body_text']}")
|
|
58
|
+
if msg.get("attachments"):
|
|
59
|
+
names = ", ".join(a["name"] for a in msg["attachments"])
|
|
60
|
+
print(f"Attachments: {names}")
|
|
61
|
+
print()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _copy_silent(text):
|
|
65
|
+
"""Copy to clipboard without UI messages (for --generate)."""
|
|
66
|
+
import subprocess
|
|
67
|
+
import shutil
|
|
68
|
+
try:
|
|
69
|
+
import pyperclip
|
|
70
|
+
pyperclip.copy(text)
|
|
71
|
+
return True
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
for name, cmd in [
|
|
75
|
+
("xclip", ["xclip", "-selection", "clipboard"]),
|
|
76
|
+
("xsel", ["xsel", "--clipboard", "--input"]),
|
|
77
|
+
("wl-copy", ["wl-copy"]),
|
|
78
|
+
]:
|
|
79
|
+
if shutil.which(name):
|
|
80
|
+
try:
|
|
81
|
+
proc = subprocess.run(cmd, input=text, text=True, capture_output=True, timeout=5)
|
|
82
|
+
if proc.returncode == 0:
|
|
83
|
+
return True
|
|
84
|
+
except Exception:
|
|
85
|
+
continue
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── Interactive TUI functions ───────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def display_inbox(email, poll_interval):
|
|
92
|
+
"""Displays the inbox for a given email address with live polling.
|
|
93
|
+
|
|
94
|
+
Returns the number of new messages received this session.
|
|
95
|
+
"""
|
|
96
|
+
show_inbox_header(email)
|
|
97
|
+
|
|
98
|
+
seen_mail_ids = load_seen_mail_ids()
|
|
99
|
+
session_new_count = 0
|
|
100
|
+
|
|
101
|
+
with Live(console=console, screen=False, auto_refresh=False) as live:
|
|
102
|
+
while True:
|
|
103
|
+
try:
|
|
104
|
+
messages = fetch_messages(email)
|
|
105
|
+
table, has_new, batch_count = build_inbox_table(
|
|
106
|
+
messages, seen_mail_ids, get_attachment_url
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if has_new:
|
|
110
|
+
session_new_count += batch_count
|
|
111
|
+
live.update(table, refresh=True)
|
|
112
|
+
save_seen_mail_ids(seen_mail_ids)
|
|
113
|
+
|
|
114
|
+
time.sleep(poll_interval)
|
|
115
|
+
except requests.RequestException as e:
|
|
116
|
+
show_error(f"Error fetching inbox: {e}")
|
|
117
|
+
time.sleep(poll_interval)
|
|
118
|
+
except KeyboardInterrupt:
|
|
119
|
+
break
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
time.sleep(poll_interval)
|
|
122
|
+
|
|
123
|
+
return session_new_count
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def view_full_message(email):
|
|
127
|
+
"""Fetch all messages and let the user pick one to read in full."""
|
|
128
|
+
try:
|
|
129
|
+
messages = fetch_messages(email)
|
|
130
|
+
except requests.RequestException as e:
|
|
131
|
+
show_error(f"Error fetching messages: {e}")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if not messages:
|
|
135
|
+
show_info("No messages found.")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
show_messages_selection(messages)
|
|
139
|
+
choices = [str(i) for i in range(1, len(messages) + 1)]
|
|
140
|
+
choice = ask_choice(
|
|
141
|
+
"[bold]Enter message index to view[/bold]",
|
|
142
|
+
choices=choices,
|
|
143
|
+
show_choices=False,
|
|
144
|
+
)
|
|
145
|
+
msg = messages[int(choice) - 1]
|
|
146
|
+
show_full_message(msg)
|
|
147
|
+
wait_for_enter()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def download_attachments(email):
|
|
151
|
+
"""List messages with attachments and let the user download them."""
|
|
152
|
+
try:
|
|
153
|
+
messages = fetch_messages(email)
|
|
154
|
+
except requests.RequestException as e:
|
|
155
|
+
show_error(f"Error fetching messages: {e}")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
with_attachments = [m for m in messages if m.get("attachments")]
|
|
159
|
+
if not with_attachments:
|
|
160
|
+
show_info("No messages with attachments found.")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
show_messages_selection(with_attachments)
|
|
164
|
+
choices = [str(i) for i in range(1, len(with_attachments) + 1)]
|
|
165
|
+
choice = ask_choice(
|
|
166
|
+
"[bold]Enter message index to download attachments[/bold]",
|
|
167
|
+
choices=choices,
|
|
168
|
+
show_choices=False,
|
|
169
|
+
)
|
|
170
|
+
msg = with_attachments[int(choice) - 1]
|
|
171
|
+
atts = msg["attachments"]
|
|
172
|
+
|
|
173
|
+
save_dir = ATTACHMENTS_DIR / email
|
|
174
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
|
|
176
|
+
for att in atts:
|
|
177
|
+
dest = save_dir / att["name"]
|
|
178
|
+
try:
|
|
179
|
+
download_attachment(att["id"], dest)
|
|
180
|
+
show_success(f"Downloaded: {att['name']}")
|
|
181
|
+
except requests.RequestException as e:
|
|
182
|
+
show_error(f"Failed to download {att['name']}: {e}")
|
|
183
|
+
|
|
184
|
+
wait_for_enter()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def copy_to_clipboard(text):
|
|
188
|
+
"""Copy text to clipboard (interactive — shows UI messages)."""
|
|
189
|
+
import subprocess
|
|
190
|
+
import shutil
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
import pyperclip
|
|
194
|
+
pyperclip.copy(text)
|
|
195
|
+
show_success(f"Copied to clipboard: {text}")
|
|
196
|
+
wait_for_enter()
|
|
197
|
+
return
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
backends = [
|
|
202
|
+
("xclip", ["xclip", "-selection", "clipboard"]),
|
|
203
|
+
("xsel", ["xsel", "--clipboard", "--input"]),
|
|
204
|
+
("wl-copy", ["wl-copy"]),
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
for name, cmd in backends:
|
|
208
|
+
if shutil.which(name):
|
|
209
|
+
try:
|
|
210
|
+
proc = subprocess.run(cmd, input=text, text=True, capture_output=True, timeout=5)
|
|
211
|
+
if proc.returncode == 0:
|
|
212
|
+
show_success(f"Copied to clipboard: {text}")
|
|
213
|
+
wait_for_enter()
|
|
214
|
+
return
|
|
215
|
+
except Exception:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
show_info(
|
|
219
|
+
"Clipboard copy requires a clipboard tool.\n"
|
|
220
|
+
" X11: sudo apt install xclip (or xsel)\n"
|
|
221
|
+
" Wayland: sudo apt install wl-clipboard"
|
|
222
|
+
)
|
|
223
|
+
wait_for_enter()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def email_actions_menu(email):
|
|
227
|
+
"""Submenu for actions on a specific email after exiting inbox."""
|
|
228
|
+
config = load_config()
|
|
229
|
+
poll_interval = config["poll_interval"]
|
|
230
|
+
|
|
231
|
+
while True:
|
|
232
|
+
show_logo()
|
|
233
|
+
console.print(get_email_actions_panel(email))
|
|
234
|
+
|
|
235
|
+
choice = ask_choice(
|
|
236
|
+
"[bold]Choose an option[/bold]",
|
|
237
|
+
choices=["1", "2", "3", "4", "5", "6"],
|
|
238
|
+
default="1",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if choice == "1":
|
|
242
|
+
clear_unread_count(email)
|
|
243
|
+
session_count = display_inbox(email, poll_interval)
|
|
244
|
+
if session_count > 0:
|
|
245
|
+
save_unread_count(email, session_count)
|
|
246
|
+
elif choice == "2":
|
|
247
|
+
view_full_message(email)
|
|
248
|
+
elif choice == "3":
|
|
249
|
+
download_attachments(email)
|
|
250
|
+
elif choice == "4":
|
|
251
|
+
copy_to_clipboard(email)
|
|
252
|
+
elif choice == "5":
|
|
253
|
+
if delete_email(email):
|
|
254
|
+
remove_unread_entry(email)
|
|
255
|
+
show_success(f"Deleted email: {email}")
|
|
256
|
+
wait_for_enter()
|
|
257
|
+
return
|
|
258
|
+
show_error("Email not found in history.")
|
|
259
|
+
wait_for_enter()
|
|
260
|
+
elif choice == "6":
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def login_to_old_email():
|
|
265
|
+
"""Allows the user to select an old email to log in to."""
|
|
266
|
+
old_emails = get_old_emails()
|
|
267
|
+
if not old_emails:
|
|
268
|
+
show_info("No old emails to log in to.")
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
unread_counts = get_unread_counts()
|
|
272
|
+
show_emails_with_unread(old_emails, unread_counts)
|
|
273
|
+
choice = ask_choice(
|
|
274
|
+
"[bold]Enter the index of the email to restore[/bold]",
|
|
275
|
+
choices=[str(i) for i in range(1, len(old_emails) + 1)],
|
|
276
|
+
show_choices=False,
|
|
277
|
+
)
|
|
278
|
+
return old_emails[int(choice) - 1]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def delete_email_saved():
|
|
282
|
+
"""Main menu: delete a specific email from history."""
|
|
283
|
+
old_emails = get_old_emails()
|
|
284
|
+
if not old_emails:
|
|
285
|
+
show_info("No saved emails to delete.")
|
|
286
|
+
wait_for_enter()
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
unread_counts = get_unread_counts()
|
|
290
|
+
show_emails_with_unread(old_emails, unread_counts)
|
|
291
|
+
choice = ask_choice(
|
|
292
|
+
"[bold]Enter the index of the email to delete[/bold]",
|
|
293
|
+
choices=[str(i) for i in range(1, len(old_emails) + 1)],
|
|
294
|
+
show_choices=False,
|
|
295
|
+
)
|
|
296
|
+
email = old_emails[int(choice) - 1]
|
|
297
|
+
|
|
298
|
+
confirm = Prompt.ask(
|
|
299
|
+
f"Delete [bold]{email}[/bold]? (y/n)", choices=["y", "n"], default="n"
|
|
300
|
+
)
|
|
301
|
+
if confirm == "y":
|
|
302
|
+
if delete_email(email):
|
|
303
|
+
remove_unread_entry(email)
|
|
304
|
+
show_success(f"Deleted email: {email}")
|
|
305
|
+
else:
|
|
306
|
+
show_error("Email not found in history.")
|
|
307
|
+
wait_for_enter()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def settings_menu():
|
|
311
|
+
"""Settings menu for configuring poll interval."""
|
|
312
|
+
config = load_config()
|
|
313
|
+
|
|
314
|
+
while True:
|
|
315
|
+
show_logo()
|
|
316
|
+
console.print(get_settings_panel(config["poll_interval"]))
|
|
317
|
+
|
|
318
|
+
choice = ask_choice(
|
|
319
|
+
"[bold]Choose an option[/bold]",
|
|
320
|
+
choices=["1", "2"],
|
|
321
|
+
default="2",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if choice == "1":
|
|
325
|
+
interval = Prompt.ask(
|
|
326
|
+
"[bold]Poll interval in seconds[/bold]",
|
|
327
|
+
default=str(config["poll_interval"]),
|
|
328
|
+
)
|
|
329
|
+
try:
|
|
330
|
+
val = int(interval)
|
|
331
|
+
if val < 1:
|
|
332
|
+
show_info("Minimum interval is 1 second.")
|
|
333
|
+
continue
|
|
334
|
+
config["poll_interval"] = val
|
|
335
|
+
save_config(config)
|
|
336
|
+
show_success(f"Poll interval set to {val}s")
|
|
337
|
+
wait_for_enter()
|
|
338
|
+
except ValueError:
|
|
339
|
+
show_error("Invalid number.")
|
|
340
|
+
wait_for_enter()
|
|
341
|
+
elif choice == "2":
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def main_menu():
|
|
346
|
+
"""Displays the main menu and handles user choices."""
|
|
347
|
+
config = load_config()
|
|
348
|
+
poll_interval = config["poll_interval"]
|
|
349
|
+
|
|
350
|
+
while True:
|
|
351
|
+
show_logo()
|
|
352
|
+
console.print(get_main_menu_panel())
|
|
353
|
+
|
|
354
|
+
choice = ask_choice(
|
|
355
|
+
"[bold]Choose an option[/bold]",
|
|
356
|
+
choices=["1", "2", "3", "4", "5", "6", "7"],
|
|
357
|
+
default="1",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if choice == "1":
|
|
361
|
+
try:
|
|
362
|
+
email = get_random_email()
|
|
363
|
+
except requests.RequestException as e:
|
|
364
|
+
show_error(f"Error generating email: {e}")
|
|
365
|
+
continue
|
|
366
|
+
save_email(email)
|
|
367
|
+
show_success(f"Your new email is: {email}")
|
|
368
|
+
session_count = display_inbox(email, poll_interval)
|
|
369
|
+
if session_count > 0:
|
|
370
|
+
save_unread_count(email, session_count)
|
|
371
|
+
email_actions_menu(email)
|
|
372
|
+
elif choice == "2":
|
|
373
|
+
emails = get_old_emails()
|
|
374
|
+
unread_counts = get_unread_counts()
|
|
375
|
+
show_emails_with_unread(emails, unread_counts)
|
|
376
|
+
wait_for_enter()
|
|
377
|
+
elif choice == "3":
|
|
378
|
+
email = login_to_old_email()
|
|
379
|
+
if email:
|
|
380
|
+
clear_unread_count(email)
|
|
381
|
+
session_count = display_inbox(email, poll_interval)
|
|
382
|
+
if session_count > 0:
|
|
383
|
+
save_unread_count(email, session_count)
|
|
384
|
+
email_actions_menu(email)
|
|
385
|
+
elif choice == "4":
|
|
386
|
+
delete_email_saved()
|
|
387
|
+
elif choice == "5":
|
|
388
|
+
remove_all_data()
|
|
389
|
+
show_success("Successfully removed all old mails data.")
|
|
390
|
+
wait_for_enter()
|
|
391
|
+
elif choice == "6":
|
|
392
|
+
settings_menu()
|
|
393
|
+
config = load_config()
|
|
394
|
+
poll_interval = config["poll_interval"]
|
|
395
|
+
elif choice == "7":
|
|
396
|
+
console.print("[bold cyan]Thanks for using tmail! Goodbye![/bold cyan]")
|
|
397
|
+
sys.exit()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ── CLI flags + entry point ─────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
def _watch_inbox(email):
|
|
403
|
+
"""Live-poll inbox in plain text mode (used by --watch)."""
|
|
404
|
+
seen_ids = load_seen_mail_ids()
|
|
405
|
+
config = load_config()
|
|
406
|
+
interval = config["poll_interval"]
|
|
407
|
+
print(f"Watching {email} (poll every {interval}s, Ctrl+C to stop)...")
|
|
408
|
+
try:
|
|
409
|
+
while True:
|
|
410
|
+
try:
|
|
411
|
+
messages = fetch_messages(email)
|
|
412
|
+
new = []
|
|
413
|
+
for msg in messages:
|
|
414
|
+
mid = str(msg["id"])
|
|
415
|
+
if mid not in seen_ids:
|
|
416
|
+
seen_ids.add(mid)
|
|
417
|
+
new.append(msg)
|
|
418
|
+
if new:
|
|
419
|
+
_print_messages_plain(new)
|
|
420
|
+
save_seen_mail_ids(seen_ids)
|
|
421
|
+
time.sleep(interval)
|
|
422
|
+
except requests.RequestException as e:
|
|
423
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
424
|
+
time.sleep(interval)
|
|
425
|
+
except KeyboardInterrupt:
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def main():
|
|
430
|
+
import argparse
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
from importlib.metadata import version as _v
|
|
434
|
+
VERSION = _v("tmail-cli")
|
|
435
|
+
except Exception:
|
|
436
|
+
VERSION = "0.1.0"
|
|
437
|
+
|
|
438
|
+
parser = argparse.ArgumentParser(
|
|
439
|
+
description="Disposable email addresses in your terminal.",
|
|
440
|
+
)
|
|
441
|
+
parser.add_argument("-g", "--generate", action="store_true",
|
|
442
|
+
help="generate a new random email and print it")
|
|
443
|
+
parser.add_argument("-l", "--list", action="store_true",
|
|
444
|
+
help="list saved emails")
|
|
445
|
+
parser.add_argument("-i", "--inbox", metavar="EMAIL", nargs="?",
|
|
446
|
+
const="__generate__",
|
|
447
|
+
help="fetch inbox for EMAIL (one-shot, plain text)")
|
|
448
|
+
parser.add_argument("-w", "--watch", metavar="EMAIL", nargs="?",
|
|
449
|
+
const="__generate__",
|
|
450
|
+
help="live-poll inbox for EMAIL (plain text, Ctrl+C to stop)")
|
|
451
|
+
parser.add_argument("-d", "--delete", metavar="EMAIL",
|
|
452
|
+
help="delete EMAIL from history")
|
|
453
|
+
parser.add_argument("-n", "--interval", metavar="SECONDS", type=int,
|
|
454
|
+
help="set poll interval in seconds")
|
|
455
|
+
parser.add_argument("-c", "--clear", action="store_true",
|
|
456
|
+
help="clear all stored data")
|
|
457
|
+
parser.add_argument("-V", "--version", action="store_true",
|
|
458
|
+
help="show version and exit")
|
|
459
|
+
|
|
460
|
+
args = parser.parse_args()
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
if args.version:
|
|
464
|
+
print(f"tmail v{VERSION}")
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
# --generate can combine with --inbox or --watch
|
|
468
|
+
generated = None
|
|
469
|
+
if args.generate:
|
|
470
|
+
generated = get_random_email()
|
|
471
|
+
save_email(generated)
|
|
472
|
+
print(generated)
|
|
473
|
+
_copy_silent(generated)
|
|
474
|
+
|
|
475
|
+
if generated and not args.inbox and not args.watch:
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
if args.list:
|
|
479
|
+
emails = get_old_emails()
|
|
480
|
+
unread = get_unread_counts()
|
|
481
|
+
if not emails:
|
|
482
|
+
print("No saved emails.")
|
|
483
|
+
return
|
|
484
|
+
for i, email in enumerate(emails, 1):
|
|
485
|
+
count = unread.get(email, 0)
|
|
486
|
+
badge = f" ({count} unread)" if count else ""
|
|
487
|
+
print(f"{i}. {email}{badge}")
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
if args.inbox:
|
|
491
|
+
target = args.inbox
|
|
492
|
+
if target == "__generate__" or generated is not None:
|
|
493
|
+
if generated is None:
|
|
494
|
+
generated = get_random_email()
|
|
495
|
+
save_email(generated)
|
|
496
|
+
print(generated)
|
|
497
|
+
_copy_silent(generated)
|
|
498
|
+
target = generated
|
|
499
|
+
_print_messages_plain(fetch_messages(target))
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
if args.watch:
|
|
503
|
+
target = args.watch
|
|
504
|
+
if target == "__generate__" or generated is not None:
|
|
505
|
+
if generated is None:
|
|
506
|
+
generated = get_random_email()
|
|
507
|
+
save_email(generated)
|
|
508
|
+
print(generated)
|
|
509
|
+
_copy_silent(generated)
|
|
510
|
+
target = generated
|
|
511
|
+
_watch_inbox(target)
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
if args.delete:
|
|
515
|
+
if delete_email(args.delete):
|
|
516
|
+
remove_unread_entry(args.delete)
|
|
517
|
+
print(f"Deleted: {args.delete}")
|
|
518
|
+
else:
|
|
519
|
+
print(f"Email not found: {args.delete}", file=sys.stderr)
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
if args.interval is not None:
|
|
523
|
+
config = load_config()
|
|
524
|
+
config["poll_interval"] = args.interval
|
|
525
|
+
save_config(config)
|
|
526
|
+
print(f"Poll interval set to {args.interval}s")
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
if args.clear:
|
|
530
|
+
remove_all_data()
|
|
531
|
+
print("All data cleared.")
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
# Default: interactive TUI
|
|
535
|
+
main_menu()
|
|
536
|
+
|
|
537
|
+
except requests.RequestException as e:
|
|
538
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
539
|
+
sys.exit(1)
|
|
540
|
+
except KeyboardInterrupt:
|
|
541
|
+
console.print("\n[bold cyan]Goodbye![/bold cyan]")
|
|
542
|
+
sys.exit(0)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
if __name__ == "__main__":
|
|
546
|
+
main()
|
tmail/storage.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
DATA_DIR = Path(".tmail_data")
|
|
5
|
+
OLD_MAILS_FILE = DATA_DIR / "old_mails.json"
|
|
6
|
+
SEEN_MAILS_FILE = DATA_DIR / "seen_mails.json"
|
|
7
|
+
CONFIG_FILE = DATA_DIR / "config.json"
|
|
8
|
+
UNREAD_FILE = DATA_DIR / "unread.json"
|
|
9
|
+
ATTACHMENTS_DIR = DATA_DIR / "attachments"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# --- Email history ---
|
|
13
|
+
|
|
14
|
+
def save_email(email):
|
|
15
|
+
"""Saves a generated email to the history."""
|
|
16
|
+
DATA_DIR.mkdir(exist_ok=True)
|
|
17
|
+
if not OLD_MAILS_FILE.exists():
|
|
18
|
+
with open(OLD_MAILS_FILE, "w") as f:
|
|
19
|
+
json.dump([], f)
|
|
20
|
+
|
|
21
|
+
with open(OLD_MAILS_FILE, "r+") as f:
|
|
22
|
+
try:
|
|
23
|
+
emails = json.load(f)
|
|
24
|
+
except json.JSONDecodeError:
|
|
25
|
+
emails = []
|
|
26
|
+
if email not in emails:
|
|
27
|
+
emails.append(email)
|
|
28
|
+
f.seek(0)
|
|
29
|
+
json.dump(emails, f, indent=4)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_old_emails():
|
|
33
|
+
"""Retrieves the list of previously generated emails."""
|
|
34
|
+
if not OLD_MAILS_FILE.exists():
|
|
35
|
+
return []
|
|
36
|
+
with open(OLD_MAILS_FILE, "r") as f:
|
|
37
|
+
try:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def delete_email(email):
|
|
44
|
+
"""Removes a specific email from the history. Returns True if found."""
|
|
45
|
+
emails = get_old_emails()
|
|
46
|
+
if email not in emails:
|
|
47
|
+
return False
|
|
48
|
+
emails.remove(email)
|
|
49
|
+
with open(OLD_MAILS_FILE, "w") as f:
|
|
50
|
+
json.dump(emails, f, indent=4)
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- Seen mail IDs ---
|
|
55
|
+
|
|
56
|
+
def load_seen_mail_ids():
|
|
57
|
+
"""Loads the set of seen mail IDs from disk."""
|
|
58
|
+
if not SEEN_MAILS_FILE.exists():
|
|
59
|
+
return set()
|
|
60
|
+
with open(SEEN_MAILS_FILE, "r") as f:
|
|
61
|
+
try:
|
|
62
|
+
return set(json.load(f))
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
return set()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def save_seen_mail_ids(ids):
|
|
68
|
+
"""Persists the set of seen mail IDs to disk."""
|
|
69
|
+
with open(SEEN_MAILS_FILE, "w") as f:
|
|
70
|
+
json.dump(list(ids), f)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --- Unread counts ---
|
|
74
|
+
|
|
75
|
+
def save_unread_count(email, count):
|
|
76
|
+
"""Saves the unread message count for an email address."""
|
|
77
|
+
DATA_DIR.mkdir(exist_ok=True)
|
|
78
|
+
data = {}
|
|
79
|
+
if UNREAD_FILE.exists():
|
|
80
|
+
with open(UNREAD_FILE, "r") as f:
|
|
81
|
+
try:
|
|
82
|
+
data = json.load(f)
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
pass
|
|
85
|
+
data[email] = count
|
|
86
|
+
with open(UNREAD_FILE, "w") as f:
|
|
87
|
+
json.dump(data, f, indent=4)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_unread_counts():
|
|
91
|
+
"""Returns the dict of email -> unread count."""
|
|
92
|
+
if not UNREAD_FILE.exists():
|
|
93
|
+
return {}
|
|
94
|
+
with open(UNREAD_FILE, "r") as f:
|
|
95
|
+
try:
|
|
96
|
+
return json.load(f)
|
|
97
|
+
except json.JSONDecodeError:
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def clear_unread_count(email):
|
|
102
|
+
"""Resets the unread count for an email to zero."""
|
|
103
|
+
save_unread_count(email, 0)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def remove_unread_entry(email):
|
|
107
|
+
"""Removes the unread tracking entry for an email."""
|
|
108
|
+
data = get_unread_counts()
|
|
109
|
+
if email in data:
|
|
110
|
+
del data[email]
|
|
111
|
+
with open(UNREAD_FILE, "w") as f:
|
|
112
|
+
json.dump(data, f, indent=4)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# --- Config ---
|
|
116
|
+
|
|
117
|
+
def load_config():
|
|
118
|
+
"""Loads user settings from config file."""
|
|
119
|
+
if not CONFIG_FILE.exists():
|
|
120
|
+
return {"poll_interval": 5}
|
|
121
|
+
with open(CONFIG_FILE, "r") as f:
|
|
122
|
+
try:
|
|
123
|
+
return json.load(f)
|
|
124
|
+
except json.JSONDecodeError:
|
|
125
|
+
return {"poll_interval": 5}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def save_config(config):
|
|
129
|
+
"""Saves user settings to config file."""
|
|
130
|
+
DATA_DIR.mkdir(exist_ok=True)
|
|
131
|
+
with open(CONFIG_FILE, "w") as f:
|
|
132
|
+
json.dump(config, f, indent=4)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --- Cleanup ---
|
|
136
|
+
|
|
137
|
+
def remove_all_data():
|
|
138
|
+
"""Removes all stored data files and the data directory."""
|
|
139
|
+
for p in [OLD_MAILS_FILE, SEEN_MAILS_FILE, CONFIG_FILE, UNREAD_FILE]:
|
|
140
|
+
if p.exists():
|
|
141
|
+
p.unlink()
|
|
142
|
+
if ATTACHMENTS_DIR.exists():
|
|
143
|
+
import shutil
|
|
144
|
+
shutil.rmtree(ATTACHMENTS_DIR)
|
|
145
|
+
if DATA_DIR.exists():
|
|
146
|
+
try:
|
|
147
|
+
DATA_DIR.rmdir()
|
|
148
|
+
except OSError:
|
|
149
|
+
pass
|
tmail/ui.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.panel import Panel
|
|
3
|
+
from rich.prompt import Prompt
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
import pyfiglet
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def show_logo():
|
|
11
|
+
"""Displays the application logo and welcome panel."""
|
|
12
|
+
console.clear()
|
|
13
|
+
banner = pyfiglet.figlet_format("tMail", font="slant")
|
|
14
|
+
console.print(f"[bold green]{banner}[/bold green]")
|
|
15
|
+
logo = Panel(
|
|
16
|
+
"[dim]Your disposable email solution[/dim]",
|
|
17
|
+
title="[bold cyan]Welcome[/bold cyan]",
|
|
18
|
+
border_style="green",
|
|
19
|
+
expand=False,
|
|
20
|
+
)
|
|
21
|
+
console.print(logo)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_main_menu_panel():
|
|
25
|
+
"""Returns the main menu options panel."""
|
|
26
|
+
return Panel(
|
|
27
|
+
"[1] Generate Random Mail\n"
|
|
28
|
+
"[2] See Mails You Created\n"
|
|
29
|
+
"[3] Log In To Old Mails\n"
|
|
30
|
+
"[4] Delete Email From History\n"
|
|
31
|
+
"[5] Remove All Old Mail's Data\n"
|
|
32
|
+
"[6] Settings\n"
|
|
33
|
+
"[7] Exit",
|
|
34
|
+
title="[bold cyan]Choose Your Option[/bold cyan]",
|
|
35
|
+
border_style="blue",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_email_actions_panel(email):
|
|
40
|
+
"""Returns the email action submenu panel."""
|
|
41
|
+
return Panel(
|
|
42
|
+
"[1] View Inbox (live)\n"
|
|
43
|
+
"[2] View Full Message\n"
|
|
44
|
+
"[3] Download Attachments\n"
|
|
45
|
+
"[4] Copy Email Address\n"
|
|
46
|
+
"[5] Delete This Email\n"
|
|
47
|
+
"[6] Back to Main Menu",
|
|
48
|
+
title=f"[bold cyan]Email: {email}[/bold cyan]",
|
|
49
|
+
border_style="blue",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_settings_panel(poll_interval):
|
|
54
|
+
"""Returns the settings menu panel."""
|
|
55
|
+
return Panel(
|
|
56
|
+
f"Poll interval: [bold]{poll_interval}s[/bold]\n\n"
|
|
57
|
+
"[1] Change Poll Interval\n"
|
|
58
|
+
"[2] Back to Main Menu",
|
|
59
|
+
title="[bold cyan]Settings[/bold cyan]",
|
|
60
|
+
border_style="blue",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def show_emails_with_unread(emails, unread_counts):
|
|
65
|
+
"""Displays a list of emails with unread counts in a table."""
|
|
66
|
+
if not emails:
|
|
67
|
+
console.print("[yellow]No old emails found.[/yellow]")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
table = Table(title="[bold green]Your Created Emails[/bold green]")
|
|
71
|
+
table.add_column("Index", style="cyan")
|
|
72
|
+
table.add_column("Email Address", style="magenta")
|
|
73
|
+
table.add_column("Unread", style="yellow", justify="right")
|
|
74
|
+
|
|
75
|
+
for i, email in enumerate(emails, 1):
|
|
76
|
+
count = unread_counts.get(email, 0)
|
|
77
|
+
badge = str(count) if count else ""
|
|
78
|
+
table.add_row(str(i), email, badge)
|
|
79
|
+
|
|
80
|
+
console.print(table)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def show_emails_table(emails):
|
|
84
|
+
"""Displays a simple list of emails without unread counts."""
|
|
85
|
+
show_emails_with_unread(emails, {})
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def show_inbox_header(email):
|
|
89
|
+
"""Prints the inbox header for a given email."""
|
|
90
|
+
console.print(Panel(f"Inbox for: [bold green]{email}[/bold green]", expand=False))
|
|
91
|
+
console.print("[dim]Press Ctrl+C to go back to the email menu...[/dim]")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_inbox_table(messages, seen_ids, attachment_url_fn):
|
|
95
|
+
"""Builds a Rich table from fetched messages, filtering seen IDs.
|
|
96
|
+
|
|
97
|
+
Returns (table, has_new_messages, new_count).
|
|
98
|
+
"""
|
|
99
|
+
table = Table(title="[bold blue]Inbox[/bold blue]")
|
|
100
|
+
table.add_column("From", style="cyan")
|
|
101
|
+
table.add_column("To", style="cyan")
|
|
102
|
+
table.add_column("Subject", style="magenta")
|
|
103
|
+
table.add_column("Body", style="white")
|
|
104
|
+
table.add_column("Attachments", style="yellow")
|
|
105
|
+
|
|
106
|
+
new_count = 0
|
|
107
|
+
for msg in messages:
|
|
108
|
+
msg_id = str(msg["id"])
|
|
109
|
+
if msg_id in seen_ids:
|
|
110
|
+
continue
|
|
111
|
+
new_count += 1
|
|
112
|
+
seen_ids.add(msg_id)
|
|
113
|
+
|
|
114
|
+
attachments = []
|
|
115
|
+
if msg.get("attachments"):
|
|
116
|
+
for att in msg["attachments"]:
|
|
117
|
+
download_url = attachment_url_fn(att["id"])
|
|
118
|
+
attachments.append(f"[link={download_url}]{att['name']}[/link]")
|
|
119
|
+
|
|
120
|
+
body = msg["body_text"]
|
|
121
|
+
if len(body) > 100:
|
|
122
|
+
body = body[:100] + "..."
|
|
123
|
+
|
|
124
|
+
table.add_row(
|
|
125
|
+
msg["from"],
|
|
126
|
+
msg["to"],
|
|
127
|
+
msg["subject"],
|
|
128
|
+
body,
|
|
129
|
+
"\n".join(attachments) if attachments else "None",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return table, new_count > 0, new_count
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def show_full_message(msg):
|
|
136
|
+
"""Displays a single message with full body in a Panel."""
|
|
137
|
+
header = (
|
|
138
|
+
f"[bold]From:[/bold] {msg['from']}\n"
|
|
139
|
+
f"[bold]To:[/bold] {msg['to']}\n"
|
|
140
|
+
f"[bold]Subject:[/bold] {msg['subject']}\n"
|
|
141
|
+
)
|
|
142
|
+
body = msg["body_text"]
|
|
143
|
+
content = f"{header}\n{body}"
|
|
144
|
+
|
|
145
|
+
if msg.get("attachments"):
|
|
146
|
+
att_names = ", ".join(a["name"] for a in msg["attachments"])
|
|
147
|
+
content += f"\n\n[bold]Attachments:[/bold] {att_names}"
|
|
148
|
+
|
|
149
|
+
console.print(Panel(content, title="[bold cyan]Message[/bold cyan]", border_style="cyan"))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def show_messages_selection(messages):
|
|
153
|
+
"""Displays an indexed table of messages for selection.
|
|
154
|
+
|
|
155
|
+
Returns list of (index, message) tuples.
|
|
156
|
+
"""
|
|
157
|
+
table = Table(title="[bold blue]Messages[/bold blue]")
|
|
158
|
+
table.add_column("Idx", style="cyan")
|
|
159
|
+
table.add_column("From", style="cyan")
|
|
160
|
+
table.add_column("Subject", style="magenta")
|
|
161
|
+
table.add_column("Attachments", style="yellow")
|
|
162
|
+
|
|
163
|
+
for i, msg in enumerate(messages, 1):
|
|
164
|
+
att_count = len(msg.get("attachments") or [])
|
|
165
|
+
att_text = f"{att_count} file(s)" if att_count else "None"
|
|
166
|
+
table.add_row(str(i), msg["from"], msg["subject"], att_text)
|
|
167
|
+
|
|
168
|
+
console.print(table)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def ask_choice(prompt_text, choices, default="1", **kwargs):
|
|
172
|
+
"""Prompts the user to make a choice from a list."""
|
|
173
|
+
return Prompt.ask(prompt_text, choices=choices, default=default, **kwargs)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def wait_for_enter():
|
|
177
|
+
"""Waits for the user to press Enter."""
|
|
178
|
+
Prompt.ask("[bold]Press Enter to go back...[/bold]")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def show_error(message):
|
|
182
|
+
"""Prints an error message in red."""
|
|
183
|
+
console.print(f"[bold red]{message}[/bold red]")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def show_success(message):
|
|
187
|
+
"""Prints a success message in green."""
|
|
188
|
+
console.print(f"[bold green]{message}[/bold green]")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def show_info(message):
|
|
192
|
+
"""Prints an informational message in yellow."""
|
|
193
|
+
console.print(f"[bold yellow]{message}[/bold yellow]")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tmail-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool for generating temporary email addresses and viewing inboxes
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: pyfiglet
|
|
9
|
+
Requires-Dist: pyperclip
|
|
10
|
+
Requires-Dist: requests
|
|
11
|
+
Requires-Dist: rich
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="https://img.shields.io/badge/tmail-v0.1.0-blue" alt="version">
|
|
16
|
+
<img src="https://img.shields.io/badge/python-%3E%3D3.10-green" alt="python">
|
|
17
|
+
<img src="https://img.shields.io/badge/license-MIT-yellow" alt="license">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<h1 align="center">📧 tmail</h1>
|
|
21
|
+
<p align="center"><em>Disposable email addresses, right from your terminal.</em></p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
tmail is a lightweight CLI tool that generates temporary email addresses
|
|
26
|
+
and live-polls their inbox. Built with Python and [Rich](https://github.com/Textualize/rich).
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install uv
|
|
32
|
+
uv tool install tmail
|
|
33
|
+
tmail # interactive TUI
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## CLI flags
|
|
37
|
+
|
|
38
|
+
| Command | Description |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `tmail` | Open the interactive TUI menu |
|
|
41
|
+
| `tmail -g` | Generate a new email, print it, copy to clipboard |
|
|
42
|
+
| `tmail -g -w` | Generate and immediately watch the new inbox |
|
|
43
|
+
| `tmail -g -i` | Generate and fetch inbox once (one-shot) |
|
|
44
|
+
| `tmail -l` | List saved emails with unread counts |
|
|
45
|
+
| `tmail -i <email>` | Fetch inbox once (plain text, good for scripts) |
|
|
46
|
+
| `tmail -w <email>` | Live-poll inbox (plain text, Ctrl+C to stop) |
|
|
47
|
+
| `tmail -d <email>` | Delete an email from history |
|
|
48
|
+
| `tmail -n <seconds>` | Set poll interval |
|
|
49
|
+
| `tmail -c` | Clear all stored data |
|
|
50
|
+
| `tmail -V` | Show version |
|
|
51
|
+
| `tmail -h` | Show help |
|
|
52
|
+
|
|
53
|
+
## Documentation
|
|
54
|
+
|
|
55
|
+
| Doc | What's inside |
|
|
56
|
+
|---|---|
|
|
57
|
+
| [User Guide](docs/guide.md) | Full feature walkthrough, menu reference, settings, clipboard, attachments |
|
|
58
|
+
| [Development Guide](docs/development.md) | Architecture, module breakdown, code examples, adding features |
|
|
59
|
+
|
|
60
|
+
## Data
|
|
61
|
+
|
|
62
|
+
All data is stored in `.tmail_data/` as JSON files — no database required.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
tmail/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
tmail/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
|
|
3
|
+
tmail/api.py,sha256=jwhnmNkZLAHGG0GNM11pZ2iZXq7PDwG0RmEWq7W2zoc,1238
|
|
4
|
+
tmail/cli.py,sha256=4pQuKS4MYLEALPpLEUeIvRq7mFCJarPWjmDgLC0mhlc,17335
|
|
5
|
+
tmail/storage.py,sha256=_DPCCwsB77hfcvdsXsZ-AebTkYo4uCNQV2BUNBuuIsI,3906
|
|
6
|
+
tmail/ui.py,sha256=gV2XfvXwutYvbM152Hm4iz7bAIyFvPNK94WcLNkpcFk,5954
|
|
7
|
+
tmail_cli-0.1.0.dist-info/METADATA,sha256=IloI3kroziPTFSivx_5tQDeLsItBflXNPsigYF9UuQk,2030
|
|
8
|
+
tmail_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
9
|
+
tmail_cli-0.1.0.dist-info/entry_points.txt,sha256=XPP86TGHc5338jEDKUKm_j99fESb6LgY8T95lYzKCPY,41
|
|
10
|
+
tmail_cli-0.1.0.dist-info/licenses/LICENSE,sha256=TX9sLhh2IQXkFJ_xzF4GJuTPcW29clNZ8YVPVVIXw-M,1066
|
|
11
|
+
tmail_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 rkriad585
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|