@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 +0 -0
- package/.gitattributes +2 -0
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/assets/image.png +0 -0
- package/bin/index.js +67 -0
- package/package.json +37 -0
- package/src/api.js +112 -0
- package/src/config.js +88 -0
- package/src/tui.js +296 -0
package/.env
ADDED
|
File without changes
|
package/.gitattributes
ADDED
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
|
+

|
|
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)
|
package/assets/image.png
ADDED
|
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
|
+
}
|