fcemail 0.1.11
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/.github/workflows/release.yml +90 -0
- package/.goreleaser.yaml +83 -0
- package/LICENSE +21 -0
- package/README.md +226 -0
- package/bin/fce +0 -0
- package/cmd/root.go +678 -0
- package/go.mod +26 -0
- package/go.sum +49 -0
- package/internal/api/client.go +197 -0
- package/internal/auth/auth.go +204 -0
- package/internal/config/config.go +112 -0
- package/internal/display/logo.go +242 -0
- package/internal/ws/watch.go +144 -0
- package/logo.webp +0 -0
- package/main.go +7 -0
- package/package.json +17 -0
- package/scripts/install-binary.js +11 -0
- package/scripts/install.sh +65 -0
package/cmd/root.go
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"math/rand"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/DishIs/fce-cli/internal/api"
|
|
12
|
+
"github.com/DishIs/fce-cli/internal/auth"
|
|
13
|
+
"github.com/DishIs/fce-cli/internal/config"
|
|
14
|
+
"github.com/DishIs/fce-cli/internal/display"
|
|
15
|
+
"github.com/DishIs/fce-cli/internal/ws"
|
|
16
|
+
"github.com/spf13/cobra"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
var (
|
|
20
|
+
Version = "dev"
|
|
21
|
+
Commit = "none"
|
|
22
|
+
Date = "unknown"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// ── Root command ──────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
var rootCmd = &cobra.Command{
|
|
28
|
+
Use: "fce",
|
|
29
|
+
Short: "FreeCustom.Email CLI — disposable inboxes, OTP extraction, real-time email",
|
|
30
|
+
Long: `
|
|
31
|
+
◉ fce — FreeCustom.Email CLI
|
|
32
|
+
|
|
33
|
+
Manage disposable inboxes, extract OTPs, and stream
|
|
34
|
+
real-time email events from your terminal.
|
|
35
|
+
|
|
36
|
+
Get started:
|
|
37
|
+
fce login Authenticate with your account
|
|
38
|
+
fce watch random Watch a random inbox for emails
|
|
39
|
+
fce status View your account and plan
|
|
40
|
+
|
|
41
|
+
Docs: https://www.freecustom.email/api/cli
|
|
42
|
+
`,
|
|
43
|
+
SilenceUsage: true,
|
|
44
|
+
SilenceErrors: true,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func Execute() {
|
|
48
|
+
if err := rootCmd.Execute(); err != nil {
|
|
49
|
+
display.Error(err.Error())
|
|
50
|
+
os.Exit(1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func init() {
|
|
55
|
+
rootCmd.AddCommand(
|
|
56
|
+
loginCmd,
|
|
57
|
+
logoutCmd,
|
|
58
|
+
statusCmd,
|
|
59
|
+
usageCmd,
|
|
60
|
+
watchCmd,
|
|
61
|
+
inboxCmd,
|
|
62
|
+
messagesCmd,
|
|
63
|
+
otpCmd,
|
|
64
|
+
domainsCmd,
|
|
65
|
+
versionCmd,
|
|
66
|
+
devCmd,
|
|
67
|
+
updateCmd,
|
|
68
|
+
uninstallCmd,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── fce uninstall ─────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
var uninstallCmd = &cobra.Command{
|
|
75
|
+
Use: "uninstall",
|
|
76
|
+
Short: "Remove all local configuration and credentials",
|
|
77
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
78
|
+
fmt.Print("Are you sure you want to remove all local configuration and logout? (y/N): ")
|
|
79
|
+
var confirm string
|
|
80
|
+
fmt.Scanln(&confirm)
|
|
81
|
+
if strings.ToLower(confirm) != "y" {
|
|
82
|
+
display.Info("Aborted.")
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if err := config.Purge(); err != nil {
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
display.Success("Local configuration and credentials cleared.")
|
|
91
|
+
|
|
92
|
+
// Platform-specific instructions
|
|
93
|
+
display.Header("Next Steps")
|
|
94
|
+
fmt.Println("To completely remove the fce binary, run the command for your platform:")
|
|
95
|
+
fmt.Println()
|
|
96
|
+
fmt.Println(display.TableBadge("Homebrew", "brew uninstall fce"))
|
|
97
|
+
fmt.Println(display.TableBadge("Scoop", "scoop uninstall fce"))
|
|
98
|
+
fmt.Println(display.TableBadge("Choco", "choco uninstall fce"))
|
|
99
|
+
fmt.Println(display.TableBadge("NPM", "npm uninstall -g fce-cli"))
|
|
100
|
+
fmt.Println(display.TableBadge("Manual", "sudo rm /usr/local/bin/fce"))
|
|
101
|
+
fmt.Println()
|
|
102
|
+
|
|
103
|
+
return nil
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── fce dev ───────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
var devCmd = &cobra.Command{
|
|
110
|
+
Use: "dev",
|
|
111
|
+
Short: "Instantly register a dev inbox and start watching for emails",
|
|
112
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
113
|
+
client, err := requireAuth()
|
|
114
|
+
if err != nil {
|
|
115
|
+
return err
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
inbox := devInbox()
|
|
119
|
+
display.Info(fmt.Sprintf("Temporary inbox: %s", inbox))
|
|
120
|
+
|
|
121
|
+
if _, err := client.RegisterInbox(inbox); err != nil {
|
|
122
|
+
return fmt.Errorf("failed to register inbox: %w", err)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
display.Success("Watching for emails...")
|
|
126
|
+
fmt.Println()
|
|
127
|
+
|
|
128
|
+
return ws.Watch(client.GetAPIKey(), inbox)
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── fce update ────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
var updateCmd = &cobra.Command{
|
|
135
|
+
Use: "update",
|
|
136
|
+
Short: "Update the CLI to the latest version",
|
|
137
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
138
|
+
display.Info("Checking for updates...")
|
|
139
|
+
|
|
140
|
+
installCmd := "curl -fsSL https://raw.githubusercontent.com/DishIs/fce-cli/main/scripts/install.sh | sh"
|
|
141
|
+
fmt.Printf("Running installer: %s\n", installCmd)
|
|
142
|
+
|
|
143
|
+
// Execute the installation command
|
|
144
|
+
var c *exec.Cmd
|
|
145
|
+
if os.Getenv("OS") == "Windows_NT" {
|
|
146
|
+
display.Warn("Auto-update on Windows is currently best handled via Scoop or Chocolatey.")
|
|
147
|
+
fmt.Println("Try: scoop update fce OR choco upgrade fce")
|
|
148
|
+
return nil
|
|
149
|
+
} else {
|
|
150
|
+
c = exec.Command("sh", "-c", installCmd)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
c.Stdout = os.Stdout
|
|
154
|
+
c.Stderr = os.Stderr
|
|
155
|
+
if err := c.Run(); err != nil {
|
|
156
|
+
return fmt.Errorf("update failed: %w", err)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
display.Success("Update complete!")
|
|
160
|
+
return nil
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── fce version ───────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
var versionCmd = &cobra.Command{
|
|
167
|
+
Use: "version",
|
|
168
|
+
Short: "Show version info",
|
|
169
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
170
|
+
fmt.Printf("fce %s (%s) %s\n", Version, Commit, Date)
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── fce login ─────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
var loginCmd = &cobra.Command{
|
|
177
|
+
Use: "login",
|
|
178
|
+
Short: "Authenticate with your FreeCustom.Email account",
|
|
179
|
+
Long: `Opens your browser to complete authentication.
|
|
180
|
+
After logging in on the website, your API key is saved
|
|
181
|
+
securely in your OS keychain.`,
|
|
182
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
183
|
+
return auth.Login()
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── fce logout ────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
var logoutCmd = &cobra.Command{
|
|
190
|
+
Use: "logout",
|
|
191
|
+
Short: "Remove stored credentials",
|
|
192
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
193
|
+
return auth.Logout()
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── fce status ────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
var statusCmd = &cobra.Command{
|
|
200
|
+
Use: "status",
|
|
201
|
+
Short: "Show account info, plan, and inbox counts",
|
|
202
|
+
Example: ` fce status`,
|
|
203
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
204
|
+
client, err := requireAuth()
|
|
205
|
+
if err != nil {
|
|
206
|
+
return err
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
result, err := client.GetMe()
|
|
210
|
+
if err != nil {
|
|
211
|
+
return err
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
d, _ := result["data"].(map[string]interface{})
|
|
215
|
+
if d == nil {
|
|
216
|
+
d = result
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
plan := strVal(d, "plan")
|
|
220
|
+
planLabel := strVal(d, "plan_label")
|
|
221
|
+
price := strVal(d, "price")
|
|
222
|
+
credits := fmt.Sprintf("%v", intVal(d, "credits"))
|
|
223
|
+
apiInboxes := fmt.Sprintf("%v", intVal(d, "api_inbox_count"))
|
|
224
|
+
appInboxes := fmt.Sprintf("%v", intVal(d, "app_inbox_count"))
|
|
225
|
+
|
|
226
|
+
display.Header("Account")
|
|
227
|
+
display.Table([]display.Row{
|
|
228
|
+
{Key: "Plan", Value: planLabel + " " + display.PlanBadge(plan)},
|
|
229
|
+
{Key: "Price", Value: price},
|
|
230
|
+
{Key: "Credits", Value: credits + " remaining"},
|
|
231
|
+
{Key: "API inboxes", Value: apiInboxes},
|
|
232
|
+
{Key: "App inboxes", Value: appInboxes},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Cache plan in config
|
|
236
|
+
cfg, _ := config.LoadConfig()
|
|
237
|
+
cfg.Plan = plan
|
|
238
|
+
cfg.PlanLabel = planLabel
|
|
239
|
+
_ = config.SaveConfig(cfg)
|
|
240
|
+
|
|
241
|
+
return nil
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── fce usage ─────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
var usageCmd = &cobra.Command{
|
|
248
|
+
Use: "usage",
|
|
249
|
+
Short: "Show request usage for the current billing period",
|
|
250
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
251
|
+
client, err := requireAuth()
|
|
252
|
+
if err != nil {
|
|
253
|
+
return err
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
result, err := client.GetUsage()
|
|
257
|
+
if err != nil {
|
|
258
|
+
return err
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
d, _ := result["data"].(map[string]interface{})
|
|
262
|
+
if d == nil {
|
|
263
|
+
d = result
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
used := fmt.Sprintf("%v", d["requests_used"])
|
|
267
|
+
limit := fmt.Sprintf("%v", d["requests_limit"])
|
|
268
|
+
remaining := fmt.Sprintf("%v", d["requests_remaining"])
|
|
269
|
+
pct := strVal(d, "percent_used")
|
|
270
|
+
credits := fmt.Sprintf("%v", d["credits_remaining"])
|
|
271
|
+
resets := strVal(d, "resets")
|
|
272
|
+
|
|
273
|
+
display.Header("Usage")
|
|
274
|
+
display.Table([]display.Row{
|
|
275
|
+
{Key: "Requests used", Value: used + " / " + limit},
|
|
276
|
+
{Key: "Remaining", Value: remaining},
|
|
277
|
+
{Key: "Percent used", Value: pct},
|
|
278
|
+
{Key: "Credits remaining", Value: credits},
|
|
279
|
+
{Key: "Resets approx", Value: resets},
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
return nil
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── fce watch ─────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
var watchCmd = &cobra.Command{
|
|
289
|
+
Use: "watch [inbox|random]",
|
|
290
|
+
Short: "Stream emails in real time via WebSocket [Startup plan+]",
|
|
291
|
+
Long: `Connects to a WebSocket and streams incoming emails to your terminal.
|
|
292
|
+
|
|
293
|
+
fce watch Watch all registered inboxes
|
|
294
|
+
fce watch random Watch a new random inbox (auto-registers)
|
|
295
|
+
fce watch test@ditmail.info Watch a specific inbox
|
|
296
|
+
|
|
297
|
+
Requires Startup plan or above. Emails arrive in under 200ms.`,
|
|
298
|
+
Example: ` fce watch
|
|
299
|
+
fce watch random
|
|
300
|
+
fce watch mytest@ditube.info`,
|
|
301
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
302
|
+
client, err := requireAuth()
|
|
303
|
+
if err != nil {
|
|
304
|
+
return err
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Plan gate
|
|
308
|
+
me, err := client.GetMe()
|
|
309
|
+
if err != nil {
|
|
310
|
+
return err
|
|
311
|
+
}
|
|
312
|
+
d, _ := me["data"].(map[string]interface{})
|
|
313
|
+
if d == nil {
|
|
314
|
+
d = me
|
|
315
|
+
}
|
|
316
|
+
plan := strVal(d, "plan")
|
|
317
|
+
if !api.HasPlan(plan, api.PlanStartup) {
|
|
318
|
+
display.PlanGate("startup", "WebSocket watch")
|
|
319
|
+
return nil
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
apiKey := client.GetAPIKey()
|
|
323
|
+
mailbox := ""
|
|
324
|
+
|
|
325
|
+
if len(args) > 0 {
|
|
326
|
+
arg := args[0]
|
|
327
|
+
if arg == "random" {
|
|
328
|
+
// Generate a random inbox and register it
|
|
329
|
+
mailbox = randomInbox()
|
|
330
|
+
display.Info(fmt.Sprintf("Registering random inbox: %s", mailbox))
|
|
331
|
+
if _, err := client.RegisterInbox(mailbox); err != nil {
|
|
332
|
+
return fmt.Errorf("failed to register inbox: %w", err)
|
|
333
|
+
}
|
|
334
|
+
display.Success(fmt.Sprintf("Inbox ready: %s", mailbox))
|
|
335
|
+
fmt.Println()
|
|
336
|
+
} else {
|
|
337
|
+
mailbox = arg
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return ws.Watch(apiKey, mailbox)
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── fce inbox ─────────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
var inboxCmd = &cobra.Command{
|
|
348
|
+
Use: "inbox",
|
|
349
|
+
Short: "Manage registered inboxes",
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
var inboxListCmd = &cobra.Command{
|
|
353
|
+
Use: "list",
|
|
354
|
+
Aliases: []string{"ls"},
|
|
355
|
+
Short: "List all registered inboxes",
|
|
356
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
357
|
+
client, err := requireAuth()
|
|
358
|
+
if err != nil {
|
|
359
|
+
return err
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
inboxes, err := client.ListInboxes()
|
|
363
|
+
if err != nil {
|
|
364
|
+
return err
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if len(inboxes) == 0 {
|
|
368
|
+
display.Info("No inboxes registered.")
|
|
369
|
+
display.Info("Add one with: fce inbox add <address>")
|
|
370
|
+
return nil
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
display.Header(fmt.Sprintf("Inboxes (%d)", len(inboxes)))
|
|
374
|
+
items := make([]string, 0, len(inboxes))
|
|
375
|
+
for _, item := range inboxes {
|
|
376
|
+
if m, ok := item.(map[string]interface{}); ok {
|
|
377
|
+
items = append(items, strVal(m, "inbox"))
|
|
378
|
+
} else if s, ok := item.(string); ok {
|
|
379
|
+
items = append(items, s)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
display.List(items)
|
|
383
|
+
return nil
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
var inboxAddCmd = &cobra.Command{
|
|
388
|
+
Use: "add <address>",
|
|
389
|
+
Aliases: []string{"register"},
|
|
390
|
+
Short: "Register a new inbox",
|
|
391
|
+
Example: ` fce inbox add mytest@ditmail.info
|
|
392
|
+
fce inbox add random`,
|
|
393
|
+
Args: cobra.ExactArgs(1),
|
|
394
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
395
|
+
client, err := requireAuth()
|
|
396
|
+
if err != nil {
|
|
397
|
+
return err
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
inbox := args[0]
|
|
401
|
+
if inbox == "random" {
|
|
402
|
+
inbox = randomInbox()
|
|
403
|
+
display.Info(fmt.Sprintf("Generated inbox: %s", inbox))
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
result, err := client.RegisterInbox(inbox)
|
|
407
|
+
if err != nil {
|
|
408
|
+
return err
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
registered := strVal(result, "inbox")
|
|
412
|
+
if registered == "" {
|
|
413
|
+
registered = inbox
|
|
414
|
+
}
|
|
415
|
+
display.Success(fmt.Sprintf("Registered: %s", registered))
|
|
416
|
+
return nil
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
var inboxRemoveCmd = &cobra.Command{
|
|
421
|
+
Use: "remove <address>",
|
|
422
|
+
Aliases: []string{"rm", "delete", "unregister"},
|
|
423
|
+
Short: "Unregister an inbox",
|
|
424
|
+
Args: cobra.ExactArgs(1),
|
|
425
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
426
|
+
client, err := requireAuth()
|
|
427
|
+
if err != nil {
|
|
428
|
+
return err
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if _, err := client.UnregisterInbox(args[0]); err != nil {
|
|
432
|
+
return err
|
|
433
|
+
}
|
|
434
|
+
display.Success(fmt.Sprintf("Removed: %s", args[0]))
|
|
435
|
+
return nil
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
func init() {
|
|
440
|
+
inboxCmd.AddCommand(inboxListCmd, inboxAddCmd, inboxRemoveCmd)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── fce messages ──────────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
var messagesCmd = &cobra.Command{
|
|
446
|
+
Use: "messages <inbox> [id]",
|
|
447
|
+
Aliases: []string{"msgs", "mail"},
|
|
448
|
+
Short: "List messages in an inbox or view a specific message",
|
|
449
|
+
Example: ` fce messages mytest@ditmail.info
|
|
450
|
+
fce messages mytest@ditmail.info u7hPpV5sA`,
|
|
451
|
+
Args: cobra.MinimumNArgs(1),
|
|
452
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
453
|
+
client, err := requireAuth()
|
|
454
|
+
if err != nil {
|
|
455
|
+
return err
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
inbox := args[0]
|
|
459
|
+
|
|
460
|
+
// View specific message
|
|
461
|
+
if len(args) > 1 {
|
|
462
|
+
id := args[1]
|
|
463
|
+
msg, err := client.GetMessage(inbox, id)
|
|
464
|
+
if err != nil {
|
|
465
|
+
return err
|
|
466
|
+
}
|
|
467
|
+
display.MessageContent(msg)
|
|
468
|
+
return nil
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// List messages
|
|
472
|
+
msgs, err := client.ListMessages(inbox)
|
|
473
|
+
if err != nil {
|
|
474
|
+
return err
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if len(msgs) == 0 {
|
|
478
|
+
display.Info("No messages in this inbox.")
|
|
479
|
+
return nil
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
display.Header(fmt.Sprintf("Messages in %s (%d)", inbox, len(msgs)))
|
|
483
|
+
fmt.Println()
|
|
484
|
+
|
|
485
|
+
for _, item := range msgs {
|
|
486
|
+
m, ok := item.(map[string]interface{})
|
|
487
|
+
if !ok {
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
id := strVal(m, "id")
|
|
491
|
+
from := strVal(m, "from")
|
|
492
|
+
subject := strVal(m, "subject")
|
|
493
|
+
date := strVal(m, "date")
|
|
494
|
+
otp := strVal(m, "otp")
|
|
495
|
+
|
|
496
|
+
display.Table([]display.Row{
|
|
497
|
+
{Key: "ID", Value: id},
|
|
498
|
+
{Key: "From", Value: from},
|
|
499
|
+
{Key: "Subject", Value: subject},
|
|
500
|
+
{Key: "Date", Value: date},
|
|
501
|
+
{Key: "OTP", Value: func() string {
|
|
502
|
+
if otp == "" || otp == "__DETECTED__" {
|
|
503
|
+
return "—"
|
|
504
|
+
}
|
|
505
|
+
return otp
|
|
506
|
+
}()},
|
|
507
|
+
})
|
|
508
|
+
display.Divider()
|
|
509
|
+
}
|
|
510
|
+
fmt.Println()
|
|
511
|
+
return nil
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── fce otp ───────────────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
var otpCmd = &cobra.Command{
|
|
518
|
+
Use: "otp <inbox>",
|
|
519
|
+
Short: "Get the latest OTP from an inbox [Growth plan+]",
|
|
520
|
+
Long: `Fetches the most recent one-time password from an inbox.
|
|
521
|
+
OTP is extracted automatically — no regex needed.
|
|
522
|
+
|
|
523
|
+
Requires Growth plan or above.`,
|
|
524
|
+
Example: ` fce otp mytest@ditmail.info`,
|
|
525
|
+
Args: cobra.ExactArgs(1),
|
|
526
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
527
|
+
client, err := requireAuth()
|
|
528
|
+
if err != nil {
|
|
529
|
+
return err
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Plan gate
|
|
533
|
+
me, err := client.GetMe()
|
|
534
|
+
if err != nil {
|
|
535
|
+
return err
|
|
536
|
+
}
|
|
537
|
+
d, _ := me["data"].(map[string]interface{})
|
|
538
|
+
if d == nil {
|
|
539
|
+
d = me
|
|
540
|
+
}
|
|
541
|
+
plan := strVal(d, "plan")
|
|
542
|
+
if !api.HasPlan(plan, api.PlanGrowth) {
|
|
543
|
+
display.PlanGate("growth", "OTP extraction")
|
|
544
|
+
return nil
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
result, err := client.GetOTP(args[0])
|
|
548
|
+
if err != nil {
|
|
549
|
+
return err
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
otp := strVal(result, "otp")
|
|
553
|
+
link := strVal(result, "verification_link")
|
|
554
|
+
from := strVal(result, "from")
|
|
555
|
+
subj := strVal(result, "subject")
|
|
556
|
+
ts := fmt.Sprintf("%v", result["timestamp"])
|
|
557
|
+
|
|
558
|
+
if otp == "" || otp == "null" {
|
|
559
|
+
display.Info("No OTP found in recent messages.")
|
|
560
|
+
return nil
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
display.Header("OTP")
|
|
564
|
+
rows := []display.Row{
|
|
565
|
+
{Key: "OTP", Value: otp},
|
|
566
|
+
{Key: "From", Value: from},
|
|
567
|
+
{Key: "Subj", Value: subj},
|
|
568
|
+
{Key: "Time", Value: ts},
|
|
569
|
+
}
|
|
570
|
+
if link != "" && link != "null" {
|
|
571
|
+
rows = append(rows, display.Row{Key: "Link", Value: link})
|
|
572
|
+
}
|
|
573
|
+
display.Table(rows)
|
|
574
|
+
return nil
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ── fce domains ───────────────────────────────────────────────────────────────
|
|
579
|
+
|
|
580
|
+
var domainsCmd = &cobra.Command{
|
|
581
|
+
Use: "domains",
|
|
582
|
+
Short: "List available domains on your plan",
|
|
583
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
584
|
+
client, err := requireAuth()
|
|
585
|
+
if err != nil {
|
|
586
|
+
return err
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
domains, err := client.ListDomains()
|
|
590
|
+
if err != nil {
|
|
591
|
+
return err
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if len(domains) == 0 {
|
|
595
|
+
display.Info("No domains available.")
|
|
596
|
+
return nil
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
display.Header(fmt.Sprintf("Available Domains (%d)", len(domains)))
|
|
600
|
+
items := make([]string, 0, len(domains))
|
|
601
|
+
for _, item := range domains {
|
|
602
|
+
if m, ok := item.(map[string]interface{}); ok {
|
|
603
|
+
domain := strVal(m, "domain")
|
|
604
|
+
tier := strVal(m, "tier")
|
|
605
|
+
suffix := ""
|
|
606
|
+
if tier == "pro" {
|
|
607
|
+
suffix = " " + display.PlanBadge("pro")
|
|
608
|
+
}
|
|
609
|
+
items = append(items, domain+suffix)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
display.List(items)
|
|
613
|
+
return nil
|
|
614
|
+
},
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
618
|
+
|
|
619
|
+
func requireAuth() (*api.Client, error) {
|
|
620
|
+
client, err := api.New()
|
|
621
|
+
if err != nil {
|
|
622
|
+
display.Error("Not logged in.")
|
|
623
|
+
display.Info("Run: fce login")
|
|
624
|
+
return nil, fmt.Errorf("not authenticated")
|
|
625
|
+
}
|
|
626
|
+
return client, nil
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
func strVal(m map[string]interface{}, key string) string {
|
|
630
|
+
v, _ := m[key].(string)
|
|
631
|
+
return v
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
func intVal(m map[string]interface{}, key string) int {
|
|
635
|
+
switch v := m[key].(type) {
|
|
636
|
+
case float64:
|
|
637
|
+
return int(v)
|
|
638
|
+
case int:
|
|
639
|
+
return v
|
|
640
|
+
}
|
|
641
|
+
return 0
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// randomInbox generates a random inbox address using a free-tier domain
|
|
645
|
+
var randomDomains = []string{
|
|
646
|
+
"ditube.info", "ditmail.info", "ditapi.info",
|
|
647
|
+
"ditgame.info", "ditplay.info", "ditcloud.info",
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
var adjectives = []string{
|
|
651
|
+
"swift", "clear", "quiet", "bright", "calm",
|
|
652
|
+
"sharp", "bold", "cool", "crisp", "light",
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
var nouns = []string{
|
|
656
|
+
"fox", "hawk", "mint", "wave", "peak",
|
|
657
|
+
"pine", "vale", "reef", "beam", "dusk",
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
func randomInbox() string {
|
|
661
|
+
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
662
|
+
adj := adjectives[rng.Intn(len(adjectives))]
|
|
663
|
+
noun := nouns[rng.Intn(len(nouns))]
|
|
664
|
+
n := rng.Intn(9000) + 1000
|
|
665
|
+
domain := randomDomains[rng.Intn(len(randomDomains))]
|
|
666
|
+
return strings.ToLower(fmt.Sprintf("%s%s%d@%s", adj, noun, n, domain))
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
func devInbox() string {
|
|
670
|
+
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
671
|
+
chars := "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
672
|
+
suffix := make([]byte, 4)
|
|
673
|
+
for i := range suffix {
|
|
674
|
+
suffix[i] = chars[rng.Intn(len(chars))]
|
|
675
|
+
}
|
|
676
|
+
domain := randomDomains[rng.Intn(len(randomDomains))]
|
|
677
|
+
return fmt.Sprintf("dev-%s@%s", string(suffix), domain)
|
|
678
|
+
}
|
package/go.mod
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module github.com/DishIs/fce-cli
|
|
2
|
+
|
|
3
|
+
go 1.22
|
|
4
|
+
|
|
5
|
+
require (
|
|
6
|
+
github.com/charmbracelet/lipgloss v0.12.1
|
|
7
|
+
github.com/gorilla/websocket v1.5.3
|
|
8
|
+
github.com/spf13/cobra v1.8.1
|
|
9
|
+
github.com/zalando/go-keyring v0.2.5
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
require (
|
|
13
|
+
github.com/alessio/shellescape v1.4.1 // indirect
|
|
14
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
15
|
+
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
|
16
|
+
github.com/danieljoos/wincred v1.2.0 // indirect
|
|
17
|
+
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
|
18
|
+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
19
|
+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
20
|
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
21
|
+
github.com/mattn/go-runewidth v0.0.15 // indirect
|
|
22
|
+
github.com/muesli/termenv v0.15.2 // indirect
|
|
23
|
+
github.com/rivo/uniseg v0.4.7 // indirect
|
|
24
|
+
github.com/spf13/pflag v1.0.5 // indirect
|
|
25
|
+
golang.org/x/sys v0.19.0 // indirect
|
|
26
|
+
)
|