This commit is contained in:
Lyz Coote
2026-02-02 18:41:13 +01:00
commit d6a5cb8a67
161 changed files with 8630 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
package utils
import (
"fmt"
"os"
"syscall"
"time"
"unsafe"
)
type FileMetadata struct {
Name string
Size int64
LastModified time.Time
ProductVersion string
FileVersion string
OriginalFilename string
ProductName string
FileDescription string
CompanyName string
}
// GetFileMetadata returns metadata for the given file path.
// It retrieves basic file info and Windows-specific version info if available.
func GetFileMetadata(path string) (*FileMetadata, error) {
fileInfo, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("failed to stat file: %w", err)
}
metadata := &FileMetadata{
Name: fileInfo.Name(),
Size: fileInfo.Size(),
LastModified: fileInfo.ModTime(),
}
// Try to get version info
versionInfo, err := getVersionInfo(path)
if err == nil {
metadata.ProductVersion = versionInfo["ProductVersion"]
metadata.FileVersion = versionInfo["FileVersion"]
metadata.OriginalFilename = versionInfo["OriginalFilename"]
metadata.ProductName = versionInfo["ProductName"]
metadata.FileDescription = versionInfo["FileDescription"]
metadata.CompanyName = versionInfo["CompanyName"]
}
return metadata, nil
}
func getVersionInfo(path string) (map[string]string, error) {
var version = syscall.NewLazyDLL("version.dll")
var getFileVersionInfoSize = version.NewProc("GetFileVersionInfoSizeW")
var getFileVersionInfo = version.NewProc("GetFileVersionInfoW")
var verQueryValue = version.NewProc("VerQueryValueW")
pathPtr, _ := syscall.UTF16PtrFromString(path)
// Get size of version info
size, _, err := getFileVersionInfoSize.Call(uintptr(unsafe.Pointer(pathPtr)), 0)
if size == 0 {
return nil, err
}
// Get version info
info := make([]byte, size)
ret, _, err := getFileVersionInfo.Call(
uintptr(unsafe.Pointer(pathPtr)),
0,
size,
uintptr(unsafe.Pointer(&info[0])),
)
if ret == 0 {
return nil, err
}
// Query language and codepage
var langCodePagePtr *struct {
Language uint16
CodePage uint16
}
var length uint32
subBlock := "\\VarFileInfo\\Translation"
subBlockPtr, _ := syscall.UTF16PtrFromString(subBlock)
ret, _, err = verQueryValue.Call(
uintptr(unsafe.Pointer(&info[0])),
uintptr(unsafe.Pointer(subBlockPtr)),
uintptr(unsafe.Pointer(&langCodePagePtr)),
uintptr(unsafe.Pointer(&length)),
)
if ret == 0 {
return nil, err
}
// Helper to query string values
queryValue := func(key string) string {
query := fmt.Sprintf("\\StringFileInfo\\%04x%04x\\%s", langCodePagePtr.Language, langCodePagePtr.CodePage, key)
queryPtr, _ := syscall.UTF16PtrFromString(query)
var valPtr *uint16
var valLen uint32
ret, _, _ := verQueryValue.Call(
uintptr(unsafe.Pointer(&info[0])),
uintptr(unsafe.Pointer(queryPtr)),
uintptr(unsafe.Pointer(&valPtr)),
uintptr(unsafe.Pointer(&valLen)),
)
if ret != 0 && valLen > 0 {
// valPtr points to a UTF-16 string, create a Go string from it
// We need to iterate until null terminator because valLen includes it
// but syscall.UTF16ToString expects a slice without the terminator if we want clean output,
// or we can just use the pointer.
// However, easier way with unsafe:
return syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(valPtr))[:valLen])
}
return ""
}
results := make(map[string]string)
keys := []string{"ProductVersion", "FileVersion", "OriginalFilename", "ProductName", "FileDescription", "CompanyName"}
for _, key := range keys {
results[key] = queryValue(key)
}
return results, nil
}

View File

