@syren0914/tempmail-cli 1.0.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.
package/.env ADDED
File without changes
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kira
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.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # tempMail-cli
2
+
3
+ A tiny, single-file CLI to spin up a temporary email inbox, watch it live in a terminal UI, copy the address, preview messages (From/Subject/Date + body), and nuke the inbox when you're done.
4
+
5
+ ![TempMail CLI Screenshot](assets/image.png)
6
+
7
+ ## Quick Start
8
+
9
+ **Create & watch an inbox:** `tempmail create`
10
+
11
+ **Delete last inbox:** `tempmail delete`
12
+
13
+ Mouse support (click to select) or ↑/↓ + Enter
14
+
15
+ Press `c` anytime to copy the inbox address to your clipboard
16
+
17
+ ## Features
18
+
19
+ - **One-liner UX:** `tempmail create`
20
+ - **Live TUI** (polling every 2s by default)
21
+ - **Preview header** shows From (full email), Subject, Date
22
+ - **Clipboard support:** Windows/WSL (clip.exe), macOS (pbcopy), Wayland (wl-copy), X11 (xclip/xsel)
23
+ - **Safe delete** with `tempmail delete`
24
+ - **Works on** Linux, macOS, and WSL
25
+
26
+ ## Requirements
27
+
28
+ - **Python 3.8+**
29
+ - **TempMail.so via RapidAPI:**
30
+ - `RAPIDAPI_KEY`
31
+ - `TEMPMAIL_TOKEN`
32
+ - Main build uses `requests` (see "Install")
33
+ - No-dependency variant available (`tempmail-np`) that uses only the Python stdlib.
34
+
35
+ ## Install
36
+
37
+ ### Option A — Distro package (Debian/Ubuntu/WSL)
38
+ ```bash
39
+ sudo apt-get update
40
+ sudo apt-get install -y python3-requests
41
+ ```
42
+
43
+ ### Option B — Virtualenv (any OS)
44
+ ```bash
45
+ python3 -m venv ~/.venvs/tempmail
46
+ source ~/.venvs/tempmail/bin/activate
47
+ pip install requests
48
+ # (optional) deactivate when done
49
+ ```
50
+
51
+ ### Put the CLI on your PATH
52
+ ```bash
53
+ mkdir -p ~/.local/bin
54
+ cp tempmail ~/.local/bin/tempmail
55
+ chmod +x ~/.local/bin/tempmail
56
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
57
+ source ~/.bashrc
58
+ ```
59
+
60
+ **Windows/WSL note:** if you copied the file from `C:\...`, convert line endings just in case:
61
+ ```bash
62
+ sed -i 's/\r$//' ~/.local/bin/tempmail
63
+ ```
64
+
65
+ ## Credentials (env vars)
66
+
67
+ ### Getting Your Tokens
68
+
69
+ To use this CLI, you'll need to obtain API credentials from [TempMail.so](https://tempmail.so):
70
+
71
+ 1. **RAPIDAPI_KEY**: Get this from RapidAPI when you subscribe to the TempMail.so API
72
+ 2. **TEMPMAIL_TOKEN**: Get this from your TempMail.so account dashboard
73
+
74
+ Visit [https://tempmail.so](https://tempmail.so) to sign up and get your tokens.
75
+
76
+ ### Setting Up Environment Variables
77
+
78
+ ```bash
79
+ # temporary for the current shell
80
+ export RAPIDAPI_KEY="YOUR_RAPIDAPI_KEY"
81
+ export TEMPMAIL_TOKEN="YOUR_TEMPMAIL_ACCOUNT_TOKEN"
82
+
83
+ # or persist for every shell:
84
+ cat > ~/.tempmail_env <<'EOF'
85
+ export RAPIDAPI_KEY="YOUR_RAPIDAPI_KEY"
86
+ export TEMPMAIL_TOKEN="YOUR_TEMPMAIL_ACCOUNT_TOKEN"
87
+ EOF
88
+ chmod 600 ~/.tempmail_env
89
+ echo 'source ~/.tempmail_env' >> ~/.bashrc
90
+ source ~/.bashrc
91
+ ```
92
+
93
+ ## Usage
94
+
95
+ ```bash
96
+ # Create inbox, auto-copy address, open live TUI
97
+ tempmail create
98
+
99
+ # Useful options:
100
+ # --minutes 10 lifespan (default 10)
101
+ # --prefix mybox custom local-part (random if omitted)
102
+ # --domain example.com choose a specific domain (first available if omitted)
103
+ # --interval 2 poll seconds (default 2)
104
+ tempmail create --minutes 5 --prefix myproj --interval 2
105
+
106
+ # Delete the last-created inbox
107
+ tempmail delete
108
+ ```
109
+
110
+ ## TUI controls
111
+
112
+ - **Mouse:** click a message to preview
113
+ - **Keyboard:** ↑/↓ select, Enter refresh preview
114
+ - `r` = refresh, `c` = copy inbox address, `d` = delete inbox, `q` = quit
115
+
116
+ ## No-dependency build (optional)
117
+
118
+ If you can't install `requests`, use the stdlib-only binary:
119
+
120
+ ```bash
121
+ cp tempmail-np ~/.local/bin/tempmail
122
+ chmod +x ~/.local/bin/tempmail
123
+ tempmail create
124
+ ```
125
+
126
+ ## Troubleshooting
127
+
128
+ ### `import: command not found` or `$'\r'` errors
129
+ The file likely has Windows CRLF line endings or a broken shebang. Fix:
130
+
131
+ ```bash
132
+ sed -i 's/\r$//' ~/.local/bin/tempmail
133
+ sed -i '1s|^.*$|#!/usr/bin/env python3|' ~/.local/bin/tempmail
134
+ chmod +x ~/.local/bin/tempmail
135
+ ```
136
+
137
+ As a fallback, wrap it:
138
+ ```bash
139
+ mv ~/.local/bin/tempmail ~/.local/bin/tempmail.py
140
+ printf '#!/usr/bin/env bash\nexec python3 "$HOME/.local/bin/tempmail.py" "$@"\n' > ~/.local/bin/tempmail
141
+ chmod +x ~/.local/bin/tempmail ~/.local/bin/tempmail.py
142
+ ```
143
+
144
+ ### Clipboard not copying
145
+ Install a helper:
146
+ - **Wayland:** `sudo apt-get install wl-clipboard` → `wl-copy`
147
+ - **X11:** `sudo apt-get install xclip` or `xsel`
148
+ - **WSL/Windows:** ensure `clip.exe` exists (it does on recent Windows)
149
+
150
+ ### Mouse doesn't select
151
+ Use ↑/↓ + Enter. (Some terminals don't forward mouse events.)
152
+
153
+ ## Project layout
154
+
155
+ ```
156
+ tempmail-cli/
157
+ ├─ tempmail # main Python script (executable)
158
+ ├─ tempmail-np # optional no-deps variant (urllib only)
159
+ ├─ README.md
160
+ ├─ LICENSE # MIT
161
+ ├─ requirements.txt # contains: requests
162
+ ├─ assets/
163
+ │ └─ screenshot.png # add your screenshot here
164
+ └─ .gitignore
165
+ ```
166
+
167
+ ### requirements.txt
168
+ ```
169
+ requests
170
+ ```
171
+
172
+ ### .gitignore
173
+ ```
174
+ __pycache__/
175
+ *.pyc
176
+ .venv/
177
+ .DS_Store
178
+ ```
179
+
180
+ ## Roadmap
181
+
182
+ - [ ] `y` to copy selected mail's text to clipboard
183
+ - [ ] Save/preview attachments
184
+ - [ ] Export message as `.eml`
185
+
186
+ ## License
187
+
188
+ MIT — see [LICENSE](LICENSE)
Binary file
package/bin/index.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { ensureConfig, clearConfig } from '../src/config.js';
6
+ import { createInbox } from '../src/api.js';
7
+ import { InboxUI } from '../src/tui.js';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('tempmail')
13
+ .description('A tiny CLI to spin up a temporary email inbox')
14
+ .version('1.0.0');
15
+
16
+ program
17
+ .command('create')
18
+ .description('Create inbox and open live view')
19
+ .option('-m, --minutes <number>', 'lifespan in minutes', parseFloat, 10)
20
+ .option('-p, --prefix <string>', 'local-part (random if omitted)')
21
+ .option('-d, --domain <string>', 'domain (first available if omitted)')
22
+ .action(async (options) => {
23
+ try {
24
+ await ensureConfig();
25
+ const inbox = await createInbox(options.prefix, options.domain, options.minutes);
26
+
27
+ console.log(chalk.green(`\nCreated inbox: ${chalk.bold(inbox.email)}`));
28
+ console.log(chalk.gray(`Opening live view... (q to quit, d to delete, c to copy address)\n`));
29
+
30
+ const ui = new InboxUI(inbox);
31
+ ui.run();
32
+ } catch (error) {
33
+ if (error.message.includes('401')) {
34
+ await ensureConfig(true);
35
+ // Retry once
36
+ try {
37
+ const inbox = await createInbox(options.prefix, options.domain, options.minutes);
38
+ console.log(chalk.green(`\nCreated inbox: ${chalk.bold(inbox.email)}`));
39
+ const ui = new InboxUI(inbox);
40
+ ui.run();
41
+ } catch (retryError) {
42
+ console.error(chalk.red('\nRetry failed:'), retryError.message);
43
+ process.exit(1);
44
+ }
45
+ } else {
46
+ console.error(chalk.red('\nError:'), error.message);
47
+ process.exit(1);
48
+ }
49
+ }
50
+ });
51
+
52
+ program
53
+ .command('config')
54
+ .description('Manage configuration')
55
+ .option('--clear', 'Clear saved tokens')
56
+ .action(async (options) => {
57
+ if (options.clear) {
58
+ clearConfig();
59
+ } else {
60
+ const cfg = await ensureConfig();
61
+ console.log(chalk.cyan('Current Configuration:'));
62
+ console.log(`RAPIDAPI_KEY: ${cfg.rk ? chalk.green('SET') : chalk.red('NOT SET')}`);
63
+ console.log(`TEMPMAIL_TOKEN: ${cfg.tk ? chalk.green('SET') : chalk.red('NOT SET')}`);
64
+ }
65
+ });
66
+
67
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@syren0914/tempmail-cli",
3
+ "version": "1.0.0",
4
+ "description": "A tiny, single-file CLI to spin up a temporary email inbox, watch it live in a terminal UI.",
5
+ "type": "module",
6
+ "main": "bin/index.js",
7
+ "bin": {
8
+ "tempmail": "./bin/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/index.js",
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Syren0914/tempMail-cli.git"
17
+ },
18
+ "keywords": [
19
+ "tempmail",
20
+ "cli",
21
+ "email",
22
+ "temporary"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "axios": "^1.13.5",
28
+ "blessed": "^0.1.81",
29
+ "chalk": "^5.6.2",
30
+ "clipboardy": "^5.3.0",
31
+ "commander": "^14.0.3",
32
+ "conf": "^15.1.0",
33
+ "dotenv": "^17.3.1",
34
+ "inquirer": "^13.2.2",
35
+ "open": "^11.0.0"
36
+ }
37
+ }
package/src/api.js ADDED
@@ -0,0 +1,112 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from './config.js';
3
+
4
+ const BASE = 'https://tempmail-so.p.rapidapi.com';
5
+
6
+ async function api(method, path, options = {}) {
7
+ const { rk, tk } = getConfig();
8
+ const url = `${BASE}${path}`;
9
+
10
+ const { headers: extraHeaders, ...restOptions } = options;
11
+ const headers = {
12
+ 'x-rapidapi-key': rk,
13
+ 'Authorization': `Bearer ${tk}`,
14
+ ...extraHeaders
15
+ };
16
+
17
+ try {
18
+ const response = await axios({
19
+ method,
20
+ url,
21
+ headers,
22
+ ...restOptions
23
+ });
24
+ return response.data;
25
+ } catch (error) {
26
+ if (error.response) {
27
+ throw new Error(`HTTP ${error.response.status}: ${JSON.stringify(error.response.data)}`);
28
+ }
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ export async function listDomains() {
34
+ const out = await api('GET', '/domains');
35
+ const data = out.data || out;
36
+ return data.map(item => item.name || item.domain).filter(Boolean);
37
+ }
38
+
39
+ export async function createInbox(prefix, domain, minutes = 10) {
40
+ if (!domain) {
41
+ const domains = await listDomains();
42
+ if (!domains.length) throw new Error('No domains available');
43
+ domain = domains[0];
44
+ }
45
+
46
+ if (!prefix) {
47
+ prefix = 'tm' + Math.random().toString(36).substring(2, 9);
48
+ }
49
+
50
+ const lifespan = Math.round(minutes * 60);
51
+ const data = new URLSearchParams({
52
+ address: prefix,
53
+ name: prefix,
54
+ domain,
55
+ lifespan: lifespan.toString()
56
+ });
57
+
58
+ const out = await api('POST', '/inboxes', {
59
+ data,
60
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
61
+ });
62
+
63
+ const payload = out.data || out;
64
+ return {
65
+ inbox_id: payload.id,
66
+ email: `${prefix}@${domain}`,
67
+ created: Math.floor(Date.now() / 1000)
68
+ };
69
+ }
70
+
71
+ export async function deleteInbox(inbox_id) {
72
+ return api('DELETE', `/inboxes/${inbox_id}`);
73
+ }
74
+
75
+ export async function listMails(inbox_id) {
76
+ const out = await api('GET', `/inboxes/${inbox_id}/mails`);
77
+ const payload = out.data || out;
78
+ let data = [];
79
+
80
+ if (Array.isArray(payload)) {
81
+ data = payload;
82
+ } else if (typeof payload === 'object') {
83
+ for (const k of ['mails', 'items', 'rows', 'list', 'data']) {
84
+ if (Array.isArray(payload[k])) {
85
+ data = payload[k];
86
+ break;
87
+ }
88
+ }
89
+ }
90
+
91
+ return data.map(m => ({
92
+ id: m.id || m._id,
93
+ subject: m.subject || m.title || '(no subject)',
94
+ from: m.from || m.sender || '',
95
+ received: m.received || m.date || ''
96
+ }));
97
+ }
98
+
99
+ export async function readMail(inbox_id, mail_id) {
100
+ const out = await api('GET', `/inboxes/${inbox_id}/mails/${mail_id}`);
101
+ return out.data || out;
102
+ }
103
+
104
+ export async function downloadAttachment(inbox_id, mail_id, attachment_id) {
105
+ // RapidAPI TempMail.so usually returns attachments inside the mail object or via a dedicated path.
106
+ // We'll use the GET /inboxes/{id}/mails/{mid}/attachments/{aid} pattern if supported,
107
+ // or return the buffer if it's already in the mail object.
108
+ const out = await api('GET', `/inboxes/${inbox_id}/mails/${mail_id}/attachments/${attachment_id}`, {
109
+ responseType: 'arraybuffer'
110
+ });
111
+ return out;
112
+ }
package/src/config.js ADDED
@@ -0,0 +1,88 @@
1
+ import Conf from 'conf';
2
+ import open from 'open';
3
+ import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+ import dotenv from 'dotenv';
6
+
7
+ dotenv.config();
8
+
9
+ const schema = {
10
+ rapidapiKey: {
11
+ type: 'string',
12
+ default: process.env.RAPIDAPI_KEY || ''
13
+ },
14
+ tempmailToken: {
15
+ type: 'string',
16
+ default: process.env.TEMPMAIL_TOKEN || ''
17
+ }
18
+ };
19
+
20
+ const config = new Conf({ projectName: '@syren0914/tempmail-cli', schema });
21
+
22
+ export async function ensureConfig(force = false) {
23
+ let rk = config.get('rapidapiKey') || process.env.RAPIDAPI_KEY;
24
+ let tk = config.get('tempmailToken') || process.env.TEMPMAIL_TOKEN;
25
+
26
+ if (!rk || !tk || force) {
27
+ if (force) {
28
+ console.log(chalk.red('\n🚫 Authentication failed with current tokens.'));
29
+ } else {
30
+ console.log(chalk.yellow('\n⚠️ API configuration missing.'));
31
+ }
32
+ console.log(chalk.cyan('You need a RapidAPI Key and a TempMail.so Token to use this CLI.'));
33
+
34
+ const { setup } = await inquirer.prompt([
35
+ {
36
+ type: 'confirm',
37
+ name: 'setup',
38
+ message: 'Would you like to open tempmail.so to get your tokens?',
39
+ default: true
40
+ }
41
+ ]);
42
+
43
+ if (setup) {
44
+ await open('https://tempmail.so/mailboxes');
45
+ console.log(chalk.green('\nOpening browser... Login and go to Account -> Account Information to find your token.'));
46
+ }
47
+
48
+ const answers = await inquirer.prompt([
49
+ {
50
+ type: 'input',
51
+ name: 'rk',
52
+ message: 'Enter your RAPIDAPI_KEY:',
53
+ validate: input => input.length > 0 || 'Key is required'
54
+ },
55
+ {
56
+ type: 'input',
57
+ name: 'tk',
58
+ message: 'Enter your TEMPMAIL_TOKEN:',
59
+ validate: input => input.length > 0 || 'Token is required'
60
+ }
61
+ ]);
62
+
63
+ config.set('rapidapiKey', answers.rk);
64
+ config.set('tempmailToken', answers.tk);
65
+
66
+ // Update current process env as well
67
+ process.env.RAPIDAPI_KEY = answers.rk;
68
+ process.env.TEMPMAIL_TOKEN = answers.tk;
69
+
70
+ rk = answers.rk;
71
+ tk = answers.tk;
72
+ console.log(chalk.green('✅ Configuration saved!\n'));
73
+ }
74
+
75
+ return { rk, tk };
76
+ }
77
+
78
+ export function getConfig() {
79
+ return {
80
+ rk: config.get('rapidapiKey') || process.env.RAPIDAPI_KEY,
81
+ tk: config.get('tempmailToken') || process.env.TEMPMAIL_TOKEN
82
+ };
83
+ }
84
+
85
+ export function clearConfig() {
86
+ config.clear();
87
+ console.log(chalk.red('Configuration cleared.'));
88
+ }
package/src/tui.js ADDED
@@ -0,0 +1,296 @@
1
+ import blessed from 'blessed';
2
+ import clipboardy from 'clipboardy';
3
+ import chalk from 'chalk';
4
+ import open from 'open';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ import { listMails, readMail, deleteInbox, downloadAttachment } from './api.js';
9
+
10
+ export class InboxUI {
11
+ constructor(inbox) {
12
+ this.inbox = inbox;
13
+ this.screen = blessed.screen({
14
+ smartCSR: true,
15
+ title: 'TempMail-cli',
16
+ fullUnicode: true
17
+ });
18
+
19
+ this.mails = [];
20
+ this.selected = 0;
21
+ this.previewCache = new Map();
22
+ this.currentAttachments = [];
23
+
24
+ this.layout();
25
+ this.setupKeys();
26
+ this.setupFlash();
27
+ }
28
+
29
+ layout() {
30
+ this.header = blessed.box({
31
+ parent: this.screen,
32
+ top: 0,
33
+ left: 0,
34
+ width: '100%',
35
+ height: 1,
36
+ content: ` TempMail: {bold}${this.inbox.email}{/bold} ({yellow-fg}v{/yellow-fg}=view img, {yellow-fg}s{/yellow-fg}=save all, {yellow-fg}q{/yellow-fg}=quit, {yellow-fg}r{/yellow-fg}=refresh, {yellow-fg}d{/yellow-fg}=delete, {yellow-fg}c{/yellow-fg}=copy)`,
37
+ tags: true,
38
+ style: { bg: 'blue', fg: 'white' }
39
+ });
40
+
41
+ const listWidth = '45%';
42
+
43
+ this.inboxList = blessed.list({
44
+ parent: this.screen,
45
+ label: ' Inbox ',
46
+ top: 1,
47
+ left: 0,
48
+ width: listWidth,
49
+ height: '100%-2',
50
+ border: { type: 'line' },
51
+ style: {
52
+ selected: { bg: 'cyan', fg: 'black' },
53
+ label: { fg: 'white' }
54
+ },
55
+ keys: true,
56
+ mouse: true
57
+ });
58
+
59
+ this.preview = blessed.box({
60
+ parent: this.screen,
61
+ label: ' Preview ',
62
+ top: 1,
63
+ left: listWidth,
64
+ width: '100%-45%',
65
+ height: '100%-2',
66
+ border: { type: 'line' },
67
+ padding: { left: 1, right: 1 },
68
+ tags: true,
69
+ scrollable: true,
70
+ alwaysScroll: true,
71
+ scrollbar: { ch: ' ', style: { bg: 'white' } },
72
+ mouse: true
73
+ });
74
+
75
+ this.footer = blessed.box({
76
+ parent: this.screen,
77
+ bottom: 0,
78
+ left: 0,
79
+ width: '100%',
80
+ height: 1,
81
+ content: '',
82
+ style: { fg: 'yellow' }
83
+ });
84
+ }
85
+
86
+ setupKeys() {
87
+ this.screen.key(['q', 'C-c', 'escape'], () => this.screen.destroy());
88
+
89
+ this.screen.key(['r'], () => this.refresh());
90
+
91
+ this.screen.key(['c'], () => {
92
+ try {
93
+ clipboardy.writeSync(this.inbox.email);
94
+ this.flash('Address copied to clipboard!');
95
+ } catch (e) {
96
+ this.flash('Copy failed: ' + e.message);
97
+ }
98
+ });
99
+
100
+ this.screen.key(['v'], () => this.viewAttachment());
101
+ this.screen.key(['s'], () => this.saveAttachments());
102
+
103
+ this.screen.key(['d'], async () => {
104
+ const confirm = blessed.question({
105
+ parent: this.screen,
106
+ top: 'center',
107
+ left: 'center',
108
+ width: 'shrink',
109
+ height: 'shrink',
110
+ label: ' Confirm ',
111
+ border: 'line',
112
+ });
113
+
114
+ confirm.ask('Delete this inbox?', async (err, value) => {
115
+ if (value) {
116
+ try {
117
+ await deleteInbox(this.inbox.inbox_id);
118
+ this.screen.destroy();
119
+ process.exit(0);
120
+ } catch (e) {
121
+ this.flash('Delete failed: ' + e.message);
122
+ }
123
+ }
124
+ });
125
+ });
126
+
127
+ this.inboxList.on('select item', (item, index) => {
128
+ this.selected = index;
129
+ this.updatePreview();
130
+ });
131
+
132
+ this.inboxList.key(['enter'], () => {
133
+ const mail = this.mails[this.selected];
134
+ if (mail) {
135
+ this.previewCache.delete(mail.id);
136
+ this.updatePreview();
137
+ }
138
+ });
139
+ }
140
+
141
+ setupFlash() {
142
+ this.flashTimeout = null;
143
+ }
144
+
145
+ flash(msg) {
146
+ this.footer.setContent(' ' + msg);
147
+ this.screen.render();
148
+ if (this.flashTimeout) clearTimeout(this.flashTimeout);
149
+ this.flashTimeout = setTimeout(() => {
150
+ this.footer.setContent('');
151
+ this.screen.render();
152
+ }, 3000);
153
+ }
154
+
155
+ async refresh() {
156
+ try {
157
+ this.flash('Fetching mails...');
158
+ const ms = await listMails(this.inbox.inbox_id);
159
+ this.mails = ms;
160
+
161
+ const items = ms.map((m, i) => {
162
+ const subj = m.subject.substring(0, 30).padEnd(30);
163
+ return `${i+1}. ${subj} ← ${m.from.substring(0, 20)}`;
164
+ });
165
+
166
+ this.inboxList.setItems(items);
167
+ if (items.length > 0) {
168
+ this.inboxList.select(this.selected);
169
+ }
170
+ this.updatePreview();
171
+ this.screen.render();
172
+ } catch (e) {
173
+ this.flash('Error: ' + e.message);
174
+ }
175
+ }
176
+
177
+ async updatePreview() {
178
+ const mail = this.mails[this.selected];
179
+ if (!mail) {
180
+ this.preview.setContent('{center}No mails selected{/center}');
181
+ this.screen.render();
182
+ return;
183
+ }
184
+
185
+ let cached = this.previewCache.get(mail.id);
186
+ if (!cached) {
187
+ try {
188
+ const full = await readMail(this.inbox.inbox_id, mail.id);
189
+ const body = full.textContent || stripHtml(full.htmlContent) || '(no content)';
190
+
191
+ this.currentAttachments = full.attachments || [];
192
+ let attachStr = '';
193
+ if (this.currentAttachments.length > 0) {
194
+ attachStr = `\n\n{yellow-fg}{bold}Attachments (${this.currentAttachments.length}):{/bold}{/yellow-fg}\n`;
195
+ this.currentAttachments.forEach((a, i) => {
196
+ attachStr += ` - ${a.filename || a.name || 'unnamed'} (${formatSize(a.size)})\n`;
197
+ });
198
+ attachStr += `\n{gray-fg}Press 'v' to view first image, 's' to save all{/gray-fg}`;
199
+ }
200
+
201
+ cached = `{bold}From:{/bold} ${full.from || mail.from}\n{bold}Subject:{/bold} ${full.subject || mail.subject}\n{bold}Date:{/bold} ${full.received || mail.received}\n{blue-fg}${'-'.repeat(40)}{/blue-fg}\n\n${body}${attachStr}`;
202
+ this.previewCache.set(mail.id, cached);
203
+ } catch (e) {
204
+ cached = '{red-fg}Error loading mail: ' + e.message + '{/red-fg}';
205
+ }
206
+ }
207
+
208
+ this.preview.setContent(cached);
209
+ this.screen.render();
210
+ }
211
+
212
+ async viewAttachment() {
213
+ if (!this.currentAttachments.length) {
214
+ this.flash('No attachments in this email.');
215
+ return;
216
+ }
217
+
218
+ const img = this.currentAttachments.find(a => isImage(a.filename || a.name));
219
+ if (!img) {
220
+ this.flash('No image attachments found to view.');
221
+ return;
222
+ }
223
+
224
+ try {
225
+ this.flash('Downloading image to view...');
226
+ const mail = this.mails[this.selected];
227
+ const buffer = await downloadAttachment(this.inbox.inbox_id, mail.id, img.id);
228
+
229
+ const tmpDir = os.tmpdir();
230
+ const fileName = img.filename || img.name || 'temp_image.png';
231
+ const filePath = path.join(tmpDir, `tempmail_${Date.now()}_${fileName}`);
232
+
233
+ fs.writeFileSync(filePath, Buffer.from(buffer));
234
+ await open(filePath);
235
+ this.flash('Opened image in system viewer.');
236
+ } catch (e) {
237
+ this.flash('View failed: ' + e.message);
238
+ }
239
+ }
240
+
241
+ async saveAttachments() {
242
+ if (!this.currentAttachments.length) {
243
+ this.flash('No attachments to save.');
244
+ return;
245
+ }
246
+
247
+ try {
248
+ const saveDir = path.join(process.cwd(), 'attachments');
249
+ if (!fs.existsSync(saveDir)) fs.mkdirSync(saveDir);
250
+
251
+ this.flash(`Saving ${this.currentAttachments.length} attachments...`);
252
+ const mail = this.mails[this.selected];
253
+
254
+ for (const a of this.currentAttachments) {
255
+ const buffer = await downloadAttachment(this.inbox.inbox_id, mail.id, a.id);
256
+ const fileName = a.filename || a.name || `attachment_${a.id}`;
257
+ const filePath = path.join(saveDir, fileName);
258
+ fs.writeFileSync(filePath, Buffer.from(buffer));
259
+ }
260
+
261
+ this.flash(`Saved to ${path.relative(process.cwd(), saveDir)}/`);
262
+ } catch (e) {
263
+ this.flash('Save failed: ' + e.message);
264
+ }
265
+ }
266
+
267
+ run() {
268
+ this.refresh();
269
+ setInterval(() => this.refresh(), 5000); // Poll every 5s
270
+ this.screen.render();
271
+ }
272
+ }
273
+
274
+ function stripHtml(html) {
275
+ if (!html) return '';
276
+ return html
277
+ .replace(/<br\s*\/?>/gi, '\n')
278
+ .replace(/<\/p>/gi, '\n\n')
279
+ .replace(/<[^>]+>/g, '')
280
+ .replace(/\n{3,}/g, '\n\n')
281
+ .trim();
282
+ }
283
+
284
+ function formatSize(bytes) {
285
+ if (!bytes) return '0 B';
286
+ const k = 1024;
287
+ const sizes = ['B', 'KB', 'MB', 'GB'];
288
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
289
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
290
+ }
291
+
292
+ function isImage(filename) {
293
+ if (!filename) return false;
294
+ const ext = filename.split('.').pop().toLowerCase();
295
+ return ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
296
+ }