feat: Refactor of Go backend code
This commit is contained in:
169
DOCUMENTATION.md
169
DOCUMENTATION.md
@@ -37,11 +37,15 @@ EMLy is built using the **Wails v2** framework, which combines a Go backend with
|
|||||||
├─────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────┤
|
||||||
│ Wails Bridge (Auto-generated TypeScript bindings) │
|
│ Wails Bridge (Auto-generated TypeScript bindings) │
|
||||||
├─────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────┤
|
||||||
│ Backend (Go) │
|
│ Backend (Go - Modular Architecture) │
|
||||||
│ ├── App Logic (app.go) │
|
│ ├── app.go - Core struct & lifecycle │
|
||||||
│ ├── Email Parsing (backend/utils/mail/) │
|
│ ├── app_mail.go - Email parsing (EML/MSG/PEC) │
|
||||||
│ ├── Windows APIs (screenshot, debugger detection) │
|
│ ├── app_viewer.go - Viewer window management │
|
||||||
│ └── File Operations │
|
│ ├── app_screenshot.go - Window capture │
|
||||||
|
│ ├── app_bugreport.go - Bug reporting system │
|
||||||
|
│ ├── app_settings.go - Settings import/export │
|
||||||
|
│ ├── app_system.go - Windows system utilities │
|
||||||
|
│ └── backend/utils/ - Shared utilities │
|
||||||
└─────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -71,7 +75,13 @@ EMLy is built using the **Wails v2** framework, which combines a Go backend with
|
|||||||
|
|
||||||
```
|
```
|
||||||
EMLy/
|
EMLy/
|
||||||
├── app.go # Main application logic
|
├── app.go # Core App struct, lifecycle, and configuration
|
||||||
|
├── app_mail.go # Email reading methods (EML, MSG, PEC)
|
||||||
|
├── app_viewer.go # Viewer window management (image, PDF, EML)
|
||||||
|
├── app_screenshot.go # Screenshot capture functionality
|
||||||
|
├── app_bugreport.go # Bug report creation and submission
|
||||||
|
├── app_settings.go # Settings import/export
|
||||||
|
├── app_system.go # Windows system utilities (registry, encoding)
|
||||||
├── main.go # Application entry point
|
├── main.go # Application entry point
|
||||||
├── logger.go # Logging utilities
|
├── logger.go # Logging utilities
|
||||||
├── wails.json # Wails configuration
|
├── wails.json # Wails configuration
|
||||||
@@ -117,6 +127,12 @@ EMLy/
|
|||||||
│ │ │ ├── wailsjs/ # Auto-generated Go bindings
|
│ │ │ ├── wailsjs/ # Auto-generated Go bindings
|
||||||
│ │ │ ├── types/ # TypeScript types
|
│ │ │ ├── types/ # TypeScript types
|
||||||
│ │ │ └── utils/ # Utility functions
|
│ │ │ └── utils/ # Utility functions
|
||||||
|
│ │ │ └── mail/ # Email utilities (modular)
|
||||||
|
│ │ │ ├── index.ts # Barrel export
|
||||||
|
│ │ │ ├── constants.ts # IFRAME_UTIL_HTML, CONTENT_TYPES, etc.
|
||||||
|
│ │ │ ├── data-utils.ts # arrayBufferToBase64, createDataUrl
|
||||||
|
│ │ │ ├── attachment-handlers.ts # openPDFAttachment, openImageAttachment
|
||||||
|
│ │ │ └── email-loader.ts # loadEmailFromPath, processEmailBody
|
||||||
│ │ └── messages/ # i18n translation files
|
│ │ └── messages/ # i18n translation files
|
||||||
│ │ ├── en.json
|
│ │ ├── en.json
|
||||||
│ │ └── it.json
|
│ │ └── it.json
|
||||||
@@ -153,41 +169,104 @@ if strings.Contains(arg, "--view-image") {
|
|||||||
|
|
||||||
### Application Core (`app.go`)
|
### Application Core (`app.go`)
|
||||||
|
|
||||||
The `App` struct is the main application controller, exposed to the frontend via Wails bindings.
|
The `App` struct is the main application controller, exposed to the frontend via Wails bindings. The code is organized into multiple files for maintainability.
|
||||||
|
|
||||||
#### Key Properties
|
#### Key Properties
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type App struct {
|
type App struct {
|
||||||
ctx context.Context
|
ctx context.Context // Wails application context
|
||||||
StartupFilePath string // File opened via command line
|
StartupFilePath string // File opened via command line
|
||||||
CurrentMailFilePath string // Currently loaded mail file
|
CurrentMailFilePath string // Currently loaded mail file
|
||||||
|
openImagesMux sync.Mutex // Mutex for image viewer tracking
|
||||||
openImages map[string]bool // Track open image viewers
|
openImages map[string]bool // Track open image viewers
|
||||||
|
openPDFsMux sync.Mutex // Mutex for PDF viewer tracking
|
||||||
openPDFs map[string]bool // Track open PDF viewers
|
openPDFs map[string]bool // Track open PDF viewers
|
||||||
|
openEMLsMux sync.Mutex // Mutex for EML viewer tracking
|
||||||
openEMLs map[string]bool // Track open EML viewers
|
openEMLs map[string]bool // Track open EML viewers
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Core Methods
|
#### Backend File Organization
|
||||||
|
|
||||||
|
The Go backend is split into logical files:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `app.go` | Core App struct, constructor, lifecycle methods (startup/shutdown), configuration |
|
||||||
|
| `app_mail.go` | Email reading: `ReadEML`, `ReadMSG`, `ReadPEC`, `ReadMSGOSS`, `ShowOpenFileDialog` |
|
||||||
|
| `app_viewer.go` | Viewer windows: `OpenImageWindow`, `OpenPDFWindow`, `OpenEMLWindow`, `OpenPDF`, `OpenImage`, `GetViewerData` |
|
||||||
|
| `app_screenshot.go` | Screenshots: `TakeScreenshot`, `SaveScreenshot`, `SaveScreenshotAs` |
|
||||||
|
| `app_bugreport.go` | Bug reports: `CreateBugReportFolder`, `SubmitBugReport`, `zipFolder` |
|
||||||
|
| `app_settings.go` | Settings I/O: `ExportSettings`, `ImportSettings` |
|
||||||
|
| `app_system.go` | System utilities: `CheckIsDefaultEMLHandler`, `OpenDefaultAppsSettings`, `ConvertToUTF8`, `OpenFolderInExplorer` |
|
||||||
|
|
||||||
|
#### Core Methods by Category
|
||||||
|
|
||||||
|
**Lifecycle & Configuration (`app.go`)**
|
||||||
|
|
||||||
| Method | Description |
|
| Method | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
|
| `startup(ctx)` | Wails startup callback, saves context |
|
||||||
|
| `shutdown(ctx)` | Wails shutdown callback for cleanup |
|
||||||
|
| `QuitApp()` | Terminates the application |
|
||||||
| `GetConfig()` | Returns application configuration from `config.ini` |
|
| `GetConfig()` | Returns application configuration from `config.ini` |
|
||||||
|
| `SaveConfig(cfg)` | Saves configuration to `config.ini` |
|
||||||
| `GetStartupFile()` | Returns file path passed via command line |
|
| `GetStartupFile()` | Returns file path passed via command line |
|
||||||
| `SetCurrentMailFilePath()` | Updates the current mail file path |
|
| `SetCurrentMailFilePath()` | Updates the current mail file path |
|
||||||
| `ReadEML(path)` | Parses an EML file and returns email data |
|
| `GetMachineData()` | Returns system information |
|
||||||
| `ReadMSG(path)` | Parses an MSG file and returns email data |
|
| `IsDebuggerRunning()` | Checks if a debugger is attached |
|
||||||
|
|
||||||
|
**Email Reading (`app_mail.go`)**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `ReadEML(path)` | Parses a standard .eml file |
|
||||||
|
| `ReadMSG(path, useExternal)` | Parses a Microsoft .msg file |
|
||||||
| `ReadPEC(path)` | Parses PEC (Italian certified email) files |
|
| `ReadPEC(path)` | Parses PEC (Italian certified email) files |
|
||||||
| `ShowOpenFileDialog()` | Opens native file picker for EML/MSG files |
|
| `ShowOpenFileDialog()` | Opens native file picker for EML/MSG files |
|
||||||
| `OpenImageWindow(data, filename)` | Opens image in new viewer window |
|
|
||||||
| `OpenPDFWindow(data, filename)` | Opens PDF in new viewer window |
|
**Viewer Windows (`app_viewer.go`)**
|
||||||
| `OpenEMLWindow(data, filename)` | Opens EML attachment in new window |
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `OpenImageWindow(data, filename)` | Opens image in built-in viewer |
|
||||||
|
| `OpenPDFWindow(data, filename)` | Opens PDF in built-in viewer |
|
||||||
|
| `OpenEMLWindow(data, filename)` | Opens EML attachment in new EMLy window |
|
||||||
|
| `OpenImage(data, filename)` | Opens image with system default app |
|
||||||
|
| `OpenPDF(data, filename)` | Opens PDF with system default app |
|
||||||
|
| `GetViewerData()` | Returns viewer data for viewer mode detection |
|
||||||
|
|
||||||
|
**Screenshots (`app_screenshot.go`)**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
| `TakeScreenshot()` | Captures window screenshot as base64 PNG |
|
| `TakeScreenshot()` | Captures window screenshot as base64 PNG |
|
||||||
| `SubmitBugReport(input)` | Creates bug report with screenshot and system info |
|
| `SaveScreenshot()` | Saves screenshot to temp directory |
|
||||||
|
| `SaveScreenshotAs()` | Opens save dialog for screenshot |
|
||||||
|
|
||||||
|
**Bug Reports (`app_bugreport.go`)**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `CreateBugReportFolder()` | Creates folder with screenshot and mail file |
|
||||||
|
| `SubmitBugReport(input)` | Creates complete bug report with ZIP archive |
|
||||||
|
|
||||||
|
**Settings (`app_settings.go`)**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
| `ExportSettings(json)` | Exports settings to JSON file |
|
| `ExportSettings(json)` | Exports settings to JSON file |
|
||||||
| `ImportSettings()` | Imports settings from JSON file |
|
| `ImportSettings()` | Imports settings from JSON file |
|
||||||
| `IsDebuggerRunning()` | Checks if a debugger is attached |
|
|
||||||
| `QuitApp()` | Terminates the application |
|
**System Utilities (`app_system.go`)**
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `CheckIsDefaultEMLHandler()` | Checks if EMLy is default for .eml files |
|
||||||
|
| `OpenDefaultAppsSettings()` | Opens Windows default apps settings |
|
||||||
|
| `ConvertToUTF8(string)` | Converts string to valid UTF-8 |
|
||||||
|
| `OpenFolderInExplorer(path)` | Opens folder in Windows Explorer |
|
||||||
|
|
||||||
### Email Parsing (`backend/utils/mail/`)
|
### Email Parsing (`backend/utils/mail/`)
|
||||||
|
|
||||||
@@ -315,6 +394,62 @@ if (att.contentType.startsWith("image/")) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Frontend Mail Utilities (`lib/utils/mail/`)
|
||||||
|
|
||||||
|
The frontend email handling code is organized into modular utility files:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `index.ts` | Barrel export for all mail utilities |
|
||||||
|
| `constants.ts` | Constants: `IFRAME_UTIL_HTML`, `CONTENT_TYPES`, `PEC_FILES`, `EMAIL_EXTENSIONS` |
|
||||||
|
| `data-utils.ts` | Data conversion: `arrayBufferToBase64`, `createDataUrl`, `looksLikeBase64`, `tryDecodeBase64` |
|
||||||
|
| `attachment-handlers.ts` | Attachment opening: `openPDFAttachment`, `openImageAttachment`, `openEMLAttachment` |
|
||||||
|
| `email-loader.ts` | Email loading: `loadEmailFromPath`, `openAndLoadEmail`, `processEmailBody`, `isEmailFile` |
|
||||||
|
|
||||||
|
#### Key Functions
|
||||||
|
|
||||||
|
**Data Utilities** (`data-utils.ts`)
|
||||||
|
```typescript
|
||||||
|
// Convert ArrayBuffer to base64 string
|
||||||
|
function arrayBufferToBase64(buffer: unknown): string;
|
||||||
|
|
||||||
|
// Create data URL for file downloads
|
||||||
|
function createDataUrl(contentType: string, base64Data: string): string;
|
||||||
|
|
||||||
|
// Check if string looks like base64 encoded content
|
||||||
|
function looksLikeBase64(content: string): boolean;
|
||||||
|
|
||||||
|
// Attempt to decode base64, returns null on failure
|
||||||
|
function tryDecodeBase64(content: string): string | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Attachment Handlers** (`attachment-handlers.ts`)
|
||||||
|
```typescript
|
||||||
|
// Open PDF using built-in or external viewer based on settings
|
||||||
|
async function openPDFAttachment(base64Data: string, filename: string): Promise<AttachmentHandlerResult>;
|
||||||
|
|
||||||
|
// Open image using built-in or external viewer based on settings
|
||||||
|
async function openImageAttachment(base64Data: string, filename: string): Promise<AttachmentHandlerResult>;
|
||||||
|
|
||||||
|
// Open EML attachment in new EMLy window
|
||||||
|
async function openEMLAttachment(base64Data: string, filename: string): Promise<AttachmentHandlerResult>;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Email Loader** (`email-loader.ts`)
|
||||||
|
```typescript
|
||||||
|
// Load email from file path, handles EML/MSG/PEC detection
|
||||||
|
async function loadEmailFromPath(filePath: string): Promise<LoadEmailResult>;
|
||||||
|
|
||||||
|
// Open file dialog and load selected email
|
||||||
|
async function openAndLoadEmail(): Promise<LoadEmailResult>;
|
||||||
|
|
||||||
|
// Process email body (decode base64, fix encoding)
|
||||||
|
async function processEmailBody(body: string): Promise<string>;
|
||||||
|
|
||||||
|
// Check if file path is a valid email file
|
||||||
|
function isEmailFile(filePath: string): boolean;
|
||||||
|
```
|
||||||
|
|
||||||
### Settings Page (`(app)/settings/+page.svelte`)
|
### Settings Page (`(app)/settings/+page.svelte`)
|
||||||
|
|
||||||
Organized into cards with various configuration options:
|
Organized into cards with various configuration options:
|
||||||
|
|||||||
281
app_bugreport.go
Normal file
281
app_bugreport.go
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
// Package main provides bug reporting functionality for EMLy.
|
||||||
|
// This file contains methods for creating bug reports with screenshots,
|
||||||
|
// email files, and system information.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"emly/backend/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Bug Report Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// BugReportResult contains paths to the generated bug report files.
|
||||||
|
type BugReportResult struct {
|
||||||
|
// FolderPath is the path to the bug report folder in temp
|
||||||
|
FolderPath string `json:"folderPath"`
|
||||||
|
// ScreenshotPath is the path to the captured screenshot file
|
||||||
|
ScreenshotPath string `json:"screenshotPath"`
|
||||||
|
// MailFilePath is the path to the copied mail file (empty if no mail loaded)
|
||||||
|
MailFilePath string `json:"mailFilePath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BugReportInput contains the user-provided bug report details.
|
||||||
|
type BugReportInput struct {
|
||||||
|
// Name is the user's name
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Email is the user's email address for follow-up
|
||||||
|
Email string `json:"email"`
|
||||||
|
// Description is the detailed bug description
|
||||||
|
Description string `json:"description"`
|
||||||
|
// ScreenshotData is the base64-encoded PNG screenshot (captured before dialog opens)
|
||||||
|
ScreenshotData string `json:"screenshotData"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitBugReportResult contains the result of submitting a bug report.
|
||||||
|
type SubmitBugReportResult struct {
|
||||||
|
// ZipPath is the path to the created zip file
|
||||||
|
ZipPath string `json:"zipPath"`
|
||||||
|
// FolderPath is the path to the bug report folder
|
||||||
|
FolderPath string `json:"folderPath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Bug Report Methods
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// CreateBugReportFolder creates a folder in temp with screenshot and optionally
|
||||||
|
// the current mail file. This is used for the legacy bug report flow.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *BugReportResult: Paths to created files
|
||||||
|
// - error: Error if folder creation or file operations fail
|
||||||
|
func (a *App) CreateBugReportFolder() (*BugReportResult, error) {
|
||||||
|
// Create unique folder name with timestamp
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
folderName := fmt.Sprintf("emly_bugreport_%s", timestamp)
|
||||||
|
|
||||||
|
// Create folder in temp directory
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
bugReportFolder := filepath.Join(tempDir, folderName)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(bugReportFolder, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create bug report folder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &BugReportResult{
|
||||||
|
FolderPath: bugReportFolder,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take and save screenshot
|
||||||
|
screenshotResult, err := a.TakeScreenshot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to take screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
screenshotData, err := base64.StdEncoding.DecodeString(screenshotResult.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
screenshotPath := filepath.Join(bugReportFolder, screenshotResult.Filename)
|
||||||
|
if err := os.WriteFile(screenshotPath, screenshotData, 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save screenshot: %w", err)
|
||||||
|
}
|
||||||
|
result.ScreenshotPath = screenshotPath
|
||||||
|
|
||||||
|
// Copy currently loaded mail file if one exists
|
||||||
|
if a.CurrentMailFilePath != "" {
|
||||||
|
mailData, err := os.ReadFile(a.CurrentMailFilePath)
|
||||||
|
if err != nil {
|
||||||
|
// Log but don't fail - screenshot is still valid
|
||||||
|
Log("Failed to read mail file for bug report:", err)
|
||||||
|
} else {
|
||||||
|
mailFilename := filepath.Base(a.CurrentMailFilePath)
|
||||||
|
mailFilePath := filepath.Join(bugReportFolder, mailFilename)
|
||||||
|
|
||||||
|
if err := os.WriteFile(mailFilePath, mailData, 0644); err != nil {
|
||||||
|
Log("Failed to copy mail file for bug report:", err)
|
||||||
|
} else {
|
||||||
|
result.MailFilePath = mailFilePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitBugReport creates a complete bug report with user input, saves all files,
|
||||||
|
// and creates a zip archive ready for submission.
|
||||||
|
//
|
||||||
|
// The bug report includes:
|
||||||
|
// - User-provided description (report.txt)
|
||||||
|
// - Screenshot (captured before dialog opens)
|
||||||
|
// - Currently loaded mail file (if any)
|
||||||
|
// - System information (hostname, OS version, hardware ID)
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - input: User-provided bug report details including pre-captured screenshot
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *SubmitBugReportResult: Paths to the zip file and folder
|
||||||
|
// - error: Error if any file operation fails
|
||||||
|
func (a *App) SubmitBugReport(input BugReportInput) (*SubmitBugReportResult, error) {
|
||||||
|
// Create unique folder name with timestamp
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
folderName := fmt.Sprintf("emly_bugreport_%s", timestamp)
|
||||||
|
|
||||||
|
// Create folder in temp directory
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
bugReportFolder := filepath.Join(tempDir, folderName)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(bugReportFolder, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create bug report folder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the pre-captured screenshot (captured before dialog opened)
|
||||||
|
if input.ScreenshotData != "" {
|
||||||
|
screenshotData, err := base64.StdEncoding.DecodeString(input.ScreenshotData)
|
||||||
|
if err != nil {
|
||||||
|
Log("Failed to decode screenshot:", err)
|
||||||
|
} else {
|
||||||
|
screenshotPath := filepath.Join(bugReportFolder, fmt.Sprintf("emly_screenshot_%s.png", timestamp))
|
||||||
|
if err := os.WriteFile(screenshotPath, screenshotData, 0644); err != nil {
|
||||||
|
Log("Failed to save screenshot:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the mail file if one is loaded
|
||||||
|
if a.CurrentMailFilePath != "" {
|
||||||
|
mailData, err := os.ReadFile(a.CurrentMailFilePath)
|
||||||
|
if err != nil {
|
||||||
|
Log("Failed to read mail file for bug report:", err)
|
||||||
|
} else {
|
||||||
|
mailFilename := filepath.Base(a.CurrentMailFilePath)
|
||||||
|
mailFilePath := filepath.Join(bugReportFolder, mailFilename)
|
||||||
|
if err := os.WriteFile(mailFilePath, mailData, 0644); err != nil {
|
||||||
|
Log("Failed to copy mail file for bug report:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the report.txt file with user's description
|
||||||
|
reportContent := fmt.Sprintf(`EMLy Bug Report
|
||||||
|
================
|
||||||
|
|
||||||
|
Name: %s
|
||||||
|
Email: %s
|
||||||
|
|
||||||
|
Description:
|
||||||
|
%s
|
||||||
|
|
||||||
|
Generated: %s
|
||||||
|
`, input.Name, input.Email, input.Description, time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
reportPath := filepath.Join(bugReportFolder, "report.txt")
|
||||||
|
if err := os.WriteFile(reportPath, []byte(reportContent), 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save report file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and save machine/system information
|
||||||
|
machineInfo, err := utils.GetMachineInfo()
|
||||||
|
if err == nil && machineInfo != nil {
|
||||||
|
sysInfoContent := fmt.Sprintf(`System Information
|
||||||
|
==================
|
||||||
|
|
||||||
|
Hostname: %s
|
||||||
|
OS: %s
|
||||||
|
Version: %s
|
||||||
|
Hardware ID: %s
|
||||||
|
External IP: %s
|
||||||
|
`, machineInfo.Hostname, machineInfo.OS, machineInfo.Version, machineInfo.HWID, machineInfo.ExternalIP)
|
||||||
|
|
||||||
|
sysInfoPath := filepath.Join(bugReportFolder, "system_info.txt")
|
||||||
|
if err := os.WriteFile(sysInfoPath, []byte(sysInfoContent), 0644); err != nil {
|
||||||
|
Log("Failed to save system info:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create zip archive of the folder
|
||||||
|
zipPath := bugReportFolder + ".zip"
|
||||||
|
if err := zipFolder(bugReportFolder, zipPath); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create zip file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SubmitBugReportResult{
|
||||||
|
ZipPath: zipPath,
|
||||||
|
FolderPath: bugReportFolder,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Helper Functions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// zipFolder creates a zip archive containing all files from the source folder.
|
||||||
|
// Directories are traversed recursively but stored implicitly (no directory entries).
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - sourceFolder: Path to the folder to zip
|
||||||
|
// - destZip: Path where the zip file should be created
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if any file operation fails
|
||||||
|
func zipFolder(sourceFolder, destZip string) error {
|
||||||
|
// Create the zip file
|
||||||
|
zipFile, err := os.Create(destZip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer zipFile.Close()
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(zipFile)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
// Walk through the folder and add all files
|
||||||
|
return filepath.Walk(sourceFolder, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the root folder itself
|
||||||
|
if path == sourceFolder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get relative path for the zip entry
|
||||||
|
relPath, err := filepath.Rel(sourceFolder, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip directories (they're created implicitly)
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the file entry in the zip
|
||||||
|
writer, err := zipWriter.Create(relPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and write the file content
|
||||||
|
fileContent, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = writer.Write(fileContent)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
88
app_mail.go
Normal file
88
app_mail.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// Package main provides email reading functionality for EMLy.
|
||||||
|
// This file contains methods for reading EML, MSG, and PEC email files.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"emly/backend/utils/mail"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Email Reading Methods
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ReadEML reads a standard .eml file and returns the parsed email data.
|
||||||
|
// EML files are MIME-formatted email messages commonly exported from email clients.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filePath: Absolute path to the .eml file
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *internal.EmailData: Parsed email with headers, body, and attachments
|
||||||
|
// - error: Any parsing errors
|
||||||
|
func (a *App) ReadEML(filePath string) (*internal.EmailData, error) {
|
||||||
|
return internal.ReadEmlFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPEC reads a PEC (Posta Elettronica Certificata) .eml file.
|
||||||
|
// PEC emails are Italian certified emails that contain an inner email message
|
||||||
|
// wrapped in a certification envelope with digital signatures.
|
||||||
|
//
|
||||||
|
// This method extracts and returns the inner original email, ignoring the
|
||||||
|
// certification wrapper (daticert.xml and signature files are available as attachments).
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filePath: Absolute path to the PEC .eml file
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *internal.EmailData: The inner original email content
|
||||||
|
// - error: Any parsing errors
|
||||||
|
func (a *App) ReadPEC(filePath string) (*internal.EmailData, error) {
|
||||||
|
return internal.ReadPecInnerEml(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMSG reads a Microsoft Outlook .msg file and returns the email data.
|
||||||
|
// MSG files use the CFB (Compound File Binary) format, which is a proprietary
|
||||||
|
// format used by Microsoft Office applications.
|
||||||
|
//
|
||||||
|
// This method uses an external converter to properly parse the MSG format
|
||||||
|
// and extract headers, body, and attachments.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filePath: Absolute path to the .msg file
|
||||||
|
// - useExternalConverter: Whether to use external conversion (currently always true)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *internal.EmailData: Parsed email data
|
||||||
|
// - error: Any parsing or conversion errors
|
||||||
|
func (a *App) ReadMSG(filePath string, useExternalConverter bool) (*internal.EmailData, error) {
|
||||||
|
// The useExternalConverter parameter is kept for API compatibility
|
||||||
|
// but the implementation always uses the internal MSG reader
|
||||||
|
return internal.ReadMsgFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMSGOSS reads a .msg file using the open-source parser.
|
||||||
|
// This is an alternative entry point that explicitly uses the OSS implementation.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - filePath: Absolute path to the .msg file
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *internal.EmailData: Parsed email data
|
||||||
|
// - error: Any parsing errors
|
||||||
|
func (a *App) ReadMSGOSS(filePath string) (*internal.EmailData, error) {
|
||||||
|
return internal.ReadMsgFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowOpenFileDialog displays the system file picker dialog filtered for email files.
|
||||||
|
// This allows users to browse and select .eml or .msg files to open.
|
||||||
|
//
|
||||||
|
// The dialog is configured with filters for:
|
||||||
|
// - EML files (*.eml)
|
||||||
|
// - MSG files (*.msg)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The selected file path, or empty string if cancelled
|
||||||
|
// - error: Any dialog errors
|
||||||
|
func (a *App) ShowOpenFileDialog() (string, error) {
|
||||||
|
return internal.ShowFileDialog(a.ctx)
|
||||||
|
}
|
||||||
164
app_screenshot.go
Normal file
164
app_screenshot.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
// Package main provides screenshot functionality for EMLy.
|
||||||
|
// This file contains methods for capturing, saving, and exporting screenshots
|
||||||
|
// of the application window.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"emly/backend/utils"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Screenshot Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ScreenshotResult contains the captured screenshot data and metadata.
|
||||||
|
type ScreenshotResult struct {
|
||||||
|
// Data is the base64-encoded PNG image data
|
||||||
|
Data string `json:"data"`
|
||||||
|
// Width is the image width in pixels
|
||||||
|
Width int `json:"width"`
|
||||||
|
// Height is the image height in pixels
|
||||||
|
Height int `json:"height"`
|
||||||
|
// Filename is the suggested filename for saving
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Screenshot Methods
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// TakeScreenshot captures the current EMLy application window and returns it as base64 PNG.
|
||||||
|
// This uses Windows GDI API to capture the window contents, handling DWM composition
|
||||||
|
// for proper rendering of modern Windows applications.
|
||||||
|
//
|
||||||
|
// The method automatically detects whether the app is in main mode or viewer mode
|
||||||
|
// and captures the appropriate window.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *ScreenshotResult: Contains base64 PNG data, dimensions, and suggested filename
|
||||||
|
// - error: Error if window capture or encoding fails
|
||||||
|
func (a *App) TakeScreenshot() (*ScreenshotResult, error) {
|
||||||
|
// Determine window title based on current mode
|
||||||
|
windowTitle := "EMLy - EML Viewer for 3gIT"
|
||||||
|
|
||||||
|
// Check if running in viewer mode
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if strings.Contains(arg, "--view-image") {
|
||||||
|
windowTitle = "EMLy Image Viewer"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.Contains(arg, "--view-pdf") {
|
||||||
|
windowTitle = "EMLy PDF Viewer"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture the window using Windows GDI API
|
||||||
|
img, err := utils.CaptureWindowByTitle(windowTitle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to capture window: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode to PNG and convert to base64
|
||||||
|
base64Data, err := utils.ScreenshotToBase64PNG(img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build result with metadata
|
||||||
|
bounds := img.Bounds()
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
|
||||||
|
return &ScreenshotResult{
|
||||||
|
Data: base64Data,
|
||||||
|
Width: bounds.Dx(),
|
||||||
|
Height: bounds.Dy(),
|
||||||
|
Filename: fmt.Sprintf("emly_screenshot_%s.png", timestamp),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveScreenshot captures and saves the screenshot to the system temp directory.
|
||||||
|
// This is a convenience method that captures and saves in one step.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The full path to the saved screenshot file
|
||||||
|
// - error: Error if capture or save fails
|
||||||
|
func (a *App) SaveScreenshot() (string, error) {
|
||||||
|
// Capture the screenshot
|
||||||
|
result, err := a.TakeScreenshot()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 data
|
||||||
|
data, err := base64.StdEncoding.DecodeString(result.Data)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode screenshot data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to temp directory
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
filePath := filepath.Join(tempDir, result.Filename)
|
||||||
|
|
||||||
|
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to save screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveScreenshotAs captures a screenshot and opens a save dialog for the user
|
||||||
|
// to choose where to save it.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The selected save path, or empty string if cancelled
|
||||||
|
// - error: Error if capture, dialog, or save fails
|
||||||
|
func (a *App) SaveScreenshotAs() (string, error) {
|
||||||
|
// Capture the screenshot first
|
||||||
|
result, err := a.TakeScreenshot()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open save dialog with PNG filter
|
||||||
|
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||||
|
DefaultFilename: result.Filename,
|
||||||
|
Title: "Save Screenshot",
|
||||||
|
Filters: []runtime.FileFilter{
|
||||||
|
{
|
||||||
|
DisplayName: "PNG Images (*.png)",
|
||||||
|
Pattern: "*.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open save dialog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User cancelled
|
||||||
|
if savePath == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 data
|
||||||
|
data, err := base64.StdEncoding.DecodeString(result.Data)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode screenshot data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to selected location
|
||||||
|
if err := os.WriteFile(savePath, data, 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to save screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return savePath, nil
|
||||||
|
}
|
||||||
100
app_settings.go
Normal file
100
app_settings.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Package main provides settings import/export functionality for EMLy.
|
||||||
|
// This file contains methods for exporting and importing application settings
|
||||||
|
// as JSON files.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Settings Export/Import Methods
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ExportSettings opens a save dialog and exports the provided settings JSON
|
||||||
|
// to the selected file location.
|
||||||
|
//
|
||||||
|
// The dialog is pre-configured with:
|
||||||
|
// - Default filename: emly_settings.json
|
||||||
|
// - Filter for JSON files
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - settingsJSON: The JSON string containing all application settings
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The path where settings were saved, or empty if cancelled
|
||||||
|
// - error: Error if dialog or file operations fail
|
||||||
|
func (a *App) ExportSettings(settingsJSON string) (string, error) {
|
||||||
|
// Open save dialog with JSON filter
|
||||||
|
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||||
|
DefaultFilename: "emly_settings.json",
|
||||||
|
Title: "Export Settings",
|
||||||
|
Filters: []runtime.FileFilter{
|
||||||
|
{
|
||||||
|
DisplayName: "JSON Files (*.json)",
|
||||||
|
Pattern: "*.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open save dialog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User cancelled
|
||||||
|
if savePath == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure .json extension
|
||||||
|
if !strings.HasSuffix(strings.ToLower(savePath), ".json") {
|
||||||
|
savePath += ".json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the settings file
|
||||||
|
if err := os.WriteFile(savePath, []byte(settingsJSON), 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write settings file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return savePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportSettings opens a file dialog for the user to select a settings JSON file
|
||||||
|
// and returns its contents.
|
||||||
|
//
|
||||||
|
// The dialog is configured to only show JSON files.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: The JSON content of the selected file, or empty if cancelled
|
||||||
|
// - error: Error if dialog or file operations fail
|
||||||
|
func (a *App) ImportSettings() (string, error) {
|
||||||
|
// Open file dialog with JSON filter
|
||||||
|
openPath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||||
|
Title: "Import Settings",
|
||||||
|
Filters: []runtime.FileFilter{
|
||||||
|
{
|
||||||
|
DisplayName: "JSON Files (*.json)",
|
||||||
|
Pattern: "*.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open file dialog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User cancelled
|
||||||
|
if openPath == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the settings file
|
||||||
|
data, err := os.ReadFile(openPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read settings file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
146
app_system.go
Normal file
146
app_system.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
// Package main provides system-level utilities for EMLy.
|
||||||
|
// This file contains methods for Windows registry access, character encoding
|
||||||
|
// conversion, and file system operations.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
"golang.org/x/text/encoding/charmap"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Windows Default App Handler
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// CheckIsDefaultEMLHandler checks if EMLy is registered as the default handler
|
||||||
|
// for .eml files in Windows.
|
||||||
|
//
|
||||||
|
// This works by:
|
||||||
|
// 1. Getting the current executable path
|
||||||
|
// 2. Reading the UserChoice registry key for .eml files
|
||||||
|
// 3. Finding the command associated with the chosen ProgId
|
||||||
|
// 4. Comparing the command with our executable
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - bool: True if EMLy is the default handler
|
||||||
|
// - error: Error if registry access fails
|
||||||
|
func (a *App) CheckIsDefaultEMLHandler() (bool, error) {
|
||||||
|
// Get current executable path for comparison
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
exePath = strings.ToLower(exePath)
|
||||||
|
|
||||||
|
// Open the UserChoice key for .eml extension
|
||||||
|
// This is where Windows stores the user's chosen default app
|
||||||
|
k, err := registry.OpenKey(
|
||||||
|
registry.CURRENT_USER,
|
||||||
|
`Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.eml\UserChoice`,
|
||||||
|
registry.QUERY_VALUE,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// Key doesn't exist - user hasn't made a specific choice
|
||||||
|
// or system default is active (which is usually not us)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
|
||||||
|
// Get the ProgId (program identifier) for the chosen app
|
||||||
|
progId, _, err := k.GetStringValue("ProgId")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the command associated with this ProgId
|
||||||
|
classKeyPath := fmt.Sprintf(`%s\shell\open\command`, progId)
|
||||||
|
classKey, err := registry.OpenKey(registry.CLASSES_ROOT, classKeyPath, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("unable to find command for ProgId %s", progId)
|
||||||
|
}
|
||||||
|
defer classKey.Close()
|
||||||
|
|
||||||
|
// Get the command string
|
||||||
|
cmd, _, err := classKey.GetStringValue("")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare command with our executable
|
||||||
|
// Check if the command contains our executable name
|
||||||
|
cmdLower := strings.ToLower(cmd)
|
||||||
|
if strings.Contains(cmdLower, strings.ToLower(filepath.Base(exePath))) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenDefaultAppsSettings opens the Windows Settings app to the Default Apps page.
|
||||||
|
// This allows users to easily set EMLy as the default handler for email files.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if launching settings fails
|
||||||
|
func (a *App) OpenDefaultAppsSettings() error {
|
||||||
|
cmd := exec.Command("cmd", "/c", "start", "ms-settings:defaultapps")
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Character Encoding
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ConvertToUTF8 attempts to convert a string to valid UTF-8.
|
||||||
|
// If the string is already valid UTF-8, it's returned as-is.
|
||||||
|
// Otherwise, it assumes Windows-1252 encoding (common for legacy emails)
|
||||||
|
// and attempts to decode it.
|
||||||
|
//
|
||||||
|
// This is particularly useful for email body content that may have been
|
||||||
|
// encoded with legacy Western European character sets.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - s: The string to convert
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: UTF-8 encoded string
|
||||||
|
func (a *App) ConvertToUTF8(s string) string {
|
||||||
|
// If already valid UTF-8, return as-is
|
||||||
|
if utf8.ValidString(s) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume Windows-1252 (superset of ISO-8859-1)
|
||||||
|
// This is the most common encoding for legacy Western European text
|
||||||
|
decoder := charmap.Windows1252.NewDecoder()
|
||||||
|
decoded, _, err := transform.String(decoder, s)
|
||||||
|
if err != nil {
|
||||||
|
// Return original if decoding fails
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// File System Operations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// OpenFolderInExplorer opens the specified folder in Windows Explorer.
|
||||||
|
// This is used to show the user where bug report files are saved.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - folderPath: The path to the folder to open
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if launching explorer fails
|
||||||
|
func (a *App) OpenFolderInExplorer(folderPath string) error {
|
||||||
|
cmd := exec.Command("explorer", folderPath)
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
429
app_viewer.go
Normal file
429
app_viewer.go
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
// Package main provides viewer window functionality for EMLy.
|
||||||
|
// This file contains methods for opening attachments in viewer windows
|
||||||
|
// or with external applications.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Viewer Data Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// ImageViewerData contains the data needed to display an image in the viewer window.
|
||||||
|
type ImageViewerData struct {
|
||||||
|
// Data is the base64-encoded image data
|
||||||
|
Data string `json:"data"`
|
||||||
|
// Filename is the original filename of the image
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDFViewerData contains the data needed to display a PDF in the viewer window.
|
||||||
|
type PDFViewerData struct {
|
||||||
|
// Data is the base64-encoded PDF data
|
||||||
|
Data string `json:"data"`
|
||||||
|
// Filename is the original filename of the PDF
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewerData is a union type that contains either image or PDF viewer data.
|
||||||
|
// Used by the viewer page to determine which type of content to display.
|
||||||
|
type ViewerData struct {
|
||||||
|
// ImageData is set when viewing an image (mutually exclusive with PDFData)
|
||||||
|
ImageData *ImageViewerData `json:"imageData,omitempty"`
|
||||||
|
// PDFData is set when viewing a PDF (mutually exclusive with ImageData)
|
||||||
|
PDFData *PDFViewerData `json:"pdfData,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Built-in Viewer Window Methods
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// OpenEMLWindow opens an EML attachment in a new EMLy window.
|
||||||
|
// The EML data is saved to a temp file and a new EMLy instance is launched.
|
||||||
|
//
|
||||||
|
// This method tracks open EML files to prevent duplicate windows for the same file.
|
||||||
|
// The tracking is released when the viewer window is closed.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - base64Data: Base64-encoded EML file content
|
||||||
|
// - filename: The original filename of the EML attachment
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if the file is already open or if launching fails
|
||||||
|
func (a *App) OpenEMLWindow(base64Data string, filename string) error {
|
||||||
|
// Check if this EML is already open
|
||||||
|
a.openEMLsMux.Lock()
|
||||||
|
if a.openEMLs[filename] {
|
||||||
|
a.openEMLsMux.Unlock()
|
||||||
|
return fmt.Errorf("eml '%s' is already open", filename)
|
||||||
|
}
|
||||||
|
a.openEMLs[filename] = true
|
||||||
|
a.openEMLsMux.Unlock()
|
||||||
|
|
||||||
|
// Decode base64 data
|
||||||
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
a.openEMLsMux.Lock()
|
||||||
|
delete(a.openEMLs, filename)
|
||||||
|
a.openEMLsMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to decode base64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to temp file with timestamp to avoid conflicts
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s_%s", "emly_attachment", timestamp, filename))
|
||||||
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||||
|
a.openEMLsMux.Lock()
|
||||||
|
delete(a.openEMLs, filename)
|
||||||
|
a.openEMLsMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch new EMLy instance with the file path
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
a.openEMLsMux.Lock()
|
||||||
|
delete(a.openEMLs, filename)
|
||||||
|
a.openEMLsMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to get executable path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(exe, tempFile)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
a.openEMLsMux.Lock()
|
||||||
|
delete(a.openEMLs, filename)
|
||||||
|
a.openEMLsMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to start viewer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor process in background to release lock when closed
|
||||||
|
go func() {
|
||||||
|
cmd.Wait()
|
||||||
|
a.openEMLsMux.Lock()
|
||||||
|
delete(a.openEMLs, filename)
|
||||||
|
a.openEMLsMux.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenImageWindow opens an image attachment in a new EMLy viewer window.
|
||||||
|
// The image data is saved to a temp file and a new EMLy instance is launched
|
||||||
|
// with the --view-image flag.
|
||||||
|
//
|
||||||
|
// This method tracks open images to prevent duplicate windows for the same file.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - base64Data: Base64-encoded image data
|
||||||
|
// - filename: The original filename of the image
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if the image is already open or if launching fails
|
||||||
|
func (a *App) OpenImageWindow(base64Data string, filename string) error {
|
||||||
|
// Check if this image is already open
|
||||||
|
a.openImagesMux.Lock()
|
||||||
|
if a.openImages[filename] {
|
||||||
|
a.openImagesMux.Unlock()
|
||||||
|
return fmt.Errorf("image '%s' is already open", filename)
|
||||||
|
}
|
||||||
|
a.openImages[filename] = true
|
||||||
|
a.openImagesMux.Unlock()
|
||||||
|
|
||||||
|
// Decode base64 data
|
||||||
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
a.openImagesMux.Lock()
|
||||||
|
delete(a.openImages, filename)
|
||||||
|
a.openImagesMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to decode base64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to temp file
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s", timestamp, filename))
|
||||||
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||||
|
a.openImagesMux.Lock()
|
||||||
|
delete(a.openImages, filename)
|
||||||
|
a.openImagesMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch new EMLy instance in image viewer mode
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
a.openImagesMux.Lock()
|
||||||
|
delete(a.openImages, filename)
|
||||||
|
a.openImagesMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to get executable path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(exe, "--view-image="+tempFile)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
a.openImagesMux.Lock()
|
||||||
|
delete(a.openImages, filename)
|
||||||
|
a.openImagesMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to start viewer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor process in background to release lock when closed
|
||||||
|
go func() {
|
||||||
|
cmd.Wait()
|
||||||
|
a.openImagesMux.Lock()
|
||||||
|
delete(a.openImages, filename)
|
||||||
|
a.openImagesMux.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenPDFWindow opens a PDF attachment in a new EMLy viewer window.
|
||||||
|
// The PDF data is saved to a temp file and a new EMLy instance is launched
|
||||||
|
// with the --view-pdf flag.
|
||||||
|
//
|
||||||
|
// This method tracks open PDFs to prevent duplicate windows for the same file.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - base64Data: Base64-encoded PDF data
|
||||||
|
// - filename: The original filename of the PDF
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if the PDF is already open or if launching fails
|
||||||
|
func (a *App) OpenPDFWindow(base64Data string, filename string) error {
|
||||||
|
// Check if this PDF is already open
|
||||||
|
a.openPDFsMux.Lock()
|
||||||
|
if a.openPDFs[filename] {
|
||||||
|
a.openPDFsMux.Unlock()
|
||||||
|
return fmt.Errorf("pdf '%s' is already open", filename)
|
||||||
|
}
|
||||||
|
a.openPDFs[filename] = true
|
||||||
|
a.openPDFsMux.Unlock()
|
||||||
|
|
||||||
|
// Decode base64 data
|
||||||
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
a.openPDFsMux.Lock()
|
||||||
|
delete(a.openPDFs, filename)
|
||||||
|
a.openPDFsMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to decode base64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to temp file
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
tempFile := filepath.Join(tempDir, filename)
|
||||||
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||||
|
a.openPDFsMux.Lock()
|
||||||
|
delete(a.openPDFs, filename)
|
||||||
|
a.openPDFsMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch new EMLy instance in PDF viewer mode
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
a.openPDFsMux.Lock()
|
||||||
|
delete(a.openPDFs, filename)
|
||||||
|
a.openPDFsMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to get executable path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(exe, "--view-pdf="+tempFile)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
a.openPDFsMux.Lock()
|
||||||
|
delete(a.openPDFs, filename)
|
||||||
|
a.openPDFsMux.Unlock()
|
||||||
|
return fmt.Errorf("failed to start viewer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor process in background to release lock when closed
|
||||||
|
go func() {
|
||||||
|
cmd.Wait()
|
||||||
|
a.openPDFsMux.Lock()
|
||||||
|
delete(a.openPDFs, filename)
|
||||||
|
a.openPDFsMux.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// External Application Methods
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// OpenPDF saves a PDF to temp and opens it with the system's default PDF application.
|
||||||
|
// This is used when the user prefers external viewers over the built-in viewer.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - base64Data: Base64-encoded PDF data
|
||||||
|
// - filename: The original filename of the PDF
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if saving or launching fails
|
||||||
|
func (a *App) OpenPDF(base64Data string, filename string) error {
|
||||||
|
if base64Data == "" {
|
||||||
|
return fmt.Errorf("no data provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 data
|
||||||
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode base64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to temp file with timestamp for uniqueness
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s", timestamp, filename))
|
||||||
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open with Windows default application
|
||||||
|
cmd := exec.Command("cmd", "/c", "start", "", tempFile)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to open file: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenImage saves an image to temp and opens it with the system's default image viewer.
|
||||||
|
// This is used when the user prefers external viewers over the built-in viewer.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - base64Data: Base64-encoded image data
|
||||||
|
// - filename: The original filename of the image
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: Error if saving or launching fails
|
||||||
|
func (a *App) OpenImage(base64Data string, filename string) error {
|
||||||
|
if base64Data == "" {
|
||||||
|
return fmt.Errorf("no data provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 data
|
||||||
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode base64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to temp file with timestamp for uniqueness
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s_%s", timestamp, filename))
|
||||||
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open with Windows default application
|
||||||
|
cmd := exec.Command("cmd", "/c", "start", "", tempFile)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to open file: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Viewer Mode Detection
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// GetImageViewerData checks CLI arguments and returns image data if running in image viewer mode.
|
||||||
|
// This is called by the viewer page on startup to get the image to display.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *ImageViewerData: Image data if in viewer mode, nil otherwise
|
||||||
|
// - error: Error if reading the image file fails
|
||||||
|
func (a *App) GetImageViewerData() (*ImageViewerData, error) {
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if strings.HasPrefix(arg, "--view-image=") {
|
||||||
|
filePath := strings.TrimPrefix(arg, "--view-image=")
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read image file: %w", err)
|
||||||
|
}
|
||||||
|
// Return as base64 for consistent frontend handling
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(data)
|
||||||
|
return &ImageViewerData{
|
||||||
|
Data: encoded,
|
||||||
|
Filename: filepath.Base(filePath),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPDFViewerData checks CLI arguments and returns PDF data if running in PDF viewer mode.
|
||||||
|
// This is called by the viewer page on startup to get the PDF to display.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *PDFViewerData: PDF data if in viewer mode, nil otherwise
|
||||||
|
// - error: Error if reading the PDF file fails
|
||||||
|
func (a *App) GetPDFViewerData() (*PDFViewerData, error) {
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if strings.HasPrefix(arg, "--view-pdf=") {
|
||||||
|
filePath := strings.TrimPrefix(arg, "--view-pdf=")
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read PDF file: %w", err)
|
||||||
|
}
|
||||||
|
// Return as base64 for consistent frontend handling
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(data)
|
||||||
|
return &PDFViewerData{
|
||||||
|
Data: encoded,
|
||||||
|
Filename: filepath.Base(filePath),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetViewerData checks CLI arguments and returns viewer data for any viewer mode.
|
||||||
|
// This is a unified method that detects both image and PDF viewer modes.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *ViewerData: Contains either ImageData or PDFData depending on mode
|
||||||
|
// - error: Error if reading the file fails
|
||||||
|
func (a *App) GetViewerData() (*ViewerData, error) {
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
// Check for image viewer mode
|
||||||
|
if strings.HasPrefix(arg, "--view-image=") {
|
||||||
|
filePath := strings.TrimPrefix(arg, "--view-image=")
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read image file: %w", err)
|
||||||
|
}
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(data)
|
||||||
|
return &ViewerData{
|
||||||
|
ImageData: &ImageViewerData{
|
||||||
|
Data: encoded,
|
||||||
|
Filename: filepath.Base(filePath),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for PDF viewer mode
|
||||||
|
if strings.HasPrefix(arg, "--view-pdf=") {
|
||||||
|
filePath := strings.TrimPrefix(arg, "--view-pdf=")
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read PDF file: %w", err)
|
||||||
|
}
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(data)
|
||||||
|
return &ViewerData{
|
||||||
|
PDFData: &PDFViewerData{
|
||||||
|
Data: encoded,
|
||||||
|
Filename: filepath.Base(filePath),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user