package main import ( "bufio" "bytes" "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "flag" "fmt" "io" "mime/multipart" "net/http" "net/textproto" "os" "os/exec" "path/filepath" "regexp" "sort" "strconv" "strings" "time" ) const ( defaultBaseURL = "https://api.mailthisforme.com" maxPDFBytes = 10 * 1024 * 1024 ) var errNotPDF = errors.New("file does not start with a PDF header") type appState struct { client *apiClient reader *bufio.Reader documentPath string documentSize int64 pageCount int documentIssues []string recipient Address sender Address senderMode senderMode returnAddressID string lastRequestKey string lastMailID string } type senderMode int const ( senderDefault senderMode = iota senderReturnAddressID senderDirect ) type apiClient struct { baseURL string apiKey string httpClient *http.Client } type Address struct { Name string `json:"name,omitempty"` Company string `json:"company,omitempty"` Address1 string `json:"address1,omitempty"` Address2 string `json:"address2,omitempty"` City string `json:"city,omitempty"` State string `json:"state,omitempty"` PostalCode string `json:"postal_code,omitempty"` Country string `json:"country,omitempty"` Phone string `json:"phone,omitempty"` } type returnAddressList struct { Data []returnAddress `json:"data"` } type returnAddress struct { ID string `json:"id"` Label string `json:"label"` Name string `json:"name"` Company string `json:"company"` Address1 string `json:"address1"` Address2 string `json:"address2"` City string `json:"city"` State string `json:"state"` PostalCode string `json:"postal_code"` Country string `json:"country"` IsDefault bool `json:"is_default"` } type fileEntry struct { name string path string isDir bool } type pdfAnalysis struct { PageCount int Issues []string Warnings []string Dimensions []pageDimension } type pageDimension struct { Page int Width float64 Height float64 IsLetter bool } func main() { fileFlag := flag.String("f", "", "PDF document to preselect") flag.Parse() loadDotEnv(".env") apiKey := firstEnv("MAILTHISFORME_API_KEY", "MTFM_API_KEY") baseURL := firstEnv("MAILTHISFORME_BASE_URL", "MTFM_BASE_URL") if baseURL == "" { baseURL = defaultBaseURL } timeout := 60 * time.Second if raw := os.Getenv("MTFM_HTTP_TIMEOUT"); raw != "" { parsed, err := time.ParseDuration(raw) if err == nil && parsed > 0 { timeout = parsed } } state := &appState{ client: &apiClient{ baseURL: strings.TrimRight(baseURL, "/"), apiKey: apiKey, httpClient: &http.Client{ Timeout: timeout, }, }, reader: bufio.NewReader(os.Stdin), senderMode: senderDefault, } if strings.TrimSpace(*fileFlag) != "" { message, err := state.setDocumentPath(*fileFlag) if err != nil { fmt.Fprintf(os.Stderr, "error: -f: %v\n", err) os.Exit(1) } if message != "" { fmt.Fprintf(os.Stderr, "warning: %s\n", message) } } if err := state.run(); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } func (a *appState) run() error { for { a.drawHome() choice := a.prompt("Choose an option") switch strings.TrimSpace(choice) { case "1": a.selectDocument() case "2": a.setRecipient() case "3": a.setSender() case "4": a.quoteDocument() case "5": a.sendDocument() case "6": a.showBalance() case "7": a.checkStatus() case "c", "C": a.convertSelectedDocument() case "8", "q", "Q": fmt.Println("Goodbye.") return nil default: a.pause("Unknown option.") } } } func (a *appState) drawHome() { clearScreen() fmt.Println("MailThisForMe TUI") fmt.Println(strings.Repeat("=", 19)) fmt.Printf("API: %s\n", a.client.baseURL) fmt.Printf("API key: %s\n", maskSecret(a.client.apiKey)) fmt.Printf("PDF: %s\n", a.documentSummary()) fmt.Printf("Pages: %s\n", pageSummary(a.pageCount)) if len(a.documentIssues) > 0 { fmt.Printf("PDF check: %s\n", strings.Join(a.documentIssues, " ")) } fmt.Printf("To: %s\n", addressOneLine(a.recipient)) fmt.Printf("Sender: %s\n", a.senderSummary()) if a.lastMailID != "" { fmt.Printf("Last job: %s\n", a.lastMailID) } fmt.Println() fmt.Println("1. Select PDF document") fmt.Println("2. Enter recipient address") fmt.Println("3. Choose sender / return address") fmt.Println("4. Quote selected document") fmt.Println("5. Send selected document") fmt.Println("6. Show balance") fmt.Println("7. Check job status") fmt.Println("C. Convert selected PDF to US Letter") fmt.Println("8. Quit") fmt.Println() } func (a *appState) selectDocument() { dir, err := os.Getwd() if err != nil { a.pause(fmt.Sprintf("Could not read current directory: %v", err)) return } if a.documentPath != "" { dir = filepath.Dir(a.documentPath) } for { entries, err := readFileEntries(dir) if err != nil { a.pause(fmt.Sprintf("Could not list %s: %v", dir, err)) return } clearScreen() fmt.Println("Select PDF Document") fmt.Println(strings.Repeat("=", 19)) fmt.Printf("Directory: %s\n\n", dir) for i, entry := range entries { label := entry.name if entry.isDir { label += string(os.PathSeparator) } fmt.Printf("%2d. %s\n", i+1, label) } fmt.Println() fmt.Println("Type a number to open/select, a path to select directly, or blank to cancel.") raw := strings.TrimSpace(a.prompt("Selection")) if raw == "" { return } var selected string if index, err := strconv.Atoi(raw); err == nil { if index < 1 || index > len(entries) { a.pause("Invalid selection.") continue } entry := entries[index-1] if entry.isDir { dir = entry.path continue } selected = entry.path } else { selected = expandPath(raw) if !filepath.IsAbs(selected) { selected = filepath.Join(dir, selected) } if info, err := os.Stat(selected); err == nil && info.IsDir() { dir = selected continue } } message, err := a.setDocumentPath(selected) if err != nil { a.pause(fmt.Sprintf("Could not select document: %v", err)) continue } if message != "" { a.pause(fmt.Sprintf("Selected %s (%s, %s). %s", a.documentPath, humanBytes(a.documentSize), pageSummary(a.pageCount), message)) } else { a.pause(fmt.Sprintf("Selected %s (%s, %s).", a.documentPath, humanBytes(a.documentSize), pageSummary(a.pageCount))) } return } } func (a *appState) setDocumentPath(path string) (string, error) { path = expandPath(path) absPath, err := filepath.Abs(path) if err == nil { path = absPath } info, err := os.Stat(path) if err != nil { return "", fmt.Errorf("could not read file: %w", err) } if info.IsDir() { return "", errors.New("that path is a directory, not a PDF") } if !strings.EqualFold(filepath.Ext(path), ".pdf") { return "", errors.New("selected file must have a .pdf extension") } if info.Size() > maxPDFBytes { return "", fmt.Errorf("file is %s; MailThisForMe allows PDFs up to 10 MB", humanBytes(info.Size())) } pages, err := estimatePDFPageCount(path) pageWarning := "" if err != nil { if errors.Is(err, errNotPDF) { return "", err } pageWarning = fmt.Sprintf("%v.", err) pages = 0 } analysis, err := analyzePDF(path) if err != nil { return "", err } a.documentPath = path a.documentSize = info.Size() a.pageCount = pages if analysis.PageCount > 0 { a.pageCount = analysis.PageCount } a.documentIssues = append([]string{}, analysis.Issues...) messages := append([]string{}, analysis.Issues...) messages = append(messages, analysis.Warnings...) if pageWarning != "" { messages = append(messages, pageWarning) } return strings.Join(messages, " "), nil } func (a *appState) setRecipient() { clearScreen() fmt.Println("Recipient Address") fmt.Println(strings.Repeat("=", 17)) addr := a.promptAddress(a.recipient) a.recipient = addr a.pause("Recipient updated.") } func (a *appState) setSender() { for { clearScreen() fmt.Println("Sender / Return Address") fmt.Println(strings.Repeat("=", 23)) fmt.Println("1. Use account default return address") fmt.Println("2. Enter return address id") fmt.Println("3. List saved return addresses") fmt.Println("4. Enter direct sender address") fmt.Println("5. Back") fmt.Println() choice := a.prompt("Choose an option") switch strings.TrimSpace(choice) { case "1": a.senderMode = senderDefault a.returnAddressID = "" a.pause("Sender will use the account default return address.") return case "2": id := strings.TrimSpace(a.prompt("Return address id (raddr_...)")) if id == "" { a.pause("No id entered.") continue } a.senderMode = senderReturnAddressID a.returnAddressID = id a.pause("Return address id set.") return case "3": a.chooseReturnAddress() return case "4": clearScreen() fmt.Println("Direct Sender Address") fmt.Println(strings.Repeat("=", 21)) a.sender = a.promptAddress(a.sender) a.senderMode = senderDirect a.returnAddressID = "" a.pause("Direct sender address updated.") return case "5": return default: a.pause("Unknown option.") } } } func (a *appState) chooseReturnAddress() { if !a.client.hasAPIKey() { a.pause("API key is required to list return addresses.") return } var list returnAddressList if err := a.client.getJSON(context.Background(), "/v1/return-addresses", true, &list); err != nil { a.pause(fmt.Sprintf("Could not list return addresses: %v", err)) return } if len(list.Data) == 0 { a.pause("No saved return addresses found.") return } clearScreen() fmt.Println("Saved Return Addresses") fmt.Println(strings.Repeat("=", 22)) for i, item := range list.Data { defaultMark := "" if item.IsDefault { defaultMark = " [default]" } fmt.Printf("%d. %s%s\n", i+1, item.Label, defaultMark) fmt.Printf(" %s, %s, %s %s %s (%s)\n", item.Name, item.Address1, item.City, item.State, item.PostalCode, item.ID) } fmt.Println() raw := a.prompt("Choose a number, or blank to cancel") if strings.TrimSpace(raw) == "" { return } index, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil || index < 1 || index > len(list.Data) { a.pause("Invalid selection.") return } a.senderMode = senderReturnAddressID a.returnAddressID = list.Data[index-1].ID a.pause(fmt.Sprintf("Using %s.", a.returnAddressID)) } func (a *appState) quoteDocument() { pageCount := a.pageCount if pageCount > 0 { if !confirm(a, "Use estimated "+pageSummary(pageCount)) { pageCount = 0 } } if pageCount <= 0 { pageCount = a.promptPageCount() if pageCount <= 0 { return } } if pageCount > 10 { a.pause(fmt.Sprintf("Estimated %d pages. MailThisForMe quotes accept 1-10 pages.", pageCount)) return } payload := map[string]int{"page_count": pageCount} var out map[string]any if err := a.client.postJSON(context.Background(), "/v1/mail/quote", false, payload, &out); err != nil { a.pause(fmt.Sprintf("Quote failed: %v", err)) return } clearScreen() fmt.Println("Quote") fmt.Println(strings.Repeat("=", 5)) fmt.Println(formatAPIObject(out)) a.pause("") } func (a *appState) sendDocument() { if err := a.validateSendInputs(); err != nil { a.pause(err.Error()) return } if !a.client.hasAPIKey() { a.pause("API key is required to send mail.") return } clearScreen() fmt.Println("Send Document") fmt.Println(strings.Repeat("=", 13)) fmt.Printf("PDF: %s\n", a.documentSummary()) fmt.Printf("To: %s\n", addressOneLine(a.recipient)) fmt.Printf("Sender: %s\n", a.senderSummary()) fmt.Println() fmt.Println("MailThisForMe preview jobs are queued in test mode and do not physically mail automatically.") if !confirm(a, "Submit this document?") { return } requestKey := a.lastRequestKey if requestKey == "" || !confirm(a, "Reuse previous request key?") { requestKey = newRequestKey() } a.lastRequestKey = requestKey var out map[string]any err := a.client.sendMail(context.Background(), a.documentPath, requestKey, a.recipient, a.senderMode, a.returnAddressID, a.sender, &out) if err != nil { a.pause(fmt.Sprintf("Send failed: %v\nRequest key: %s", err, requestKey)) return } if id, ok := out["id"].(string); ok { a.lastMailID = id } clearScreen() fmt.Println("Sent") fmt.Println(strings.Repeat("=", 4)) fmt.Printf("Request key: %s\n\n", requestKey) fmt.Println(formatAPIObject(out)) a.pause("") } func (a *appState) showBalance() { if !a.client.hasAPIKey() { a.pause("API key is required to read balance.") return } var out map[string]any if err := a.client.getJSON(context.Background(), "/v1/balance", true, &out); err != nil { a.pause(fmt.Sprintf("Balance request failed: %v", err)) return } clearScreen() fmt.Println("Balance") fmt.Println(strings.Repeat("=", 7)) fmt.Println(formatAPIObject(out)) a.pause("") } func (a *appState) checkStatus() { if !a.client.hasAPIKey() { a.pause("API key is required to check status.") return } defaultID := a.lastMailID prompt := "Mail job id" if defaultID != "" { prompt += " [" + defaultID + "]" } id := strings.TrimSpace(a.prompt(prompt)) if id == "" { id = defaultID } if id == "" { a.pause("No mail job id entered.") return } var out map[string]any if err := a.client.getJSON(context.Background(), "/v1/mail/"+id, true, &out); err != nil { a.pause(fmt.Sprintf("Status request failed: %v", err)) return } clearScreen() fmt.Println("Mail Job Status") fmt.Println(strings.Repeat("=", 15)) fmt.Println(formatAPIObject(out)) a.pause("") } func (a *appState) convertSelectedDocument() { if a.documentPath == "" { a.pause("Select a PDF document first.") return } if _, err := exec.LookPath("gs"); err != nil { a.pause("Ghostscript is not available on PATH. Install gs or export the PDF as US Letter from the source application.") return } ext := filepath.Ext(a.documentPath) defaultOutput := strings.TrimSuffix(a.documentPath, ext) + "-letter" + ext output := a.promptDefault("Output PDF", defaultOutput) output = expandPath(output) if !filepath.IsAbs(output) { output = filepath.Join(filepath.Dir(a.documentPath), output) } if output == a.documentPath { a.pause("Output path must be different from the selected PDF.") return } if _, err := os.Stat(output); err == nil { if !confirm(a, "Output exists. Overwrite") { return } } else if !errors.Is(err, os.ErrNotExist) { a.pause(fmt.Sprintf("Could not inspect output path: %v", err)) return } cmd := exec.Command( "gs", "-o", output, "-sDEVICE=pdfwrite", "-sPAPERSIZE=letter", "-dFIXEDMEDIA", "-dPDFFitPage", a.documentPath, ) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { details := strings.TrimSpace(stderr.String()) if details != "" { a.pause(fmt.Sprintf("Ghostscript conversion failed: %v\n%s", err, details)) } else { a.pause(fmt.Sprintf("Ghostscript conversion failed: %v", err)) } return } message, err := a.setDocumentPath(output) if err != nil { a.pause(fmt.Sprintf("Created %s, but could not select it: %v", output, err)) return } if message != "" { a.pause(fmt.Sprintf("Created and selected %s. %s", output, message)) return } a.pause(fmt.Sprintf("Created and selected %s.", output)) } func (a *appState) validateSendInputs() error { if a.documentPath == "" { return errors.New("select a PDF document first") } if err := validateRequiredAddress(a.recipient, "recipient"); err != nil { return err } if a.senderMode == senderReturnAddressID && strings.TrimSpace(a.returnAddressID) == "" { return errors.New("enter a return address id or choose the default sender") } if a.senderMode == senderDirect { if err := validateRequiredAddress(a.sender, "sender"); err != nil { return err } } if a.pageCount > 10 { return fmt.Errorf("selected PDF is estimated at %d pages; MailThisForMe allows up to 10", a.pageCount) } if len(a.documentIssues) > 0 { return fmt.Errorf("selected PDF failed local preflight: %s", strings.Join(a.documentIssues, " ")) } return nil } func (a *appState) promptAddress(current Address) Address { addr := current addr.Name = a.promptDefault("Name", addr.Name) addr.Company = a.promptDefault("Company (optional)", addr.Company) addr.Address1 = a.promptDefault("Address line 1", addr.Address1) addr.Address2 = a.promptDefault("Address line 2 (optional)", addr.Address2) addr.City = a.promptDefault("City", addr.City) addr.State = strings.ToUpper(a.promptDefault("State", addr.State)) addr.PostalCode = a.promptDefault("Postal code", addr.PostalCode) addr.Country = strings.ToUpper(a.promptDefault("Country", defaultString(addr.Country, "US"))) addr.Phone = a.promptDefault("Phone (optional)", addr.Phone) return addr } func (a *appState) promptPageCount() int { for { raw := strings.TrimSpace(a.prompt("Page count (1-10)")) if raw == "" { return 0 } n, err := strconv.Atoi(raw) if err == nil && n >= 1 && n <= 10 { return n } fmt.Println("Enter a number from 1 to 10, or blank to cancel.") } } func (a *appState) prompt(label string) string { fmt.Print(label + ": ") text, _ := a.reader.ReadString('\n') return strings.TrimRight(text, "\r\n") } func (a *appState) promptDefault(label, current string) string { if current != "" { label += " [" + current + "]" } value := strings.TrimSpace(a.prompt(label)) if value == "" { return current } return value } func (a *appState) pause(message string) { if message != "" { fmt.Println() fmt.Println(message) } fmt.Println() fmt.Print("Press Enter to continue...") _, _ = a.reader.ReadString('\n') } func readFileEntries(dir string) ([]fileEntry, error) { parent := filepath.Clean(filepath.Join(dir, "..")) entries := []fileEntry{{ name: "..", path: parent, isDir: true, }} items, err := os.ReadDir(dir) if err != nil { return nil, err } for _, item := range items { entries = append(entries, fileEntry{ name: item.Name(), path: filepath.Join(dir, item.Name()), isDir: item.IsDir(), }) } sort.SliceStable(entries[1:], func(i, j int) bool { left := entries[i+1] right := entries[j+1] if left.isDir != right.isDir { return left.isDir } return strings.ToLower(left.name) < strings.ToLower(right.name) }) return entries, nil } func (a *appState) documentSummary() string { if a.documentPath == "" { return "not selected" } return fmt.Sprintf("%s (%s)", a.documentPath, humanBytes(a.documentSize)) } func (a *appState) senderSummary() string { switch a.senderMode { case senderDefault: return "account default return address" case senderReturnAddressID: return "return address " + a.returnAddressID case senderDirect: return addressOneLine(a.sender) default: return "not set" } } func (c *apiClient) hasAPIKey() bool { return strings.TrimSpace(c.apiKey) != "" } func (c *apiClient) getJSON(ctx context.Context, path string, authenticated bool, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) if err != nil { return err } if authenticated { req.Header.Set("Authorization", "Bearer "+c.apiKey) } return c.do(req, out) } func (c *apiClient) postJSON(ctx context.Context, path string, authenticated bool, payload any, out any) error { body, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") if authenticated { req.Header.Set("Authorization", "Bearer "+c.apiKey) } return c.do(req, out) } func (c *apiClient) sendMail(ctx context.Context, documentPath, requestKey string, recipient Address, mode senderMode, returnAddressID string, sender Address, out any) error { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFilePart(writer, "document", documentPath); err != nil { return err } addAddressFields(writer, "recipient", recipient) switch mode { case senderReturnAddressID: _ = writer.WriteField("return_address_id", returnAddressID) case senderDirect: addAddressFields(writer, "sender", sender) } metadata := `{"source":"mtfm-mailer-tui"}` _ = writer.WriteField("metadata", metadata) _ = writer.Close() req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/mail/send", &body) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.apiKey) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-MailThisForMe-Request-Key", requestKey) return c.do(req, out) } func (c *apiClient) do(req *http.Request, out any) error { resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() data, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024)) if err != nil { return err } if resp.StatusCode < 200 || resp.StatusCode > 299 { return fmt.Errorf("%s: %s", resp.Status, formatAPIError(data)) } if out == nil || len(data) == 0 { return nil } if err := json.Unmarshal(data, out); err != nil { return fmt.Errorf("could not parse JSON response: %w\n%s", err, string(data)) } return nil } func addFilePart(writer *multipart.Writer, field, path string) error { file, err := os.Open(path) if err != nil { return err } defer file.Close() header := make(textproto.MIMEHeader) header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(field), escapeQuotes(filepath.Base(path)))) header.Set("Content-Type", "application/pdf") part, err := writer.CreatePart(header) if err != nil { return err } _, err = io.Copy(part, file) return err } func addAddressFields(writer *multipart.Writer, prefix string, addr Address) { fields := map[string]string{ "name": addr.Name, "company": addr.Company, "address1": addr.Address1, "address2": addr.Address2, "city": addr.City, "state": addr.State, "postal_code": addr.PostalCode, "country": addr.Country, "phone": addr.Phone, } for key, value := range fields { value = strings.TrimSpace(value) if value == "" { continue } _ = writer.WriteField(prefix+"["+key+"]", value) } } func validateRequiredAddress(addr Address, label string) error { missing := []string{} if strings.TrimSpace(addr.Name) == "" { missing = append(missing, "name") } if strings.TrimSpace(addr.Address1) == "" { missing = append(missing, "address1") } if strings.TrimSpace(addr.City) == "" { missing = append(missing, "city") } if strings.TrimSpace(addr.State) == "" { missing = append(missing, "state") } if strings.TrimSpace(addr.PostalCode) == "" { missing = append(missing, "postal_code") } if strings.TrimSpace(addr.Country) == "" { missing = append(missing, "country") } if len(missing) > 0 { return fmt.Errorf("%s address is missing: %s", label, strings.Join(missing, ", ")) } return nil } func estimatePDFPageCount(path string) (int, error) { data, err := os.ReadFile(path) if err != nil { return 0, err } if len(data) < 5 || string(data[:5]) != "%PDF-" { return 0, errNotPDF } re := regexp.MustCompile(`/Type\s*/Page\b`) matches := re.FindAll(data, -1) if len(matches) == 0 { return 0, errors.New("could not find page objects") } if len(matches) > 10 { return len(matches), fmt.Errorf("estimated %d pages; MailThisForMe allows up to 10", len(matches)) } return len(matches), nil } func analyzePDF(path string) (pdfAnalysis, error) { data, err := os.ReadFile(path) if err != nil { return pdfAnalysis{}, err } if len(data) < 5 || string(data[:5]) != "%PDF-" { return pdfAnalysis{}, errNotPDF } analysis := pdfAnalysis{} pageRe := regexp.MustCompile(`/Type\s*/Page\b`) analysis.PageCount = len(pageRe.FindAll(data, -1)) if analysis.PageCount == 0 { analysis.Warnings = append(analysis.Warnings, "Could not estimate PDF page count.") } if analysis.PageCount > 10 { analysis.Issues = append(analysis.Issues, fmt.Sprintf("PDF has %d pages; MailThisForMe allows up to 10.", analysis.PageCount)) } mediaBoxRe := regexp.MustCompile(`/MediaBox\s*\[\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s*\]`) boxes := mediaBoxRe.FindAllSubmatch(data, -1) if len(boxes) == 0 { analysis.Warnings = append(analysis.Warnings, "Could not find PDF MediaBox dimensions for local Letter-size validation.") return analysis, nil } dimensionsToCheck := len(boxes) if analysis.PageCount > 0 && dimensionsToCheck > analysis.PageCount { dimensionsToCheck = analysis.PageCount } if analysis.PageCount > 0 && len(boxes) < analysis.PageCount && len(boxes) != 1 { analysis.Warnings = append(analysis.Warnings, fmt.Sprintf("Found %d MediaBox entries for %d pages; local page-size validation may be incomplete.", len(boxes), analysis.PageCount)) } if analysis.PageCount > 1 && len(boxes) == 1 { dimensionsToCheck = analysis.PageCount } for i := 0; i < dimensionsToCheck; i++ { box := boxes[0] if len(boxes) > 1 { box = boxes[i] } x1, _ := strconv.ParseFloat(string(box[1]), 64) y1, _ := strconv.ParseFloat(string(box[2]), 64) x2, _ := strconv.ParseFloat(string(box[3]), 64) y2, _ := strconv.ParseFloat(string(box[4]), 64) width := absFloat(x2 - x1) height := absFloat(y2 - y1) dimension := pageDimension{ Page: i + 1, Width: width, Height: height, IsLetter: isLetterSize(width, height), } analysis.Dimensions = append(analysis.Dimensions, dimension) if !dimension.IsLetter { analysis.Issues = append(analysis.Issues, fmt.Sprintf("Page %d is %s x %s points; expected US Letter 612 x 792 points.", dimension.Page, formatPoints(width), formatPoints(height))) } } return analysis, nil } func isLetterSize(width, height float64) bool { const tolerance = 2.0 portrait := absFloat(width-612) <= tolerance && absFloat(height-792) <= tolerance landscape := absFloat(width-792) <= tolerance && absFloat(height-612) <= tolerance return portrait || landscape } func absFloat(value float64) float64 { if value < 0 { return -value } return value } func formatPoints(value float64) string { formatted := strconv.FormatFloat(value, 'f', 2, 64) formatted = strings.TrimRight(formatted, "0") return strings.TrimRight(formatted, ".") } func loadDotEnv(path string) { data, err := os.ReadFile(path) if err != nil { return } lines := strings.Split(string(data), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") { continue } key, value, _ := strings.Cut(line, "=") key = strings.TrimSpace(key) value = strings.TrimSpace(value) value = strings.Trim(value, `"'`) if key != "" && os.Getenv(key) == "" { _ = os.Setenv(key, value) } } } func firstEnv(keys ...string) string { for _, key := range keys { if value := strings.TrimSpace(os.Getenv(key)); value != "" { return value } } return "" } func expandPath(path string) string { path = strings.TrimSpace(path) if path == "" { return path } if path == "~" { if home, err := os.UserHomeDir(); err == nil { return home } } if strings.HasPrefix(path, "~/") { if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, path[2:]) } } return path } func formatAPIObject(v any) string { data, err := json.MarshalIndent(v, "", " ") if err != nil { return fmt.Sprint(v) } return string(data) } func formatAPIError(data []byte) string { var decoded any if err := json.Unmarshal(data, &decoded); err == nil { return formatAPIObject(decoded) } text := strings.TrimSpace(string(data)) if text == "" { return "empty response body" } return text } func maskSecret(secret string) string { secret = strings.TrimSpace(secret) if secret == "" { return "missing" } if len(secret) <= 10 { return strings.Repeat("*", len(secret)) } return secret[:8] + "..." + secret[len(secret)-4:] } func addressOneLine(addr Address) string { parts := []string{} for _, part := range []string{addr.Name, addr.Company, addr.Address1, addr.Address2, addr.City, addr.State, addr.PostalCode, addr.Country} { part = strings.TrimSpace(part) if part != "" { parts = append(parts, part) } } if len(parts) == 0 { return "not set" } return strings.Join(parts, ", ") } func pageSummary(pages int) string { if pages <= 0 { return "unknown" } if pages == 1 { return "1 page" } return fmt.Sprintf("%d pages", pages) } func humanBytes(n int64) string { const unit = 1024 if n < unit { return fmt.Sprintf("%d B", n) } div, exp := int64(unit), 0 for size := n / unit; size >= unit; size /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp]) } func defaultString(value, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value } func confirm(a *appState, label string) bool { answer := strings.ToLower(strings.TrimSpace(a.prompt(label + " [y/N]"))) return answer == "y" || answer == "yes" } func newRequestKey() string { var buf [8]byte if _, err := rand.Read(buf[:]); err != nil { return fmt.Sprintf("mtfm-tui-%d", time.Now().UnixNano()) } return "mtfm-tui-" + time.Now().UTC().Format("20060102T150405Z") + "-" + hex.EncodeToString(buf[:]) } func escapeQuotes(value string) string { return strings.ReplaceAll(value, `"`, `\"`) } func clearScreen() { fmt.Print("\033[H\033[2J") }