@@ -0,0 +1,63 @@
package utils
import (
"log"
"os"
"path/filepath"
"gopkg.in/ini.v1"
)
// Config represents the structure of config.ini
type Config struct {
EMLy EMLyConfig `ini:"EMLy"`
}
type EMLyConfig struct {
SDKDecoderSemver string `ini:"SDK_DECODER_SEMVER"`
SDKDecoderReleaseChannel string `ini:"SDK_DECODER_RELEASE_CHANNEL"`
GUISemver string `ini:"GUI_SEMVER"`
GUIReleaseChannel string `ini:"GUI_RELEASE_CHANNEL"`
}
// LoadConfig reads the config.ini file at the given path and returns a Config struct
func LoadConfig(path string) (*Config, error) {
cfg, err := ini.Load(path)
if err != nil {
log.Printf("Fail to read file: %v", err)
return nil, err
}
config := new(Config)
if err := cfg.MapTo(config); err != nil {
log.Printf("Fail to map config: %v", err)
return nil, err
}
return config, nil
}
func SaveConfig(path string, config *Config) error {
cfg := ini.Empty()
if err := cfg.ReflectFrom(config); err != nil {
log.Printf("Fail to reflect config: %v", err)
return err
}
if err := cfg.SaveTo(path); err != nil {
log.Printf("Fail to save config file: %v", err)
return err
}
return nil
}
func DefaultConfigPath() string {
// Prefer config.ini next to the executable (packaged app), fallback to CWD (dev).
exe, err := os.Executable()
if err == nil {
p := filepath.Join(filepath.Dir(exe), "config.ini")
if _, statErr := os.Stat(p); statErr == nil {
return p
}
}
return "config.ini"
}

View File

