v1.0.0
This commit is contained in:
127
backend/utils/file-metadata.go
Normal file
127
backend/utils/file-metadata.go
Normal 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
|
||||
}
|
||||
63
backend/utils/ini-reader.go
Normal file
63
backend/utils/ini-reader.go
Normal 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"
|
||||
}
|
||||
171
backend/utils/machine-identifier.go
Normal file
171
backend/utils/machine-identifier.go
Normal 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
|
||||
}
|
||||
101
backend/utils/mail/eml_reader.go
Normal file
101
backend/utils/mail/eml_reader.go
Normal 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
|
||||
}
|
||||
21
backend/utils/mail/file_dialog.go
Normal file
21
backend/utils/mail/file_dialog.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user