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.
@@ -0,0 +1,242 @@
1
+ package display
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+
7
+ "github.com/charmbracelet/lipgloss"
8
+ )
9
+
10
+ // ── Styles ────────────────────────────────────────────────────────────────────
11
+
12
+ var (
13
+ // Adaptive colors for both light and dark terminals
14
+ colorDim = lipgloss.AdaptiveColor{Light: "244", Dark: "240"}
15
+ colorBright = lipgloss.AdaptiveColor{Light: "235", Dark: "255"}
16
+ colorMuted = lipgloss.AdaptiveColor{Light: "248", Dark: "245"}
17
+ colorAccent = lipgloss.AdaptiveColor{Light: "232", Dark: "255"}
18
+ colorBorder = lipgloss.AdaptiveColor{Light: "250", Dark: "238"}
19
+
20
+ styleDim = lipgloss.NewStyle().Foreground(colorDim)
21
+ styleBright = lipgloss.NewStyle().Foreground(colorBright).Bold(true)
22
+ styleMuted = lipgloss.NewStyle().Foreground(colorMuted)
23
+ styleAccent = lipgloss.NewStyle().Foreground(colorAccent).Bold(true)
24
+ styleBorder = lipgloss.NewStyle().Foreground(colorBorder)
25
+ styleSuccess = lipgloss.NewStyle().Foreground(colorBright)
26
+ styleError = lipgloss.NewStyle().Foreground(colorBright).Bold(true)
27
+ styleWarn = lipgloss.NewStyle().Foreground(colorDim)
28
+
29
+ panelStyle = lipgloss.NewStyle().
30
+ Border(lipgloss.RoundedBorder()).
31
+ BorderForeground(colorBorder).
32
+ Padding(0, 2)
33
+ )
34
+
35
+ // ── Logo ──────────────────────────────────────────────────────────────────────
36
+
37
+ // ASCII logo optimized for readability and Go raw string constraints
38
+ // Using @@ as a placeholder for backticks
39
+ const logoASCII = `
40
+ ______ ______ __ ______ _ __
41
+ / ____/________ ___ / ____/_ _______/ /_____ ____ ___ / ____/___ ___ ____ _(_) /
42
+ / /_ / ___/ _ \/ _ \ / / / / / ___/ __/ __ \/ __ @@__ \ / __/ / __ @@__ \/ __ @@/ / /
43
+ / __/ / / / __/ __/ /___/ /_/ (__ ) /_/ /_/ / / / / / / / /___/ / / / / / /_/ / / /
44
+ /_/ /_/ \___/\___/\____/\__,_/____/\__/\____/_/ /_/ /_/ /_____/_/ /_/ /_/\__,_/_/_/
45
+ `
46
+
47
+ const tagline = ` FreeCustom.Email — disposable inbox API`
48
+
49
+ // PrintLogo prints the full logo + wordmark on first login
50
+ func PrintLogo() {
51
+ fixedLogo := strings.ReplaceAll(logoASCII, "@@", "`")
52
+ logo := styleBright.Render(fixedLogo)
53
+ tag := styleMuted.Render(tagline)
54
+ fmt.Println(logo)
55
+ fmt.Println(tag)
56
+ fmt.Println()
57
+ }
58
+
59
+ // PrintInlineLogo prints a compact single-line logo for command headers
60
+ func PrintInlineLogo() {
61
+ icon := styleBright.Render("◉")
62
+ name := styleBright.Render("fce")
63
+ fmt.Printf("%s %s ", icon, name)
64
+ }
65
+
66
+ // ── Section headers ───────────────────────────────────────────────────────────
67
+
68
+ func Header(title string) {
69
+ bar := styleDim.Render(strings.Repeat("─", 48))
70
+ head := styleAccent.Render(title)
71
+ fmt.Println()
72
+ fmt.Println(bar)
73
+ fmt.Printf(" %s\n", head)
74
+ fmt.Println(bar)
75
+ }
76
+
77
+ // ── Status messages ───────────────────────────────────────────────────────────
78
+
79
+ func Success(msg string) {
80
+ fmt.Printf(" %s %s\n", styleBright.Render("✓"), styleSuccess.Render(msg))
81
+ }
82
+
83
+ func Error(msg string) {
84
+ fmt.Printf(" %s %s\n", styleBright.Render("✗"), styleError.Render(msg))
85
+ }
86
+
87
+ func Warn(msg string) {
88
+ fmt.Printf(" %s %s\n", styleBright.Render("!"), styleWarn.Render(msg))
89
+ }
90
+
91
+ func Info(msg string) {
92
+ fmt.Printf(" %s %s\n", styleDim.Render("·"), styleMuted.Render(msg))
93
+ }
94
+
95
+ func Step(n int, total int, msg string) {
96
+ counter := styleDim.Render(fmt.Sprintf("[%d/%d]", n, total))
97
+ fmt.Printf(" %s %s\n", counter, styleMuted.Render(msg))
98
+ }
99
+
100
+ // ── Tables ────────────────────────────────────────────────────────────────────
101
+
102
+ type Row struct {
103
+ Key string
104
+ Value string
105
+ }
106
+
107
+ func Table(rows []Row) {
108
+ maxKey := 0
109
+ for _, r := range rows {
110
+ if len(r.Key) > maxKey {
111
+ maxKey = len(r.Key)
112
+ }
113
+ }
114
+ fmt.Println()
115
+ for _, r := range rows {
116
+ pad := strings.Repeat(" ", maxKey-len(r.Key)+2)
117
+ key := styleDim.Render(r.Key)
118
+ sep := styleBorder.Render("·")
119
+ value := styleBright.Render(r.Value)
120
+ fmt.Printf(" %s%s%s %s\n", key, pad, sep, value)
121
+ }
122
+ fmt.Println()
123
+ }
124
+
125
+ // ── List ──────────────────────────────────────────────────────────────────────
126
+
127
+ func List(items []string) {
128
+ fmt.Println()
129
+ for i, item := range items {
130
+ n := styleDim.Render(fmt.Sprintf("%02d", i+1))
131
+ dot := styleBorder.Render("·")
132
+ fmt.Printf(" %s %s %s\n", n, dot, styleBright.Render(item))
133
+ }
134
+ fmt.Println()
135
+ }
136
+
137
+ // ── Plan badge ────────────────────────────────────────────────────────────────
138
+
139
+ func PlanBadge(plan string) string {
140
+ p := strings.ToLower(plan)
141
+ if p == "free" || p == "developer" {
142
+ return styleDim.Render("[" + strings.ToUpper(plan) + "]")
143
+ }
144
+
145
+ // High visibility solid badge for paid plans
146
+ return lipgloss.NewStyle().
147
+ Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "0"}).
148
+ Background(lipgloss.AdaptiveColor{Light: "235", Dark: "255"}).
149
+ Padding(0, 1).
150
+ Bold(true).
151
+ Render(strings.ToUpper(plan))
152
+ }
153
+
154
+ // ── Live event (for watch) ────────────────────────────────────────────────────
155
+
156
+ func EmailEvent(id, from, subject, otp, link string, timestamp string) {
157
+ div := styleDim.Render(strings.Repeat("─", 52))
158
+ fmt.Println()
159
+ fmt.Println(div)
160
+ fmt.Printf(" %s %s\n", styleDim.Render("ID "), styleDim.Render(id))
161
+ fmt.Printf(" %s %s\n", styleDim.Render("FROM"), styleBright.Render(from))
162
+ fmt.Printf(" %s %s\n", styleDim.Render("SUBJ"), styleMuted.Render(subject))
163
+ fmt.Printf(" %s %s\n", styleDim.Render("TIME"), styleDim.Render(timestamp))
164
+ if otp != "" {
165
+ otpVal := styleAccent.Render(otp)
166
+ fmt.Printf(" %s %s\n", styleBright.Render("OTP "), otpVal)
167
+ }
168
+ if link != "" {
169
+ button := lipgloss.NewStyle().
170
+ Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "0"}).
171
+ Background(lipgloss.AdaptiveColor{Light: "235", Dark: "255"}).
172
+ Padding(0, 1).
173
+ Bold(true).
174
+ Render("OPEN EMAIL")
175
+ fmt.Printf("\n %s %s\n", button, styleDim.Render(link))
176
+ }
177
+ fmt.Println(div)
178
+ fmt.Println()
179
+ }
180
+
181
+ func MessageContent(data map[string]interface{}) {
182
+ from := fmt.Sprintf("%v", data["from"])
183
+ subject := fmt.Sprintf("%v", data["subject"])
184
+ date := fmt.Sprintf("%v", data["date"])
185
+ text := fmt.Sprintf("%v", data["text"])
186
+ body := fmt.Sprintf("%v", data["body"])
187
+ html := fmt.Sprintf("%v", data["html"])
188
+
189
+ Header("Message Details")
190
+ Table([]Row{
191
+ {Key: "From", Value: from},
192
+ {Key: "Subject", Value: subject},
193
+ {Key: "Date", Value: date},
194
+ })
195
+
196
+ if text != "" && text != "<nil>" {
197
+ fmt.Println("\n" + styleDim.Render("── Text Content ──────────────────────────────────────────────"))
198
+ fmt.Println(text)
199
+ } else if body != "" && body != "<nil>" {
200
+ fmt.Println("\n" + styleDim.Render("── Body ──────────────────────────────────────────────────────"))
201
+ fmt.Println(body)
202
+ } else if html != "" && html != "<nil>" {
203
+ fmt.Println("\n" + styleDim.Render("── HTML (Source) ─────────────────────────────────────────────"))
204
+ fmt.Println(html)
205
+ }
206
+ fmt.Println()
207
+ }
208
+
209
+ // ── Waiting spinner (simple) ──────────────────────────────────────────────────
210
+
211
+ func Waiting(msg string) {
212
+ fmt.Printf(" %s %s\n", styleDim.Render("◌"), styleMuted.Render(msg))
213
+ }
214
+
215
+ // ── Plan gate error ───────────────────────────────────────────────────────────
216
+
217
+ func PlanGate(requiredPlan string, feature string) {
218
+ fmt.Println()
219
+ Error(fmt.Sprintf("%s requires %s plan or above.", feature, strings.Title(requiredPlan)))
220
+ Info("Upgrade at: https://www.freecustom.email/api/pricing")
221
+ fmt.Println()
222
+ }
223
+
224
+ // ── Divider ───────────────────────────────────────────────────────────────────
225
+
226
+ func Divider() {
227
+ fmt.Println(styleDim.Render(" " + strings.Repeat("─", 48)))
228
+ }
229
+
230
+ // TableBadge returns a formatted string with a badge and value
231
+ func TableBadge(label, value string) string {
232
+ badge := lipgloss.NewStyle().
233
+ Foreground(lipgloss.AdaptiveColor{Light: "255", Dark: "0"}).
234
+ Background(lipgloss.AdaptiveColor{Light: "244", Dark: "240"}).
235
+ Padding(0, 1).
236
+ Bold(true).
237
+ Width(10).
238
+ Align(lipgloss.Center).
239
+ Render(strings.ToUpper(label))
240
+
241
+ return fmt.Sprintf(" %s %s", badge, styleBright.Render(value))
242
+ }
@@ -0,0 +1,144 @@
1
+ package ws
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "net/url"
7
+ "os"
8
+ "os/signal"
9
+ "syscall"
10
+ "time"
11
+
12
+ "github.com/DishIs/fce-cli/internal/display"
13
+ "github.com/gorilla/websocket"
14
+ )
15
+
16
+ const wsBaseURL = "wss://api2.freecustom.email/v1/ws"
17
+
18
+ type emailEvent struct {
19
+ Type string `json:"type"`
20
+ ID string `json:"id"`
21
+ From string `json:"from"`
22
+ To string `json:"to"`
23
+ Subject string `json:"subject"`
24
+ Date string `json:"date"`
25
+ OTP string `json:"otp"`
26
+ VerificationLink string `json:"verificationLink"`
27
+ HasAttachment bool `json:"hasAttachment"`
28
+ Plan string `json:"plan"`
29
+ // Connected event fields
30
+ SubscribedInboxes []string `json:"subscribed_inboxes"`
31
+ ConnectionCount int `json:"connection_count"`
32
+ }
33
+
34
+ // Watch opens a WebSocket connection and streams emails to the terminal.
35
+ // mailbox can be "" to watch all registered inboxes.
36
+ func Watch(apiKey, mailbox string) error {
37
+ u, _ := url.Parse(wsBaseURL)
38
+ q := u.Query()
39
+ q.Set("api_key", apiKey)
40
+ if mailbox != "" {
41
+ q.Set("mailbox", mailbox)
42
+ }
43
+ u.RawQuery = q.Encode()
44
+
45
+ // Connect
46
+ dialer := websocket.DefaultDialer
47
+ conn, _, err := dialer.Dial(u.String(), nil)
48
+ if err != nil {
49
+ return fmt.Errorf("could not connect to WebSocket: %w", err)
50
+ }
51
+ defer conn.Close()
52
+
53
+ // Graceful shutdown on SIGINT / SIGTERM
54
+ sigCh := make(chan os.Signal, 1)
55
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
56
+ go func() {
57
+ <-sigCh
58
+ fmt.Println()
59
+ display.Info("Disconnected.")
60
+ conn.WriteMessage(websocket.CloseMessage,
61
+ websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
62
+ conn.Close()
63
+ os.Exit(0)
64
+ }()
65
+
66
+ // Ping loop to keep alive
67
+ go func() {
68
+ ticker := time.NewTicker(25 * time.Second)
69
+ defer ticker.Stop()
70
+ for range ticker.C {
71
+ msg, _ := json.Marshal(map[string]string{"type": "ping"})
72
+ conn.WriteMessage(websocket.TextMessage, msg)
73
+ }
74
+ }()
75
+
76
+ // Read loop
77
+ for {
78
+ _, raw, err := conn.ReadMessage()
79
+ if err != nil {
80
+ if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
81
+ return nil
82
+ }
83
+ return fmt.Errorf("connection error: %w", err)
84
+ }
85
+
86
+ var event emailEvent
87
+ if err := json.Unmarshal(raw, &event); err != nil {
88
+ continue
89
+ }
90
+
91
+ switch event.Type {
92
+ case "connected":
93
+ inboxList := mailbox
94
+ if inboxList == "" {
95
+ inboxList = fmt.Sprintf("%d inbox(es)", len(event.SubscribedInboxes))
96
+ }
97
+ display.Success(fmt.Sprintf("Watching %s %s",
98
+ inboxList,
99
+ display.PlanBadge(event.Plan),
100
+ ))
101
+ display.Info("Waiting for emails… (press Ctrl+C to stop)")
102
+
103
+ case "pong":
104
+ // keepalive — ignore
105
+
106
+ case "error":
107
+ // plan gate or connection error
108
+ var errEvent struct {
109
+ Type string `json:"type"`
110
+ Code string `json:"code"`
111
+ Message string `json:"message"`
112
+ }
113
+ json.Unmarshal(raw, &errEvent)
114
+ display.Error(errEvent.Message)
115
+ return nil
116
+
117
+ default:
118
+ // Email event
119
+ id := event.ID
120
+ otp := safeStr(event.OTP)
121
+ link := safeStr(event.VerificationLink)
122
+ ts := formatTimestamp(event.Date)
123
+ display.EmailEvent(id, event.From, event.Subject, otp, link, ts)
124
+ }
125
+ }
126
+ }
127
+
128
+ func safeStr(s string) string {
129
+ if s == "null" || s == "__DETECTED__" || s == "__UPGRADE_REQUIRED__" {
130
+ return ""
131
+ }
132
+ return s
133
+ }
134
+
135
+ func formatTimestamp(t string) string {
136
+ if t == "" {
137
+ return time.Now().Format("15:04:05")
138
+ }
139
+ parsed, err := time.Parse(time.RFC3339Nano, t)
140
+ if err != nil {
141
+ return t
142
+ }
143
+ return parsed.Local().Format("15:04:05")
144
+ }
package/logo.webp ADDED
Binary file
package/main.go ADDED
@@ -0,0 +1,7 @@
1
+ package main
2
+
3
+ import "github.com/DishIs/fce-cli/cmd"
4
+
5
+ func main() {
6
+ cmd.Execute()
7
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "fcemail",
3
+ "version": "0.1.11",
4
+ "description": "FreeCustom.Email CLI — Manage disposable inboxes from your terminal.",
5
+ "bin": {
6
+ "fce": "bin/fce"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node scripts/install-binary.js"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/DishIs/fce-cli.git"
14
+ },
15
+ "author": "DishIs",
16
+ "license": "MIT"
17
+ }
@@ -0,0 +1,11 @@
1
+ const { execSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ console.log('Downloading fce-cli binary...');
6
+ try {
7
+ execSync('curl -fsSL https://raw.githubusercontent.com/DishIs/fce-cli/main/scripts/install.sh | BIN_DIR=./bin sh');
8
+ } catch (err) {
9
+ console.error('Failed to download binary:', err.message);
10
+ process.exit(1);
11
+ }
@@ -0,0 +1,65 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Config
5
+ OWNER="DishIs"
6
+ REPO="fce-cli"
7
+ BINARY_NAME="fce"
8
+ GITHUB_API="https://api.github.com/repos/$OWNER/$REPO/releases/latest"
9
+
10
+ # Detect OS
11
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
12
+ case "$OS" in
13
+ linux*) OS='linux';;
14
+ darwin*) OS='darwin';;
15
+ msys*|cygwin*|mingw*) OS='windows';;
16
+ *) echo "Unsupported OS: $OS"; exit 1;;
17
+ esac
18
+
19
+ # Detect Arch
20
+ ARCH=$(uname -m)
21
+ case "$ARCH" in
22
+ x86_64) ARCH='amd64';;
23
+ arm64|aarch64) ARCH='arm64';;
24
+ *) echo "Unsupported architecture: $ARCH"; exit 1;;
25
+ esac
26
+
27
+ # Allow overriding install directory (useful for NPM)
28
+ INSTALL_DIR=${BIN_DIR:-"/usr/local/bin"}
29
+
30
+ echo "Installing $BINARY_NAME for $OS/$ARCH..."
31
+
32
+ # Get latest version and download URL
33
+ DOWNLOAD_URL=$(curl -s $GITHUB_API | grep "browser_download_url" | grep "${OS}_${ARCH}" | cut -d '"' -f 4 | head -n 1)
34
+
35
+ if [ -z "$DOWNLOAD_URL" ]; then
36
+ echo "Could not find a release for $OS/$ARCH"
37
+ exit 1
38
+ fi
39
+
40
+ # Create temp directory
41
+ TMP_DIR=$(mktemp -d)
42
+ trap 'rm -rf "$TMP_DIR"' EXIT
43
+
44
+ # Download and extract
45
+ echo "Downloading $DOWNLOAD_URL..."
46
+ curl -sL "$DOWNLOAD_URL" -o "$TMP_DIR/archive"
47
+
48
+ if [ "$OS" = "windows" ]; then
49
+ unzip -q "$TMP_DIR/archive" -d "$TMP_DIR"
50
+ else
51
+ tar -xzf "$TMP_DIR/archive" -C "$TMP_DIR"
52
+ fi
53
+
54
+ # Move to install directory
55
+ if [ "$INSTALL_DIR" = "./bin" ]; then
56
+ mkdir -p "$INSTALL_DIR"
57
+ mv "$TMP_DIR/$BINARY_NAME" "$INSTALL_DIR/"
58
+ echo "Successfully installed $BINARY_NAME to $INSTALL_DIR!"
59
+ else
60
+ echo "Installing to $INSTALL_DIR/$BINARY_NAME (requires sudo)..."
61
+ sudo mv "$TMP_DIR/$BINARY_NAME" "$INSTALL_DIR/"
62
+ echo "Successfully installed $BINARY_NAME!"
63
+ fi
64
+
65
+ $BINARY_NAME version || "$INSTALL_DIR/$BINARY_NAME" version || echo "Installation complete."