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 ADDED
File without changes
tmail/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tmail = tmail.cli:main
@@ -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.