260 lines
7.0 KiB
Go
260 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
|
|
"emly/backend/utils"
|
|
internal "emly/backend/utils/mail"
|
|
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
// App struct
|
|
type App struct {
|
|
ctx context.Context
|
|
StartupFilePath string
|
|
openImagesMux sync.Mutex
|
|
openImages map[string]bool
|
|
}
|
|
|
|
// NewApp creates a new App application struct
|
|
func NewApp() *App {
|
|
return &App{
|
|
openImages: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
// startup is called when the app starts. The context is saved
|
|
// so we can call the runtime methods
|
|
func (a *App) startup(ctx context.Context) {
|
|
a.ctx = ctx
|
|
Log("Wails startup")
|
|
}
|
|
|
|
func (a *App) GetConfig() *utils.Config {
|
|
cfgPath := utils.DefaultConfigPath()
|
|
cfg, err := utils.LoadConfig(cfgPath)
|
|
if err != nil {
|
|
Log("Failed to load config for version:", err)
|
|
return nil
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func (a *App) SaveConfig(cfg *utils.Config) error {
|
|
cfgPath := utils.DefaultConfigPath()
|
|
if err := utils.SaveConfig(cfgPath, cfg); err != nil {
|
|
Log("Failed to save config:", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *App) shutdown(ctx context.Context) {
|
|
// Best-effort cleanup.
|
|
}
|
|
|
|
func (a *App) QuitApp() {
|
|
runtime.Quit(a.ctx)
|
|
// Generate exit code 138
|
|
os.Exit(133) // 133 + 5 (SIGTRAP)
|
|
}
|
|
|
|
func (a *App) GetMachineData() *utils.MachineInfo {
|
|
data, _ := utils.GetMachineInfo()
|
|
return data
|
|
}
|
|
|
|
// GetStartupFile returns the file path if the app was opened with a file argument
|
|
func (a *App) GetStartupFile() string {
|
|
return a.StartupFilePath
|
|
}
|
|
|
|
// ReadEML reads a .eml file and returns the email data
|
|
func (a *App) ReadEML(filePath string) (*internal.EmailData, error) {
|
|
return internal.ReadEmlFile(filePath)
|
|
}
|
|
|
|
// ShowOpenFileDialog shows the file open dialog for EML files
|
|
func (a *App) ShowOpenFileDialog() (string, error) {
|
|
return internal.ShowFileDialog(a.ctx)
|
|
}
|
|
|
|
// OpenImageWindow opens a new window instance to display the image
|
|
func (a *App) OpenImageWindow(base64Data string, filename string) error {
|
|
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()
|
|
|
|
// 1. Decode base64
|
|
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)
|
|
}
|
|
|
|
// 2. Save to temp file
|
|
tempDir := os.TempDir()
|
|
// Use timestamp to make unique
|
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s", 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)
|
|
}
|
|
|
|
// 3. Launch new instance
|
|
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
|
|
}
|
|
|
|
// OpenPDF saves PDF to temp and opens with default app
|
|
func (a *App) OpenPDF(base64Data string, filename string) error {
|
|
if base64Data == "" {
|
|
return fmt.Errorf("no data provided")
|
|
}
|
|
// 1. Decode base64
|
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode base64: %w", err)
|
|
}
|
|
|
|
// 2. Save to temp file
|
|
tempDir := os.TempDir()
|
|
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s", filename))
|
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write temp file: %w", err)
|
|
}
|
|
|
|
// 3. Open with default app (Windows)
|
|
cmd := exec.Command("cmd", "/c", "start", "", tempFile)
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type ImageViewerData struct {
|
|
Data string `json:"data"`
|
|
Filename string `json:"filename"`
|
|
}
|
|
|
|
// GetImageViewerData checks CLI args and returns image data if in viewer mode
|
|
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 text file: %w", err)
|
|
}
|
|
// Return encoded base64 so frontend can handle it same way
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
return &ImageViewerData{
|
|
Data: encoded,
|
|
Filename: filepath.Base(filePath),
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// CheckIsDefaultEMLHandler verifies if the current executable is the default handler for .eml files
|
|
func (a *App) CheckIsDefaultEMLHandler() (bool, error) {
|
|
// 1. Get current executable path
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
// Normalize path for comparison
|
|
exePath = strings.ToLower(exePath)
|
|
|
|
// 2. Open UserChoice key for .eml
|
|
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 implies user hasn't made a specific choice or system default is active (not us usually)
|
|
return false, nil
|
|
}
|
|
defer k.Close()
|
|
|
|
// 3. Get ProgId
|
|
progId, _, err := k.GetStringValue("ProgId")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// 4. Find the command for 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()
|
|
|
|
cmd, _, err := classKey.GetStringValue("")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// 5. Compare command with our executable
|
|
cmdLower := strings.ToLower(cmd)
|
|
|
|
// Basic check: does the command contain our executable name?
|
|
// In a real scenario, parsing the exact path respecting quotes would be safer,
|
|
// but checking if our specific exe path is present is usually sufficient.
|
|
if strings.Contains(cmdLower, strings.ToLower(filepath.Base(exePath))) {
|
|
// More robust: escape backslashes and check presence
|
|
// cleanExe := strings.ReplaceAll(exePath, `\`, `\\`)
|
|
// For now, depending on how registry stores it (short path vs long path),
|
|
// containment of the filename is a strong indicator if the filename is unique enough (emly.exe)
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// OpenDefaultAppsSettings opens the Windows default apps settings page
|
|
func (a *App) OpenDefaultAppsSettings() error {
|
|
cmd := exec.Command("cmd", "/c", "start", "ms-settings:defaultapps")
|
|
return cmd.Start()
|
|
}
|