@@ -0,0 +1,171 @@
package utils
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"github.com/jaypipes/ghw"
"golang.org/x/sys/windows/registry"
)
type MachineInfo struct {
Hostname string `json:"Hostname"`
OS string `json:"OS"`
Version string `json:"Version"`
HWID string `json:"HWID"`
ExternalIP string `json:"ExternalIP"`
CPU ghw.CPUInfo `json:"CPU"`
RAM ghw.MemoryInfo `json:"RAM"`
GPU ghw.GPUInfo `json:"GPU"`
}
func GetMachineInfo() (*MachineInfo, error) {
info := &MachineInfo{}
// 1. Get Hostname
hostname, err := os.Hostname()
if err != nil {
return nil, fmt.Errorf("failed to get hostname: %w", err)
}
info.Hostname = hostname
// 2. Get OS Info
info.OS = fmt.Sprintf("%s %s", runtime.GOOS, runtime.GOARCH)
// 3. Get Version Info
k, _ := registry.OpenKey(
registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
registry.QUERY_VALUE,
)
defer k.Close()
product, _, _ := k.GetStringValue("ProductName")
build, _, _ := k.GetStringValue("CurrentBuild")
ubr, _, _ := k.GetIntegerValue("UBR")
display, _, _ := k.GetStringValue("DisplayVersion")
edition, _, _ := k.GetStringValue("EditionID")
// Append edition if available
if edition != "" {
product = fmt.Sprintf("%s %s", product, edition)
}
// Split display versione via H (like 23H2, 24H2, 25H2), if its => 23, then its Windows 11, not 10
if strings.HasPrefix(display, "2") {
parts := strings.SplitN(display, "H", 2)
if len(parts) > 0 {
yearPart := parts[0]
if yearPartInt := strings.TrimSpace(yearPart); yearPartInt >= "23" {
product = "Windows 11"
}
}
}
info.Version = fmt.Sprintf("%s %s %s (Build %s.%d)", product, display, edition, build, ubr)
// 3. Get HWID (Windows specific via wmic)
// Fallback or different implementation needed for Linux/Mac if required
if runtime.GOOS == "windows" {
out, err := exec.Command("wmic", "csproduct", "get", "uuid").Output()
if err == nil {
// Parse output which looks like "UUID \n <UUID> \n\n"
lines := strings.Split(string(out), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && trimmed != "UUID" {
info.HWID = trimmed
break
}
}
}
// Fallback to registry MachineGuid if wmic fails or empty
if info.HWID == "" {
// Simplified registry read attempt using reg query command to avoid cgo/syscall complexity for now
// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography -> MachineGuid
out, err := exec.Command("reg", "query", `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography`, "/v", "MachineGuid").Output()
if err == nil {
// Parse output
content := string(out)
if idx := strings.Index(content, "REG_SZ"); idx != -1 {
info.HWID = strings.TrimSpace(content[idx+6:])
}
}
}
} else {
info.HWID = "Not implemented for " + runtime.GOOS
}
// 4. Get External IP
ip, err := getExternalIP()
if err == nil {
info.ExternalIP = ip
} else {
info.ExternalIP = "Unavailable"
}
// 5. Get CPU Info
cpuInfo, err := getCPUInfo()
if err == nil {
info.CPU = *cpuInfo
}
// 6. Get GPU Info
gpuInfo, err := getGPUInfo()
if err == nil {
info.GPU = *gpuInfo
}
// 7. Get RAM Info
ramInfo, err := getRAMInfo()
if err == nil {
info.RAM = *ramInfo
}
return info, nil
}
func getExternalIP() (string, error) {
resp, err := http.Get("https://api.ipify.org?format=text")
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func getCPUInfo() (*ghw.CPUInfo, error) {
cpuInfo, _ := ghw.CPU()
if cpuInfo == nil {
return nil, fmt.Errorf("failed to get CPU info")
}
return cpuInfo, nil
}
func getGPUInfo() (*ghw.GPUInfo, error) {
gpuInfo, err := ghw.GPU()
if gpuInfo == nil {
return nil, fmt.Errorf("failed to get GPU info: %w", err)
}
return gpuInfo, nil
}
func getRAMInfo() (*ghw.MemoryInfo, error) {
memory, err := ghw.Memory()
if memory == nil {
return nil, fmt.Errorf("failed to get RAM info: %w", err)
}
return memory, nil
}

View File

@@ -0,0 +1,101 @@
package internal
import (
"fmt"
"io"
"net/mail"
"os"
"unicode/utf8"
"github.com/DusanKasan/parsemail"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
type EmailAttachment struct {
Filename string `json:"filename"`
ContentType string `json:"contentType"`
Data []byte `json:"data"`
}
type EmailData struct {
From string `json:"from"`
To []string `json:"to"`
Cc []string `json:"cc"`
Bcc []string `json:"bcc"`
Subject string `json:"subject"`
Body string `json:"body"`
Attachments []EmailAttachment `json:"attachments"`
}
func ReadEmlFile(filePath string) (*EmailData, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
email, err := parsemail.Parse(file)
if err != nil {
return nil, fmt.Errorf("failed to parse email: %w", err)
}
// Format addresses
formatAddress := func(addr []*mail.Address) []string {
var result []string
for _, a := range addr {
result = append(result, convertToUTF8(a.String()))
}
return result
}
// Determine body (prefer HTML)
body := email.HTMLBody
if body == "" {
body = email.TextBody
}
// Process attachments
var attachments []EmailAttachment
for _, att := range email.Attachments {
data, err := io.ReadAll(att.Data)
if err != nil {
continue // Handle error or skip? Skipping for now.
}
attachments = append(attachments, EmailAttachment{
Filename: att.Filename,
ContentType: att.ContentType,
Data: data,
})
}
// Format From
var from string
if len(email.From) > 0 {
from = email.From[0].String()
}
return &EmailData{
From: convertToUTF8(from),
To: formatAddress(email.To),
Cc: formatAddress(email.Cc),
Bcc: formatAddress(email.Bcc),
Subject: convertToUTF8(email.Subject),
Body: convertToUTF8(body),
Attachments: attachments,
}, nil
}
func convertToUTF8(s string) string {
if utf8.ValidString(s) {
return s
}
// If invalid UTF-8, assume Windows-1252 (superset of ISO-8859-1)
decoder := charmap.Windows1252.NewDecoder()
decoded, _, err := transform.String(decoder, s)
if err != nil {
return s // Return as-is if decoding fails
}
return decoded
}

View File

@@ -0,0 +1,21 @@
package internal
import (
"context"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
var EMLDialogOptions = runtime.OpenDialogOptions{
Title: "Select EML file",
Filters: []runtime.FileFilter{{DisplayName: "EML Files (*.eml)", Pattern: "*.eml"}},
ShowHiddenFiles: false,
}
func ShowFileDialog(ctx context.Context) (string, error) {
filePath, err := runtime.OpenFileDialog(ctx, EMLDialogOptions)
if err != nil {
return "", err
}
return filePath, nil
}