v1.2.2
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -42,3 +42,7 @@ logs/
|
|||||||
# SCS DDLs
|
# SCS DDLs
|
||||||
extra/
|
extra/
|
||||||
extra/*.dll
|
extra/*.dll
|
||||||
|
|
||||||
|
|
||||||
|
*.eml
|
||||||
|
*.msg
|
||||||
93
app.go
93
app.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@ type App struct {
|
|||||||
openImages map[string]bool
|
openImages map[string]bool
|
||||||
openPDFsMux sync.Mutex
|
openPDFsMux sync.Mutex
|
||||||
openPDFs map[string]bool
|
openPDFs map[string]bool
|
||||||
|
openEMLsMux sync.Mutex
|
||||||
|
openEMLs map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp creates a new App application struct
|
// NewApp creates a new App application struct
|
||||||
@@ -33,6 +36,7 @@ func NewApp() *App {
|
|||||||
return &App{
|
return &App{
|
||||||
openImages: make(map[string]bool),
|
openImages: make(map[string]bool),
|
||||||
openPDFs: make(map[string]bool),
|
openPDFs: make(map[string]bool),
|
||||||
|
openEMLs: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +91,89 @@ func (a *App) ReadEML(filePath string) (*internal.EmailData, error) {
|
|||||||
return internal.ReadEmlFile(filePath)
|
return internal.ReadEmlFile(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadPEC reads a PEC .eml file and returns the inner email data
|
||||||
|
func (a *App) ReadPEC(filePath string) (*internal.EmailData, error) {
|
||||||
|
return internal.ReadPecInnerEml(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMSG reads a .msg file and returns the email data
|
||||||
|
func (a *App) ReadMSG(filePath string, useExternalConverter bool) (*internal.EmailData, error) {
|
||||||
|
if useExternalConverter {
|
||||||
|
return internal.ReadMsgFile(filePath)
|
||||||
|
}
|
||||||
|
return internal.OSSReadMsgFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMSGOSS reads a .msg file and returns the email data
|
||||||
|
func (a *App) ReadMSGOSS(filePath string) (*internal.EmailData, error) {
|
||||||
|
return internal.OSSReadMsgFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
// ShowOpenFileDialog shows the file open dialog for EML files
|
// ShowOpenFileDialog shows the file open dialog for EML files
|
||||||
func (a *App) ShowOpenFileDialog() (string, error) {
|
func (a *App) ShowOpenFileDialog() (string, error) {
|
||||||
return internal.ShowFileDialog(a.ctx)
|
return internal.ShowFileDialog(a.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenEMLWindow saves EML to temp and opens a new instance of the app
|
||||||
|
func (a *App) OpenEMLWindow(base64Data string, filename string) error {
|
||||||
|
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()
|
||||||
|
|
||||||
|
// 1. Decode base64
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Save to temp file
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
// Use timestamp or unique ID to avoid conflicts if multiple files have same name
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Launch new instance
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run EMLy with the file path as argument
|
||||||
|
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 a new window instance to display the image
|
// OpenImageWindow opens a new window instance to display the image
|
||||||
func (a *App) OpenImageWindow(base64Data string, filename string) error {
|
func (a *App) OpenImageWindow(base64Data string, filename string) error {
|
||||||
a.openImagesMux.Lock()
|
a.openImagesMux.Lock()
|
||||||
@@ -114,7 +196,8 @@ func (a *App) OpenImageWindow(base64Data string, filename string) error {
|
|||||||
// 2. Save to temp file
|
// 2. Save to temp file
|
||||||
tempDir := os.TempDir()
|
tempDir := os.TempDir()
|
||||||
// Use timestamp to make unique
|
// Use timestamp to make unique
|
||||||
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s", filename))
|
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 {
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||||
a.openImagesMux.Lock()
|
a.openImagesMux.Lock()
|
||||||
delete(a.openImages, filename)
|
delete(a.openImages, filename)
|
||||||
@@ -221,7 +304,9 @@ func (a *App) OpenPDF(base64Data string, filename string) error {
|
|||||||
|
|
||||||
// 2. Save to temp file
|
// 2. Save to temp file
|
||||||
tempDir := os.TempDir()
|
tempDir := os.TempDir()
|
||||||
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s", filename))
|
// Use timestamp to make unique
|
||||||
|
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 {
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write temp file: %w", err)
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -247,7 +332,9 @@ func (a *App) OpenImage(base64Data string, filename string) error {
|
|||||||
|
|
||||||
// 2. Save to temp file
|
// 2. Save to temp file
|
||||||
tempDir := os.TempDir()
|
tempDir := os.TempDir()
|
||||||
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s", filename))
|
// Use timestamp to make unique
|
||||||
|
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 {
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write temp file: %w", err)
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
121
backend/utils/mail/eml_pec_reader.go
Normal file
121
backend/utils/mail/eml_pec_reader.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/mail"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"emly/backend/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadPecInnerEml reads the inner email (postacert.eml) from a PEC EML file.
|
||||||
|
// It opens the outer file, looks for the specific attachment, and parses it.
|
||||||
|
func ReadPecInnerEml(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()
|
||||||
|
|
||||||
|
// 1. Parse outer "Envelope"
|
||||||
|
outerEmail, err := utils.Parse(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse outer email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Look for the real content inside postacert.eml
|
||||||
|
var innerEmailData []byte
|
||||||
|
foundPec := false
|
||||||
|
|
||||||
|
for _, att := range outerEmail.Attachments {
|
||||||
|
// Standard PEC puts the real message in postacert.eml
|
||||||
|
// Using case-insensitive check and substring as per example
|
||||||
|
if strings.Contains(strings.ToLower(att.Filename), "postacert.eml") {
|
||||||
|
data, err := io.ReadAll(att.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read inner email content: %w", err)
|
||||||
|
}
|
||||||
|
innerEmailData = data
|
||||||
|
foundPec = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundPec {
|
||||||
|
return nil, fmt.Errorf("not a signed PEC or 'postacert.eml' attachment is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Parse the inner EML content
|
||||||
|
innerEmail, err := utils.Parse(bytes.NewReader(innerEmailData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse inner email structure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to format addresses (reused logic pattern from eml_reader.go)
|
||||||
|
formatAddress := func(addr []*mail.Address) []string {
|
||||||
|
var result []string
|
||||||
|
for _, a := range addr {
|
||||||
|
// convertToUTF8 is defined in eml_reader.go (same package)
|
||||||
|
result = append(result, convertToUTF8(a.String()))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine body (prefer HTML)
|
||||||
|
body := innerEmail.HTMLBody
|
||||||
|
if body == "" {
|
||||||
|
body = innerEmail.TextBody
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process attachments of the inner email
|
||||||
|
var attachments []EmailAttachment
|
||||||
|
var hasDatiCert, hasSmime, hasInnerPecEmail bool
|
||||||
|
|
||||||
|
for _, att := range innerEmail.Attachments {
|
||||||
|
data, err := io.ReadAll(att.Data)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check internal flags for the inner email (recursive PEC check?)
|
||||||
|
filenameLower := strings.ToLower(att.Filename)
|
||||||
|
if filenameLower == "daticert.xml" {
|
||||||
|
hasDatiCert = true
|
||||||
|
}
|
||||||
|
if filenameLower == "smime.p7s" {
|
||||||
|
hasSmime = true
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(filenameLower, ".eml") {
|
||||||
|
hasInnerPecEmail = true
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments = append(attachments, EmailAttachment{
|
||||||
|
Filename: att.Filename,
|
||||||
|
ContentType: att.ContentType,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isPec := hasDatiCert && hasSmime
|
||||||
|
|
||||||
|
// Format From
|
||||||
|
var from string
|
||||||
|
if len(innerEmail.From) > 0 {
|
||||||
|
from = innerEmail.From[0].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EmailData{
|
||||||
|
From: convertToUTF8(from),
|
||||||
|
To: formatAddress(innerEmail.To),
|
||||||
|
Cc: formatAddress(innerEmail.Cc),
|
||||||
|
Bcc: formatAddress(innerEmail.Bcc),
|
||||||
|
Subject: convertToUTF8(innerEmail.Subject),
|
||||||
|
Body: convertToUTF8(body),
|
||||||
|
Attachments: attachments,
|
||||||
|
IsPec: isPec,
|
||||||
|
HasInnerEmail: hasInnerPecEmail,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -5,9 +5,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/DusanKasan/parsemail"
|
"emly/backend/utils"
|
||||||
|
|
||||||
"golang.org/x/text/encoding/charmap"
|
"golang.org/x/text/encoding/charmap"
|
||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
@@ -26,6 +28,8 @@ type EmailData struct {
|
|||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Body string `json:"body"`
|
Body string `json:"body"`
|
||||||
Attachments []EmailAttachment `json:"attachments"`
|
Attachments []EmailAttachment `json:"attachments"`
|
||||||
|
IsPec bool `json:"isPec"`
|
||||||
|
HasInnerEmail bool `json:"hasInnerEmail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadEmlFile(filePath string) (*EmailData, error) {
|
func ReadEmlFile(filePath string) (*EmailData, error) {
|
||||||
@@ -35,7 +39,7 @@ func ReadEmlFile(filePath string) (*EmailData, error) {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
email, err := parsemail.Parse(file)
|
email, err := utils.Parse(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse email: %w", err)
|
return nil, fmt.Errorf("failed to parse email: %w", err)
|
||||||
}
|
}
|
||||||
@@ -55,13 +59,28 @@ func ReadEmlFile(filePath string) (*EmailData, error) {
|
|||||||
body = email.TextBody
|
body = email.TextBody
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process attachments
|
// Process attachments and detect PEC
|
||||||
var attachments []EmailAttachment
|
var attachments []EmailAttachment
|
||||||
|
var hasDatiCert, hasSmime, hasInnerEmail bool
|
||||||
|
|
||||||
for _, att := range email.Attachments {
|
for _, att := range email.Attachments {
|
||||||
data, err := io.ReadAll(att.Data)
|
data, err := io.ReadAll(att.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue // Handle error or skip? Skipping for now.
|
continue // Handle error or skip? Skipping for now.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PEC Detection Logic
|
||||||
|
filenameLower := strings.ToLower(att.Filename)
|
||||||
|
if filenameLower == "daticert.xml" {
|
||||||
|
hasDatiCert = true
|
||||||
|
}
|
||||||
|
if filenameLower == "smime.p7s" {
|
||||||
|
hasSmime = true
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(filenameLower, ".eml") {
|
||||||
|
hasInnerEmail = true
|
||||||
|
}
|
||||||
|
|
||||||
attachments = append(attachments, EmailAttachment{
|
attachments = append(attachments, EmailAttachment{
|
||||||
Filename: att.Filename,
|
Filename: att.Filename,
|
||||||
ContentType: att.ContentType,
|
ContentType: att.ContentType,
|
||||||
@@ -69,6 +88,8 @@ func ReadEmlFile(filePath string) (*EmailData, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isPec := hasDatiCert && hasSmime
|
||||||
|
|
||||||
// Format From
|
// Format From
|
||||||
var from string
|
var from string
|
||||||
if len(email.From) > 0 {
|
if len(email.From) > 0 {
|
||||||
@@ -83,6 +104,8 @@ func ReadEmlFile(filePath string) (*EmailData, error) {
|
|||||||
Subject: convertToUTF8(email.Subject),
|
Subject: convertToUTF8(email.Subject),
|
||||||
Body: convertToUTF8(body),
|
Body: convertToUTF8(body),
|
||||||
Attachments: attachments,
|
Attachments: attachments,
|
||||||
|
IsPec: isPec,
|
||||||
|
HasInnerEmail: hasInnerEmail,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,18 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var EMLDialogOptions = runtime.OpenDialogOptions{
|
var EmailDialogOptions = runtime.OpenDialogOptions{
|
||||||
Title: "Select EML file",
|
Title: "Select Email file",
|
||||||
Filters: []runtime.FileFilter{{DisplayName: "EML Files (*.eml)", Pattern: "*.eml"}},
|
Filters: []runtime.FileFilter{
|
||||||
|
{DisplayName: "Email Files (*.eml;*.msg)", Pattern: "*.eml;*.msg"},
|
||||||
|
{DisplayName: "EML Files (*.eml)", Pattern: "*.eml"},
|
||||||
|
{DisplayName: "MSG Files (*.msg)", Pattern: "*.msg"},
|
||||||
|
},
|
||||||
ShowHiddenFiles: false,
|
ShowHiddenFiles: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShowFileDialog(ctx context.Context) (string, error) {
|
func ShowFileDialog(ctx context.Context) (string, error) {
|
||||||
filePath, err := runtime.OpenFileDialog(ctx, EMLDialogOptions)
|
filePath, err := runtime.OpenFileDialog(ctx, EmailDialogOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
261
backend/utils/mail/msg_parser.go
Normal file
261
backend/utils/mail/msg_parser.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/richardlehane/mscfb"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MAPI Property Tags
|
||||||
|
const (
|
||||||
|
prSubject = "0037"
|
||||||
|
prBody = "1000"
|
||||||
|
prBodyHTML = "1013"
|
||||||
|
prSenderName = "0C1A"
|
||||||
|
prSenderEmail = "0C1F"
|
||||||
|
prDisplayTo = "0E04" // Display list of To recipients
|
||||||
|
prDisplayCc = "0E03"
|
||||||
|
prDisplayBcc = "0E02"
|
||||||
|
prMessageHeaders = "007D"
|
||||||
|
prClientSubmitTime = "0039" // Date
|
||||||
|
prAttachLongFilename = "3707"
|
||||||
|
prAttachFilename = "3704"
|
||||||
|
prAttachData = "3701"
|
||||||
|
prAttachMimeTag = "370E"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MAPI Property Types
|
||||||
|
const (
|
||||||
|
ptUnicode = "001F"
|
||||||
|
ptString8 = "001E"
|
||||||
|
ptBinary = "0102"
|
||||||
|
)
|
||||||
|
|
||||||
|
type msgParser struct {
|
||||||
|
reader *mscfb.Reader
|
||||||
|
props map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMsgFile(filePath string) (*EmailData, error) {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
doc, err := mscfb.New(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
email := &EmailData{
|
||||||
|
To: []string{},
|
||||||
|
Cc: []string{},
|
||||||
|
Bcc: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to iterate through the entries to find properties and attachments
|
||||||
|
// Since mscfb is a sequential reader, we might need to be careful.
|
||||||
|
// However, usually properties are in streams.
|
||||||
|
|
||||||
|
// Strategy:
|
||||||
|
// 1. Read all streams into a map keyed by their path/name for easier access?
|
||||||
|
// MSG files can be large (attachments), so maybe not all.
|
||||||
|
// 2. Identify properties from their stream names directly.
|
||||||
|
|
||||||
|
// Simplified approach: scan for stream names matching our patterns.
|
||||||
|
|
||||||
|
// Better approach:
|
||||||
|
// The Root Entry has "properties".
|
||||||
|
// We need to detect if we are in an attachment storage.
|
||||||
|
|
||||||
|
// Since mscfb iterates flat (Post-Order?), we can track context?
|
||||||
|
// mscfb File struct provides Name and path.
|
||||||
|
|
||||||
|
attachmentsMap := make(map[string]*EmailAttachment)
|
||||||
|
|
||||||
|
for entry, err := doc.Next(); err == nil; entry, err = doc.Next() {
|
||||||
|
name := entry.Name
|
||||||
|
|
||||||
|
// Check if it's a property stream
|
||||||
|
if strings.HasPrefix(name, "__substg1.0_") {
|
||||||
|
path := entry.Path // Path is array of directory names
|
||||||
|
|
||||||
|
// Root properties
|
||||||
|
if len(path) == 0 { // In root
|
||||||
|
val, err := io.ReadAll(doc)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processRootProperty(name, val, email)
|
||||||
|
} else if strings.HasPrefix(path[len(path)-1], "__attach_version1.0_") {
|
||||||
|
// Attachment property
|
||||||
|
attachStorageName := path[len(path)-1]
|
||||||
|
if _, exists := attachmentsMap[attachStorageName]; !exists {
|
||||||
|
attachmentsMap[attachStorageName] = &EmailAttachment{}
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := io.ReadAll(doc)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processAttachProperty(name, val, attachmentsMap[attachStorageName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize attachments
|
||||||
|
for _, att := range attachmentsMap {
|
||||||
|
if strings.Contains(strings.ToLower(att.ContentType), "multipart/signed") {
|
||||||
|
dataStr := string(att.Data)
|
||||||
|
// Check if it already looks like a plain text EML (contains typical headers)
|
||||||
|
if strings.Contains(dataStr, "Content-Type:") || strings.Contains(dataStr, "MIME-Version:") || strings.Contains(dataStr, "From:") {
|
||||||
|
if !strings.HasSuffix(strings.ToLower(att.Filename), ".eml") {
|
||||||
|
att.Filename += ".eml"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try to decode as Base64
|
||||||
|
// Clean up the base64 string: remove newlines and spaces
|
||||||
|
base64Str := strings.Map(func(r rune) rune {
|
||||||
|
if r == '\r' || r == '\n' || r == ' ' || r == '\t' {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, dataStr)
|
||||||
|
|
||||||
|
// Try standard decoding
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(base64Str)
|
||||||
|
if err != nil {
|
||||||
|
// Try raw decoding (no padding)
|
||||||
|
decoded, err = base64.RawStdEncoding.DecodeString(base64Str)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
att.Data = decoded
|
||||||
|
if !strings.HasSuffix(strings.ToLower(att.Filename), ".eml") {
|
||||||
|
att.Filename += ".eml"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Failed to decode multipart/signed attachment:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if att.Filename == "" {
|
||||||
|
att.Filename = "attachment"
|
||||||
|
}
|
||||||
|
// Only add if we have data
|
||||||
|
if len(att.Data) > 0 {
|
||||||
|
email.Attachments = append(email.Attachments, *att)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return email, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processRootProperty(name string, data []byte, email *EmailData) {
|
||||||
|
tag := name[12:16]
|
||||||
|
typ := name[16:20]
|
||||||
|
|
||||||
|
strVal := ""
|
||||||
|
if typ == ptUnicode {
|
||||||
|
strVal = decodeUTF16(data)
|
||||||
|
} else if typ == ptString8 {
|
||||||
|
strVal = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case prSubject:
|
||||||
|
email.Subject = strVal
|
||||||
|
case prBody:
|
||||||
|
if email.Body == "" { // Prefer body if not set
|
||||||
|
email.Body = strVal
|
||||||
|
}
|
||||||
|
case prBodyHTML:
|
||||||
|
email.Body = strVal // Prefer HTML
|
||||||
|
case prSenderName:
|
||||||
|
if email.From == "" {
|
||||||
|
email.From = strVal
|
||||||
|
} else {
|
||||||
|
email.From = fmt.Sprintf("%s <%s>", strVal, email.From)
|
||||||
|
}
|
||||||
|
case prSenderEmail:
|
||||||
|
if email.From == "" {
|
||||||
|
email.From = strVal
|
||||||
|
} else if !strings.Contains(email.From, "<") {
|
||||||
|
email.From = fmt.Sprintf("%s <%s>", email.From, strVal)
|
||||||
|
}
|
||||||
|
case prDisplayTo:
|
||||||
|
// Split by ; or similar if needed, but display string is usually one line
|
||||||
|
email.To = splitAndTrim(strVal)
|
||||||
|
case prDisplayCc:
|
||||||
|
email.Cc = splitAndTrim(strVal)
|
||||||
|
case prDisplayBcc:
|
||||||
|
email.Bcc = splitAndTrim(strVal)
|
||||||
|
case prClientSubmitTime:
|
||||||
|
// Date logic to be added if struct supports it
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if tag == prClientSubmitTime && typ == "0040" {
|
||||||
|
if len(data) >= 8 {
|
||||||
|
ft := binary.LittleEndian.Uint64(data)
|
||||||
|
t := time.Date(1601, 1, 1, 0, 0, 0, 0, time.UTC).Add(time.Duration(ft) * 100 * time.Nanosecond)
|
||||||
|
email.Date = t.Format(time.RFC1123Z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAttachProperty(name string, data []byte, att *EmailAttachment) {
|
||||||
|
tag := name[12:16]
|
||||||
|
typ := name[16:20]
|
||||||
|
|
||||||
|
strVal := ""
|
||||||
|
if typ == ptUnicode {
|
||||||
|
strVal = decodeUTF16(data)
|
||||||
|
} else if typ == ptString8 {
|
||||||
|
strVal = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case prAttachLongFilename:
|
||||||
|
att.Filename = strVal
|
||||||
|
case prAttachFilename:
|
||||||
|
if att.Filename == "" {
|
||||||
|
att.Filename = strVal
|
||||||
|
}
|
||||||
|
case prAttachMimeTag:
|
||||||
|
att.ContentType = strVal
|
||||||
|
case prAttachData:
|
||||||
|
att.Data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUTF16(b []byte) string {
|
||||||
|
decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()
|
||||||
|
decoded, _, _ := transform.Bytes(decoder, b)
|
||||||
|
// Remove null terminators if present
|
||||||
|
return strings.TrimRight(string(decoded), "\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitAndTrim(s string) []string {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(s, ";")
|
||||||
|
var res []string
|
||||||
|
for _, p := range parts {
|
||||||
|
t := strings.TrimSpace(p)
|
||||||
|
if t != "" {
|
||||||
|
res = append(res, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
148
backend/utils/mail/msg_reader.go
Normal file
148
backend/utils/mail/msg_reader.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadMsgFile reads a .msg file using the native Go parser.
|
||||||
|
func ReadMsgFile(filePath string) (*EmailData, error) {
|
||||||
|
return ReadMsgPecFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OSSReadMsgFile(filePath string) (*EmailData, error) {
|
||||||
|
return parseMsgFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSignedMsgExec executes 'signed_msg.exe' via cmd to convert a MSG to JSON,
|
||||||
|
// then processes the output to reconstruct the PEC email data.
|
||||||
|
func ReadMsgPecFile(filePath string) (*EmailData, error) {
|
||||||
|
fmt.Println("Called!")
|
||||||
|
// 1. Locate signed_msg.exe
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||||
|
}
|
||||||
|
baseDir := filepath.Dir(exePath)
|
||||||
|
helperExe := filepath.Join(baseDir, "signed_msg.exe")
|
||||||
|
|
||||||
|
fmt.Println(helperExe)
|
||||||
|
|
||||||
|
// 2. Create temp file for JSON output
|
||||||
|
// Using generic temp file naming with timestamp
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
tempFile, err := os.CreateTemp("", fmt.Sprintf("pec_output_%s_*.json", timestamp))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
tempPath := tempFile.Name()
|
||||||
|
tempFile.Close() // Close immediately, exe will write to it
|
||||||
|
// defer os.Remove(tempPath) // Cleanup
|
||||||
|
|
||||||
|
// 3. Run signed_msg.exe <msgPath> <jsonPath>
|
||||||
|
// Use exec.Command
|
||||||
|
// Note: Command might need to be "cmd", "/C", ... but usually direct execution works on Windows
|
||||||
|
fmt.Println(helperExe, filePath, tempPath)
|
||||||
|
cmd := exec.Command(helperExe, filePath, tempPath)
|
||||||
|
// Hide window?
|
||||||
|
// cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} // Requires syscall import
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("signed_msg.exe failed: %s, output: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Read JSON output
|
||||||
|
jsonData, err := os.ReadFile(tempPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read json output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Parse JSON
|
||||||
|
var pecJson struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
From string `json:"from"`
|
||||||
|
To []string `json:"to"`
|
||||||
|
Cc []string `json:"cc"`
|
||||||
|
Bcc []string `json:"bcc"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
HtmlBody string `json:"htmlBody"`
|
||||||
|
Attachments []struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
ContentType string `json:"contentType"`
|
||||||
|
Data string `json:"data"` // Base64
|
||||||
|
DataFormat string `json:"dataFormat"` // "base64" (optional)
|
||||||
|
} `json:"attachments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(jsonData, &pecJson); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse json output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Check for postacert.eml to determine if it is a PEC
|
||||||
|
var foundPostacert bool
|
||||||
|
var hasDatiCert, hasSmime bool
|
||||||
|
|
||||||
|
// We'll prepare attachments listing at the same time
|
||||||
|
var attachments []EmailAttachment
|
||||||
|
|
||||||
|
for _, att := range pecJson.Attachments {
|
||||||
|
attData, err := base64.StdEncoding.DecodeString(att.Data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to decode attachment %s: %v\n", att.Filename, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filenameLower := strings.ToLower(att.Filename)
|
||||||
|
if filenameLower == "postacert.eml" {
|
||||||
|
foundPostacert = true
|
||||||
|
}
|
||||||
|
if filenameLower == "daticert.xml" {
|
||||||
|
hasDatiCert = true
|
||||||
|
}
|
||||||
|
if filenameLower == "smime.p7s" {
|
||||||
|
hasSmime = true
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments = append(attachments, EmailAttachment{
|
||||||
|
Filename: att.Filename,
|
||||||
|
ContentType: att.ContentType,
|
||||||
|
Data: attData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundPostacert {
|
||||||
|
// Maybe its a normal MSG, continue to try to parse it as a regular email
|
||||||
|
normalMsgEmail, err := parseMsgFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find postacert.eml and also failed to parse as normal MSG: %w", err)
|
||||||
|
}
|
||||||
|
return normalMsgEmail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. It is a PEC. Return the outer message (wrapper)
|
||||||
|
// so the user can see the PEC envelope and attachments (postacert.eml, etc.)
|
||||||
|
|
||||||
|
body := pecJson.HtmlBody
|
||||||
|
if body == "" {
|
||||||
|
body = pecJson.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EmailData{
|
||||||
|
From: convertToUTF8(pecJson.From),
|
||||||
|
To: pecJson.To, // Assuming format is already correct or compatible
|
||||||
|
Cc: pecJson.Cc,
|
||||||
|
Bcc: pecJson.Bcc,
|
||||||
|
Subject: convertToUTF8(pecJson.Subject),
|
||||||
|
Body: convertToUTF8(body),
|
||||||
|
Attachments: attachments,
|
||||||
|
IsPec: hasDatiCert || hasSmime, // Typical PEC indicators
|
||||||
|
HasInnerEmail: foundPostacert,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
512
backend/utils/mailparser.go
Normal file
512
backend/utils/mailparser.go
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const contentTypeMultipartMixed = "multipart/mixed"
|
||||||
|
const contentTypeMultipartAlternative = "multipart/alternative"
|
||||||
|
const contentTypeMultipartRelated = "multipart/related"
|
||||||
|
const contentTypeTextHtml = "text/html"
|
||||||
|
const contentTypeTextPlain = "text/plain"
|
||||||
|
|
||||||
|
// Parse an email message read from io.Reader into parsemail.Email struct
|
||||||
|
func Parse(r io.Reader) (email Email, err error) {
|
||||||
|
msg, err := mail.ReadMessage(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err = createEmailFromHeader(msg.Header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
email.ContentType = msg.Header.Get("Content-Type")
|
||||||
|
contentType, params, err := parseContentType(email.ContentType)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeMultipartMixed, "multipart/signed":
|
||||||
|
email.TextBody, email.HTMLBody, email.Attachments, email.EmbeddedFiles, err = parseMultipartMixed(msg.Body, params["boundary"])
|
||||||
|
case contentTypeMultipartAlternative:
|
||||||
|
email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartAlternative(msg.Body, params["boundary"])
|
||||||
|
case contentTypeMultipartRelated:
|
||||||
|
email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartRelated(msg.Body, params["boundary"])
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
message, _ := ioutil.ReadAll(msg.Body)
|
||||||
|
email.TextBody = strings.TrimSuffix(string(message[:]), "\n")
|
||||||
|
case contentTypeTextHtml:
|
||||||
|
message, _ := ioutil.ReadAll(msg.Body)
|
||||||
|
email.HTMLBody = strings.TrimSuffix(string(message[:]), "\n")
|
||||||
|
default:
|
||||||
|
email.Content, err = decodeContent(msg.Body, msg.Header.Get("Content-Transfer-Encoding"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEmailFromHeader(header mail.Header) (email Email, err error) {
|
||||||
|
hp := headerParser{header: &header}
|
||||||
|
|
||||||
|
email.Subject = decodeMimeSentence(header.Get("Subject"))
|
||||||
|
email.From = hp.parseAddressList(header.Get("From"))
|
||||||
|
email.Sender = hp.parseAddress(header.Get("Sender"))
|
||||||
|
email.ReplyTo = hp.parseAddressList(header.Get("Reply-To"))
|
||||||
|
email.To = hp.parseAddressList(header.Get("To"))
|
||||||
|
email.Cc = hp.parseAddressList(header.Get("Cc"))
|
||||||
|
email.Bcc = hp.parseAddressList(header.Get("Bcc"))
|
||||||
|
email.Date = hp.parseTime(header.Get("Date"))
|
||||||
|
email.ResentFrom = hp.parseAddressList(header.Get("Resent-From"))
|
||||||
|
email.ResentSender = hp.parseAddress(header.Get("Resent-Sender"))
|
||||||
|
email.ResentTo = hp.parseAddressList(header.Get("Resent-To"))
|
||||||
|
email.ResentCc = hp.parseAddressList(header.Get("Resent-Cc"))
|
||||||
|
email.ResentBcc = hp.parseAddressList(header.Get("Resent-Bcc"))
|
||||||
|
email.ResentMessageID = hp.parseMessageId(header.Get("Resent-Message-ID"))
|
||||||
|
email.MessageID = hp.parseMessageId(header.Get("Message-ID"))
|
||||||
|
email.InReplyTo = hp.parseMessageIdList(header.Get("In-Reply-To"))
|
||||||
|
email.References = hp.parseMessageIdList(header.Get("References"))
|
||||||
|
email.ResentDate = hp.parseTime(header.Get("Resent-Date"))
|
||||||
|
|
||||||
|
if hp.err != nil {
|
||||||
|
err = hp.err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//decode whole header for easier access to extra fields
|
||||||
|
//todo: should we decode? aren't only standard fields mime encoded?
|
||||||
|
email.Header, err = decodeHeaderMime(header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseContentType(contentTypeHeader string) (contentType string, params map[string]string, err error) {
|
||||||
|
if contentTypeHeader == "" {
|
||||||
|
contentType = contentTypeTextPlain
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return mime.ParseMediaType(contentTypeHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartRelated(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
pmr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := pmr.NextPart()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeTextHtml:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeMultipartAlternative:
|
||||||
|
tb, hb, ef, err := parseMultipartAlternative(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += hb
|
||||||
|
textBody += tb
|
||||||
|
embeddedFiles = append(embeddedFiles, ef...)
|
||||||
|
default:
|
||||||
|
if isEmbeddedFile(part) {
|
||||||
|
ef, err := decodeEmbeddedFile(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedFiles = append(embeddedFiles, ef)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/related inner mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartAlternative(msg io.Reader, boundary string) (textBody, htmlBody string, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
pmr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := pmr.NextPart()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case contentTypeTextPlain:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeTextHtml:
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
case contentTypeMultipartRelated:
|
||||||
|
tb, hb, ef, err := parseMultipartRelated(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += hb
|
||||||
|
textBody += tb
|
||||||
|
embeddedFiles = append(embeddedFiles, ef...)
|
||||||
|
default:
|
||||||
|
if isEmbeddedFile(part) {
|
||||||
|
ef, err := decodeEmbeddedFile(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedFiles = append(embeddedFiles, ef)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, embeddedFiles, fmt.Errorf("Can't process multipart/alternative inner mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMultipartMixed(msg io.Reader, boundary string) (textBody, htmlBody string, attachments []Attachment, embeddedFiles []EmbeddedFile, err error) {
|
||||||
|
mr := multipart.NewReader(msg, boundary)
|
||||||
|
for {
|
||||||
|
part, err := mr.NextPart()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, params, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType == contentTypeMultipartMixed {
|
||||||
|
textBody, htmlBody, attachments, embeddedFiles, err = parseMultipartMixed(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
} else if contentType == contentTypeMultipartAlternative {
|
||||||
|
textBody, htmlBody, embeddedFiles, err = parseMultipartAlternative(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
} else if contentType == contentTypeMultipartRelated {
|
||||||
|
textBody, htmlBody, embeddedFiles, err = parseMultipartRelated(part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
} else if contentType == contentTypeTextPlain {
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
} else if contentType == contentTypeTextHtml {
|
||||||
|
ppContent, err := ioutil.ReadAll(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody += strings.TrimSuffix(string(ppContent[:]), "\n")
|
||||||
|
} else if isAttachment(part) {
|
||||||
|
at, err := decodeAttachment(part)
|
||||||
|
if err != nil {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments = append(attachments, at)
|
||||||
|
} else {
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, fmt.Errorf("Unknown multipart/mixed nested mime type: %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBody, htmlBody, attachments, embeddedFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMimeSentence(s string) string {
|
||||||
|
result := []string{}
|
||||||
|
ss := strings.Split(s, " ")
|
||||||
|
|
||||||
|
for _, word := range ss {
|
||||||
|
dec := new(mime.WordDecoder)
|
||||||
|
w, err := dec.Decode(word)
|
||||||
|
if err != nil {
|
||||||
|
if len(result) == 0 {
|
||||||
|
w = word
|
||||||
|
} else {
|
||||||
|
w = " " + word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeHeaderMime(header mail.Header) (mail.Header, error) {
|
||||||
|
parsedHeader := map[string][]string{}
|
||||||
|
|
||||||
|
for headerName, headerData := range header {
|
||||||
|
|
||||||
|
parsedHeaderData := []string{}
|
||||||
|
for _, headerValue := range headerData {
|
||||||
|
parsedHeaderData = append(parsedHeaderData, decodeMimeSentence(headerValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedHeader[headerName] = parsedHeaderData
|
||||||
|
}
|
||||||
|
|
||||||
|
return mail.Header(parsedHeader), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmbeddedFile(part *multipart.Part) bool {
|
||||||
|
return part.Header.Get("Content-Transfer-Encoding") != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEmbeddedFile(part *multipart.Part) (ef EmbeddedFile, err error) {
|
||||||
|
cid := decodeMimeSentence(part.Header.Get("Content-Id"))
|
||||||
|
decoded, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ef.CID = strings.Trim(cid, "<>")
|
||||||
|
ef.Data = decoded
|
||||||
|
ef.ContentType = part.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAttachment(part *multipart.Part) bool {
|
||||||
|
return part.FileName() != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeAttachment(part *multipart.Part) (at Attachment, err error) {
|
||||||
|
filename := decodeMimeSentence(part.FileName())
|
||||||
|
decoded, err := decodeContent(part, part.Header.Get("Content-Transfer-Encoding"))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
at.Filename = filename
|
||||||
|
at.Data = decoded
|
||||||
|
at.ContentType = strings.Split(part.Header.Get("Content-Type"), ";")[0]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeContent(content io.Reader, encoding string) (io.Reader, error) {
|
||||||
|
switch encoding {
|
||||||
|
case "base64":
|
||||||
|
decoded := base64.NewDecoder(base64.StdEncoding, content)
|
||||||
|
b, err := ioutil.ReadAll(decoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewReader(b), nil
|
||||||
|
case "7bit":
|
||||||
|
dd, err := ioutil.ReadAll(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewReader(dd), nil
|
||||||
|
case "8bit", "binary", "":
|
||||||
|
dd, err := ioutil.ReadAll(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewReader(dd), nil
|
||||||
|
case "quoted-printable":
|
||||||
|
decoded := quotedprintable.NewReader(content)
|
||||||
|
dd, err := ioutil.ReadAll(decoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewReader(dd), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown encoding: %s", encoding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type headerParser struct {
|
||||||
|
header *mail.Header
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseAddress(s string) (ma *mail.Address) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Trim(s, " \n") != "" {
|
||||||
|
ma, hp.err = mail.ParseAddress(s)
|
||||||
|
|
||||||
|
return ma
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseAddressList(s string) (ma []*mail.Address) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Trim(s, " \n") != "" {
|
||||||
|
ma, hp.err = mail.ParseAddressList(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseTime(s string) (t time.Time) {
|
||||||
|
if hp.err != nil || s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formats := []string{
|
||||||
|
time.RFC1123Z,
|
||||||
|
"Mon, 2 Jan 2006 15:04:05 -0700",
|
||||||
|
time.RFC1123Z + " (MST)",
|
||||||
|
"Mon, 2 Jan 2006 15:04:05 -0700 (MST)",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
t, hp.err = time.Parse(format, s)
|
||||||
|
if hp.err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseMessageId(s string) string {
|
||||||
|
if hp.err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Trim(s, "<> ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hp headerParser) parseMessageIdList(s string) (result []string) {
|
||||||
|
if hp.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range strings.Split(s, " ") {
|
||||||
|
if strings.Trim(p, " \n") != "" {
|
||||||
|
result = append(result, hp.parseMessageId(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment with filename, content type and data (as a io.Reader)
|
||||||
|
type Attachment struct {
|
||||||
|
Filename string
|
||||||
|
ContentType string
|
||||||
|
Data io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedFile with content id, content type and data (as a io.Reader)
|
||||||
|
type EmbeddedFile struct {
|
||||||
|
CID string
|
||||||
|
ContentType string
|
||||||
|
Data io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email with fields for all the headers defined in RFC5322 with it's attachments and
|
||||||
|
type Email struct {
|
||||||
|
Header mail.Header
|
||||||
|
|
||||||
|
Subject string
|
||||||
|
Sender *mail.Address
|
||||||
|
From []*mail.Address
|
||||||
|
ReplyTo []*mail.Address
|
||||||
|
To []*mail.Address
|
||||||
|
Cc []*mail.Address
|
||||||
|
Bcc []*mail.Address
|
||||||
|
Date time.Time
|
||||||
|
MessageID string
|
||||||
|
InReplyTo []string
|
||||||
|
References []string
|
||||||
|
|
||||||
|
ResentFrom []*mail.Address
|
||||||
|
ResentSender *mail.Address
|
||||||
|
ResentTo []*mail.Address
|
||||||
|
ResentDate time.Time
|
||||||
|
ResentCc []*mail.Address
|
||||||
|
ResentBcc []*mail.Address
|
||||||
|
ResentMessageID string
|
||||||
|
|
||||||
|
ContentType string
|
||||||
|
Content io.Reader
|
||||||
|
|
||||||
|
HTMLBody string
|
||||||
|
TextBody string
|
||||||
|
|
||||||
|
Attachments []Attachment
|
||||||
|
EmbeddedFiles []EmbeddedFile
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[EMLy]
|
[EMLy]
|
||||||
SDK_DECODER_SEMVER="1.1.0"
|
SDK_DECODER_SEMVER="1.2.1-hotfix_1"
|
||||||
SDK_DECODER_RELEASE_CHANNEL="beta"
|
SDK_DECODER_RELEASE_CHANNEL="alpha"
|
||||||
GUI_SEMVER="1.1.4"
|
GUI_SEMVER="1.2.2"
|
||||||
GUI_RELEASE_CHANNEL="beta"
|
GUI_RELEASE_CHANNEL="beta"
|
||||||
LANGUAGE="it"
|
LANGUAGE="it"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"hello_world": "Hello, {name} from en!",
|
"hello_world": "Hello, {name} from en!",
|
||||||
|
"layout_loading_text": "Loading...",
|
||||||
"error_unexpected": "An unexpected error occurred",
|
"error_unexpected": "An unexpected error occurred",
|
||||||
"sidebar_overview": "Mail Viewer",
|
"sidebar_overview": "Mail Viewer",
|
||||||
"sidebar_settings": "Settings",
|
"sidebar_settings": "Settings",
|
||||||
@@ -29,6 +30,10 @@
|
|||||||
"settings_preview_pdf_builtin_label": "Use built-in viewer for PDFs",
|
"settings_preview_pdf_builtin_label": "Use built-in viewer for PDFs",
|
||||||
"settings_preview_pdf_builtin_hint": "Uses EMLy's built-in viewer for PDF files.",
|
"settings_preview_pdf_builtin_hint": "Uses EMLy's built-in viewer for PDF files.",
|
||||||
"settings_preview_pdf_builtin_info": "Info: If disabled, PDF files will be treated as downloads instead of being previewed within the app.",
|
"settings_preview_pdf_builtin_info": "Info: If disabled, PDF files will be treated as downloads instead of being previewed within the app.",
|
||||||
|
"settings_msg_converter_title": "MSG Handling",
|
||||||
|
"settings_msg_converter_description": "Configure how MSG files are processed.",
|
||||||
|
"settings_msg_converter_label": "Use MSG to EML converter",
|
||||||
|
"settings_msg_converter_hint": "Uses an external app to convert .MSG files to .EML files. Disable it to use the OSS version (less accurate).",
|
||||||
"settings_danger_zone_title": "Danger Zone",
|
"settings_danger_zone_title": "Danger Zone",
|
||||||
"settings_danger_zone_description": "Advanced actions. Proceed with caution.",
|
"settings_danger_zone_description": "Advanced actions. Proceed with caution.",
|
||||||
"settings_danger_devtools_label": "Open DevTools",
|
"settings_danger_devtools_label": "Open DevTools",
|
||||||
@@ -56,9 +61,9 @@
|
|||||||
"settings_unsaved_toast_save": "Save changes",
|
"settings_unsaved_toast_save": "Save changes",
|
||||||
"settings_unsaved_toast_reset": "Reset",
|
"settings_unsaved_toast_reset": "Reset",
|
||||||
"mail_no_email_selected": "No email selected",
|
"mail_no_email_selected": "No email selected",
|
||||||
"mail_open_eml_btn": "Open EML File",
|
"mail_open_eml_btn": "Open EML/MSG File",
|
||||||
"mail_subject_no_subject": "(No Subject)",
|
"mail_subject_no_subject": "(No Subject)",
|
||||||
"mail_open_btn_label": "Open EML file",
|
"mail_open_btn_label": "Open EML/MSG file",
|
||||||
"mail_open_btn_title": "Open another file",
|
"mail_open_btn_title": "Open another file",
|
||||||
"mail_close_btn_label": "Close",
|
"mail_close_btn_label": "Close",
|
||||||
"mail_close_btn_title": "Close",
|
"mail_close_btn_title": "Close",
|
||||||
@@ -72,8 +77,13 @@
|
|||||||
"mail_error_image": "Failed to open image file.",
|
"mail_error_image": "Failed to open image file.",
|
||||||
"settings_toast_language_changed": "Language changed successfully!",
|
"settings_toast_language_changed": "Language changed successfully!",
|
||||||
"settings_toast_language_change_failed": "Failed to change language.",
|
"settings_toast_language_change_failed": "Failed to change language.",
|
||||||
"mail_open_btn_text": "Open EML File",
|
"mail_open_btn_text": "Open EML/MSG File",
|
||||||
"mail_close_btn_text": "Close",
|
"mail_close_btn_text": "Close",
|
||||||
"settings_danger_reset_dialog_description_part1": "This action cannot be undone.",
|
"settings_danger_reset_dialog_description_part1": "This action cannot be undone.",
|
||||||
"settings_danger_reset_dialog_description_part2": "This will permanently delete your current settings and return the app to its default state."
|
"settings_danger_reset_dialog_description_part2": "This will permanently delete your current settings and return the app to its default state.",
|
||||||
|
"mail_error_opening": "Failed to open EML file.",
|
||||||
|
"mail_pec_signed_badge": "Signed mail",
|
||||||
|
"mail_pec_feature_warning": "PEC detected: some features may be limited.",
|
||||||
|
"mail_sign_label": "Sign:",
|
||||||
|
"mail_loading_msg_conversion": "Converting MSG file... This might take a while."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"hello_world": "Ciao, {name} in it!",
|
"hello_world": "Ciao, {name} in it!",
|
||||||
|
"layout_loading_text": "Caricamento...",
|
||||||
"error_unexpected": "Si è verificato un errore imprevisto",
|
"error_unexpected": "Si è verificato un errore imprevisto",
|
||||||
"sidebar_overview": "Visualizza Mail",
|
"sidebar_overview": "Visualizza Mail",
|
||||||
"sidebar_settings": "Impostazioni",
|
"sidebar_settings": "Impostazioni",
|
||||||
@@ -29,6 +30,10 @@
|
|||||||
"settings_preview_pdf_builtin_label": "Usa visualizzatore integrato per PDF",
|
"settings_preview_pdf_builtin_label": "Usa visualizzatore integrato per PDF",
|
||||||
"settings_preview_pdf_builtin_hint": "Usa il visualizzatore integrato di EMLy per i file PDF.",
|
"settings_preview_pdf_builtin_hint": "Usa il visualizzatore integrato di EMLy per i file PDF.",
|
||||||
"settings_preview_pdf_builtin_info": "Info: Se disabilitato, i file PDF verranno trattati come download invece di essere visualizzati nell'app.",
|
"settings_preview_pdf_builtin_info": "Info: Se disabilitato, i file PDF verranno trattati come download invece di essere visualizzati nell'app.",
|
||||||
|
"settings_msg_converter_title": "Gestione MSG",
|
||||||
|
"settings_msg_converter_description": "Configura come vengono elaborati i file MSG.",
|
||||||
|
"settings_msg_converter_label": "Usa convertitore MSG in EML",
|
||||||
|
"settings_msg_converter_hint": "Usa un'applicazione esterna per convertire i file .MSG in .EML. Disabilitalo per usare la versione OSS (meno accurata).",
|
||||||
"settings_danger_zone_title": "Zona Pericolo",
|
"settings_danger_zone_title": "Zona Pericolo",
|
||||||
"settings_danger_zone_description": "Azioni avanzate. Procedere con cautela.",
|
"settings_danger_zone_description": "Azioni avanzate. Procedere con cautela.",
|
||||||
"settings_danger_devtools_label": "Apri DevTools",
|
"settings_danger_devtools_label": "Apri DevTools",
|
||||||
@@ -56,9 +61,9 @@
|
|||||||
"settings_unsaved_toast_save": "Salva",
|
"settings_unsaved_toast_save": "Salva",
|
||||||
"settings_unsaved_toast_reset": "Ripristina",
|
"settings_unsaved_toast_reset": "Ripristina",
|
||||||
"mail_no_email_selected": "Nessuna email selezionata",
|
"mail_no_email_selected": "Nessuna email selezionata",
|
||||||
"mail_open_eml_btn": "Apri File EML",
|
"mail_open_eml_btn": "Apri File EML/MSG",
|
||||||
"mail_subject_no_subject": "(Nessun Oggetto)",
|
"mail_subject_no_subject": "(Nessun Oggetto)",
|
||||||
"mail_open_btn_label": "Apri file EML",
|
"mail_open_btn_label": "Apri file EML/MSG",
|
||||||
"mail_open_btn_title": "Apri un altro file",
|
"mail_open_btn_title": "Apri un altro file",
|
||||||
"mail_close_btn_label": "Chiudi",
|
"mail_close_btn_label": "Chiudi",
|
||||||
"mail_close_btn_title": "Chiudi",
|
"mail_close_btn_title": "Chiudi",
|
||||||
@@ -72,8 +77,13 @@
|
|||||||
"mail_error_image": "Impossibile aprire il file immagine.",
|
"mail_error_image": "Impossibile aprire il file immagine.",
|
||||||
"settings_toast_language_changed": "Lingua cambiata con successo!",
|
"settings_toast_language_changed": "Lingua cambiata con successo!",
|
||||||
"settings_toast_language_change_failed": "Impossibile cambiare lingua.",
|
"settings_toast_language_change_failed": "Impossibile cambiare lingua.",
|
||||||
"mail_open_btn_text": "Apri File EML",
|
"mail_open_btn_text": "Apri file EML/MSG",
|
||||||
"mail_close_btn_text": "Chiudi",
|
"mail_close_btn_text": "Chiudi",
|
||||||
"settings_danger_reset_dialog_description_part1": "Questa azione non può essere annullata.",
|
"settings_danger_reset_dialog_description_part1": "Questa azione non può essere annullata.",
|
||||||
"settings_danger_reset_dialog_description_part2": "Questo eliminerà permanentemente le tue impostazioni attuali e riporterà l'app allo stato predefinito."
|
"settings_danger_reset_dialog_description_part2": "Questo eliminerà permanentemente le tue impostazioni attuali e riporterà l'app allo stato predefinito.",
|
||||||
|
"mail_error_opening": "Impossibile aprire il file EML.",
|
||||||
|
"mail_pec_signed_badge": "Mail firmata",
|
||||||
|
"mail_pec_feature_warning": "PEC rilevata: alcune funzionalità potrebbero essere limitate.",
|
||||||
|
"mail_sign_label": "Firma:",
|
||||||
|
"mail_loading_msg_conversion": "Conversione file MSG in corso... Potrebbe richiedere del tempo."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div id="app-loading">
|
<div id="app-loading">
|
||||||
<div class="loader-spinner"></div>
|
<div class="loader-spinner"></div>
|
||||||
<div>Loading, please wait.</div>
|
<div>Loading, please wait...</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: contents;">%sveltekit.body%</div>
|
<div style="display: contents;">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { X, MailOpen, Image, FileText, File } from "@lucide/svelte";
|
import { X, MailOpen, Image, FileText, File, ShieldCheck, Shield, Signature, FileUser, Loader2 } from "@lucide/svelte";
|
||||||
import { ShowOpenFileDialog, ReadEML, OpenPDF, OpenImageWindow, OpenPDFWindow, OpenImage } from "$lib/wailsjs/go/main/App";
|
import { ShowOpenFileDialog, ReadEML, OpenPDF, OpenImageWindow, OpenPDFWindow, OpenImage, ReadMSG, ReadPEC, OpenEMLWindow } from "$lib/wailsjs/go/main/App";
|
||||||
import type { internal } from "$lib/wailsjs/go/models";
|
import type { internal } from "$lib/wailsjs/go/models";
|
||||||
import { sidebarOpen } from "$lib/stores/app";
|
import { sidebarOpen } from "$lib/stores/app";
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
let unregisterEvents = () => {};
|
let unregisterEvents = () => {};
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
let loadingText = $state("");
|
||||||
|
|
||||||
function onClear() {
|
function onClear() {
|
||||||
mailState.clear();
|
mailState.clear();
|
||||||
@@ -33,19 +34,60 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Listen for second instance args
|
// Listen for second instance args
|
||||||
unregisterEvents = EventsOn("launchArgs", async (args: string[]) => {
|
unregisterEvents = EventsOn("launchArgs", async (args: string[]) => {
|
||||||
|
console.log("got event launchArgs:", args);
|
||||||
if (args && args.length > 0) {
|
if (args && args.length > 0) {
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
if (arg.toLowerCase().endsWith(".eml")) {
|
const lowerArg = arg.toLowerCase();
|
||||||
console.log("Loading EML from second instance:", arg);
|
if (lowerArg.endsWith(".eml") || lowerArg.endsWith(".msg")) {
|
||||||
|
console.log("Loading file from second instance:", arg);
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
loadingText = m.layout_loading_text();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const emlContent = await ReadEML(arg);
|
let emlContent;
|
||||||
|
|
||||||
|
if (lowerArg.endsWith(".msg")) {
|
||||||
|
const useExt = settingsStore.settings.useMsgConverter ?? true;
|
||||||
|
if (useExt) {
|
||||||
|
loadingText = m.mail_loading_msg_conversion();
|
||||||
|
}
|
||||||
|
emlContent = await ReadMSG(arg, useExt);
|
||||||
|
if(emlContent.isPec) {
|
||||||
|
toast.warning(m.mail_pec_feature_warning());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// EML handling
|
||||||
|
try {
|
||||||
|
emlContent = await ReadPEC(arg);
|
||||||
|
if(emlContent.isPec) {
|
||||||
|
toast.warning(m.mail_pec_feature_warning());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("ReadPEC failed, trying ReadEML:", e);
|
||||||
|
emlContent = await ReadEML(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emlContent && emlContent.body) {
|
||||||
|
const trimmed = emlContent.body.trim();
|
||||||
|
const clean = trimmed.replace(/[\s\r\n]+/g, '');
|
||||||
|
if (clean.length > 0 && clean.length % 4 === 0 && /^[A-Za-z0-9+/]+=*$/.test(clean)) {
|
||||||
|
try {
|
||||||
|
emlContent.body = window.atob(clean);
|
||||||
|
} catch (e) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mailState.setParams(emlContent);
|
mailState.setParams(emlContent);
|
||||||
sidebarOpen.set(false);
|
sidebarOpen.set(false);
|
||||||
WindowUnminimise();
|
WindowUnminimise();
|
||||||
WindowShow();
|
WindowShow();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load email:", error);
|
||||||
|
toast.error("Failed to load email file");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
loadingText = "";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -80,23 +122,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openEMLHandler(base64Data: string, filename: string) {
|
||||||
|
try {
|
||||||
|
await OpenEMLWindow(base64Data, filename);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open EML:", error);
|
||||||
|
toast.error("Failed to open EML attachment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onOpenMail() {
|
async function onOpenMail() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
loadingText = m.layout_loading_text();
|
||||||
const result = await ShowOpenFileDialog();
|
const result = await ShowOpenFileDialog();
|
||||||
if (result && result.length > 0) {
|
if (result && result.length > 0) {
|
||||||
// Handle opening the mail file
|
// Handle opening the mail file
|
||||||
try {
|
try {
|
||||||
const email: internal.EmailData = await ReadEML(result);
|
// If the file is .eml, otherwise if is .msg, read accordingly
|
||||||
|
let email: internal.EmailData;
|
||||||
|
if(result.toLowerCase().endsWith(".msg")) {
|
||||||
|
const useExt = settingsStore.settings.useMsgConverter ?? true;
|
||||||
|
if (useExt) {
|
||||||
|
loadingText = m.mail_loading_msg_conversion();
|
||||||
|
}
|
||||||
|
email = await ReadMSG(result, useExt);
|
||||||
|
} else {
|
||||||
|
email = await ReadEML(result);
|
||||||
|
}
|
||||||
|
if(email.isPec) {
|
||||||
|
toast.warning(m.mail_pec_feature_warning(), {duration: 10000});
|
||||||
|
}
|
||||||
mailState.setParams(email);
|
mailState.setParams(email);
|
||||||
sidebarOpen.set(false);
|
sidebarOpen.set(false);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to read EML file:", error);
|
console.error("Failed to read EML file:", error);
|
||||||
|
toast.error(m.mail_error_opening());
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
loadingText = "";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
loadingText = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +195,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel fill" aria-label="Events">
|
<div class="panel fill" aria-label="Events">
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="loading-overlay">
|
||||||
|
<Loader2 class="spinner" size="48" />
|
||||||
|
<div class="loading-text">{loadingText}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="events" role="log" aria-live="polite">
|
<div class="events" role="log" aria-live="polite">
|
||||||
{#if mailState.currentEmail === null}
|
{#if mailState.currentEmail === null}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -187,6 +261,14 @@
|
|||||||
<span class="label">{m.mail_bcc()}</span>
|
<span class="label">{m.mail_bcc()}</span>
|
||||||
<span class="value">{mailState.currentEmail.bcc.join(", ")}</span>
|
<span class="value">{mailState.currentEmail.bcc.join(", ")}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if mailState.currentEmail.isPec}
|
||||||
|
<span class="label">{m.mail_sign_label()}</span>
|
||||||
|
<span class="value"><span class="pec-badge" title="Posta Elettronica Certificata">
|
||||||
|
<ShieldCheck size="14" />
|
||||||
|
PEC
|
||||||
|
</span></span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -211,6 +293,32 @@
|
|||||||
<FileText size="15" />
|
<FileText size="15" />
|
||||||
<span class="att-name">{att.filename}</span>
|
<span class="att-name">{att.filename}</span>
|
||||||
</button>
|
</button>
|
||||||
|
{:else if att.filename.toLowerCase().endsWith(".eml")}
|
||||||
|
<button
|
||||||
|
class="att-btn eml"
|
||||||
|
onclick={() => openEMLHandler(arrayBufferToBase64(att.data), att.filename)}
|
||||||
|
>
|
||||||
|
<MailOpen size="14" />
|
||||||
|
<span class="att-name">{att.filename}</span>
|
||||||
|
</button>
|
||||||
|
{:else if mailState.currentEmail.isPec && att.filename.toLowerCase().endsWith(".p7s")}
|
||||||
|
<a
|
||||||
|
class="att-btn file"
|
||||||
|
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
|
||||||
|
download={att.filename}
|
||||||
|
>
|
||||||
|
<Signature size="14" />
|
||||||
|
<span class="att-name">{att.filename}</span>
|
||||||
|
</a>
|
||||||
|
{:else if mailState.currentEmail.isPec && att.filename.toLowerCase() === "daticert.xml"}
|
||||||
|
<a
|
||||||
|
class="att-btn file"
|
||||||
|
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
|
||||||
|
download={att.filename}
|
||||||
|
>
|
||||||
|
<FileUser size="14" />
|
||||||
|
<span class="att-name">{att.filename}</span>
|
||||||
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
class="att-btn file"
|
class="att-btn file"
|
||||||
@@ -247,6 +355,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 50;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure internal loader spins if not using class-based animation library like Tailwind */
|
||||||
|
:global(.spinner) {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@@ -403,6 +543,9 @@
|
|||||||
.att-btn.pdf { color: #f87171; border-color: rgba(248, 113, 113, 0.3); }
|
.att-btn.pdf { color: #f87171; border-color: rgba(248, 113, 113, 0.3); }
|
||||||
.att-btn.pdf:hover { color: #fca5a5; }
|
.att-btn.pdf:hover { color: #fca5a5; }
|
||||||
|
|
||||||
|
.att-btn.eml { color: hsl(49, 80%, 49%); border-color: rgba(224, 206, 39, 0.3); }
|
||||||
|
.att-btn.eml:hover { color: hsl(49, 80%, 65%); }
|
||||||
|
|
||||||
.att-name {
|
.att-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -497,4 +640,42 @@
|
|||||||
color: rgba(255, 255, 255, 0.4);
|
color: rgba(255, 255, 255, 0.4);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badged-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signed-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #f87171;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-align: middle;
|
||||||
|
user-select: none;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pec-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: rgba(16, 185, 129, 0.15);
|
||||||
|
color: #34d399;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-align: middle;
|
||||||
|
user-select: none;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
1
frontend/src/lib/types.d.ts
vendored
1
frontend/src/lib/types.d.ts
vendored
@@ -6,6 +6,7 @@ interface EMLy_GUI_Settings {
|
|||||||
selectedLanguage: SupportedLanguages = "en" | "it";
|
selectedLanguage: SupportedLanguages = "en" | "it";
|
||||||
useBuiltinPreview: boolean;
|
useBuiltinPreview: boolean;
|
||||||
useBuiltinPDFViewer?: boolean;
|
useBuiltinPDFViewer?: boolean;
|
||||||
|
useMsgConverter?: boolean;
|
||||||
previewFileSupportedTypes?: SupportedFileTypePreview[];
|
previewFileSupportedTypes?: SupportedFileTypePreview[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
frontend/src/lib/wailsjs/go/main/App.d.ts
vendored
8
frontend/src/lib/wailsjs/go/main/App.d.ts
vendored
@@ -20,6 +20,8 @@ export function GetViewerData():Promise<main.ViewerData>;
|
|||||||
|
|
||||||
export function OpenDefaultAppsSettings():Promise<void>;
|
export function OpenDefaultAppsSettings():Promise<void>;
|
||||||
|
|
||||||
|
export function OpenEMLWindow(arg1:string,arg2:string):Promise<void>;
|
||||||
|
|
||||||
export function OpenImage(arg1:string,arg2:string):Promise<void>;
|
export function OpenImage(arg1:string,arg2:string):Promise<void>;
|
||||||
|
|
||||||
export function OpenImageWindow(arg1:string,arg2:string):Promise<void>;
|
export function OpenImageWindow(arg1:string,arg2:string):Promise<void>;
|
||||||
@@ -32,6 +34,12 @@ export function QuitApp():Promise<void>;
|
|||||||
|
|
||||||
export function ReadEML(arg1:string):Promise<internal.EmailData>;
|
export function ReadEML(arg1:string):Promise<internal.EmailData>;
|
||||||
|
|
||||||
|
export function ReadMSG(arg1:string,arg2:boolean):Promise<internal.EmailData>;
|
||||||
|
|
||||||
|
export function ReadMSGOSS(arg1:string):Promise<internal.EmailData>;
|
||||||
|
|
||||||
|
export function ReadPEC(arg1:string):Promise<internal.EmailData>;
|
||||||
|
|
||||||
export function SaveConfig(arg1:utils.Config):Promise<void>;
|
export function SaveConfig(arg1:utils.Config):Promise<void>;
|
||||||
|
|
||||||
export function ShowOpenFileDialog():Promise<string>;
|
export function ShowOpenFileDialog():Promise<string>;
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ export function OpenDefaultAppsSettings() {
|
|||||||
return window['go']['main']['App']['OpenDefaultAppsSettings']();
|
return window['go']['main']['App']['OpenDefaultAppsSettings']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function OpenEMLWindow(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['OpenEMLWindow'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
export function OpenImage(arg1, arg2) {
|
export function OpenImage(arg1, arg2) {
|
||||||
return window['go']['main']['App']['OpenImage'](arg1, arg2);
|
return window['go']['main']['App']['OpenImage'](arg1, arg2);
|
||||||
}
|
}
|
||||||
@@ -58,6 +62,18 @@ export function ReadEML(arg1) {
|
|||||||
return window['go']['main']['App']['ReadEML'](arg1);
|
return window['go']['main']['App']['ReadEML'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ReadMSG(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['ReadMSG'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReadMSGOSS(arg1) {
|
||||||
|
return window['go']['main']['App']['ReadMSGOSS'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReadPEC(arg1) {
|
||||||
|
return window['go']['main']['App']['ReadPEC'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function SaveConfig(arg1) {
|
export function SaveConfig(arg1) {
|
||||||
return window['go']['main']['App']['SaveConfig'](arg1);
|
return window['go']['main']['App']['SaveConfig'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ export namespace internal {
|
|||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
attachments: EmailAttachment[];
|
attachments: EmailAttachment[];
|
||||||
|
isPec: boolean;
|
||||||
|
hasInnerEmail: boolean;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
return new EmailData(source);
|
return new EmailData(source);
|
||||||
@@ -213,6 +215,8 @@ export namespace internal {
|
|||||||
this.subject = source["subject"];
|
this.subject = source["subject"];
|
||||||
this.body = source["body"];
|
this.body = source["body"];
|
||||||
this.attachments = this.convertValues(source["attachments"], EmailAttachment);
|
this.attachments = this.convertValues(source["attachments"], EmailAttachment);
|
||||||
|
this.isPec = source["isPec"];
|
||||||
|
this.hasInnerEmail = source["hasInnerEmail"];
|
||||||
}
|
}
|
||||||
|
|
||||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
import "../layout.css";
|
import "../layout.css";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import * as m from "$lib/paraglide/messages.js";
|
import * as m from "$lib/paraglide/messages.js";
|
||||||
import { GetConfig } from "$lib/wailsjs/go/main/App";
|
|
||||||
import type { utils } from "$lib/wailsjs/go/models";
|
import type { utils } from "$lib/wailsjs/go/models";
|
||||||
import { Toaster } from "$lib/components/ui/sonner/index.js";
|
import { Toaster } from "$lib/components/ui/sonner/index.js";
|
||||||
import AppSidebar from "$lib/components/SidebarApp.svelte";
|
import AppSidebar from "$lib/components/SidebarApp.svelte";
|
||||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import { dev } from '$app/environment';
|
||||||
import {
|
import {
|
||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
PanelRightOpen,
|
PanelRightOpen,
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
} from "@lucide/svelte";
|
} from "@lucide/svelte";
|
||||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WindowMinimise,
|
WindowMinimise,
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
WindowIsMaximised,
|
WindowIsMaximised,
|
||||||
Quit,
|
Quit,
|
||||||
} from "$lib/wailsjs/runtime/runtime";
|
} from "$lib/wailsjs/runtime/runtime";
|
||||||
|
import { RefreshCcwDot } from "@lucide/svelte";
|
||||||
|
|
||||||
let versionInfo: utils.Config | null = $state(null);
|
let versionInfo: utils.Config | null = $state(null);
|
||||||
let isMaximized = $state(false);
|
let isMaximized = $state(false);
|
||||||
@@ -65,10 +67,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
versionInfo = await GetConfig();
|
versionInfo = data.data as utils.Config;
|
||||||
});
|
});
|
||||||
|
|
||||||
let { children } = $props();
|
let { data, children } = $props();
|
||||||
|
|
||||||
const THEME_KEY = "emly_theme";
|
const THEME_KEY = "emly_theme";
|
||||||
let theme = $state<"dark" | "light">("dark");
|
let theme = $state<"dark" | "light">("dark");
|
||||||
@@ -109,24 +111,24 @@
|
|||||||
<div class="title">
|
<div class="title">
|
||||||
<bold>EMLy</bold>
|
<bold>EMLy</bold>
|
||||||
<div class="version-wrapper">
|
<div class="version-wrapper">
|
||||||
<version
|
<version>
|
||||||
>v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy
|
{#if dev}
|
||||||
.GUIReleaseChannel}</version
|
v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy.GUIReleaseChannel} <debug>(DEBUG BUILD)</debug>
|
||||||
>
|
{:else}
|
||||||
|
v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy.GUIReleaseChannel}
|
||||||
|
{/if}
|
||||||
|
</version>
|
||||||
{#if versionInfo}
|
{#if versionInfo}
|
||||||
<div class="version-tooltip">
|
<div class="version-tooltip">
|
||||||
<div class="tooltip-item">
|
<div class="tooltip-item">
|
||||||
<span class="label">GUI:</span>
|
<span class="label">GUI:</span>
|
||||||
<span class="value">v{versionInfo.EMLy.GUISemver}</span>
|
<span class="value">v{versionInfo.EMLy.GUISemver}</span>
|
||||||
<span class="channel">({versionInfo.EMLy.GUIReleaseChannel})</span
|
<span class="channel">({versionInfo.EMLy.GUIReleaseChannel})</span>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tooltip-item">
|
<div class="tooltip-item">
|
||||||
<span class="label">SDK:</span>
|
<span class="label">SDK:</span>
|
||||||
<span class="value">v{versionInfo.EMLy.SDKDecoderSemver}</span>
|
<span class="value">v{versionInfo.EMLy.SDKDecoderSemver}</span>
|
||||||
<span class="channel"
|
<span class="channel">({versionInfo.EMLy.SDKDecoderReleaseChannel})</span>
|
||||||
>({versionInfo.EMLy.SDKDecoderReleaseChannel})</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -159,7 +161,9 @@
|
|||||||
{#await navigating?.complete}
|
{#await navigating?.complete}
|
||||||
<div class="loading-overlay">
|
<div class="loading-overlay">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<span style="opacity: 0.5; font-size: 13px">Loading...</span>
|
<span style="opacity: 0.5; font-size: 13px"
|
||||||
|
>{m.layout_loading_text()}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{:then}
|
{:then}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
@@ -192,7 +196,7 @@
|
|||||||
<House
|
<House
|
||||||
size="16"
|
size="16"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if(page.url.pathname !== "/") goto("/");
|
if (page.url.pathname !== "/") goto("/");
|
||||||
}}
|
}}
|
||||||
style="cursor: pointer; opacity: 0.7;"
|
style="cursor: pointer; opacity: 0.7;"
|
||||||
class="hover:opacity-100 transition-opacity"
|
class="hover:opacity-100 transition-opacity"
|
||||||
@@ -200,11 +204,26 @@
|
|||||||
<Settings
|
<Settings
|
||||||
size="16"
|
size="16"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (page.url.pathname !== "/settings" && page.url.pathname !== "/settings/") goto("/settings");
|
if (
|
||||||
|
page.url.pathname !== "/settings" &&
|
||||||
|
page.url.pathname !== "/settings/"
|
||||||
|
)
|
||||||
|
goto("/settings");
|
||||||
}}
|
}}
|
||||||
style="cursor: pointer; opacity: 0.7;"
|
style="cursor: pointer; opacity: 0.7;"
|
||||||
class="hover:opacity-100 transition-opacity"
|
class="hover:opacity-100 transition-opacity"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<a
|
||||||
|
data-sveltekit-reload
|
||||||
|
href="/"
|
||||||
|
class={`${buttonVariants({ variant: "destructive" })} cursor-pointer hover:cursor-pointer`}
|
||||||
|
style="text-decoration: none; margin-left: auto; height: 24px; font-size: 12px; padding: 0 8px;"
|
||||||
|
aria-label={m.settings_danger_reload_button()}
|
||||||
|
title={m.settings_danger_reload_button() + " app"}
|
||||||
|
>
|
||||||
|
<RefreshCcwDot />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:none">
|
<div style="display:none">
|
||||||
@@ -275,6 +294,12 @@
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title version debug{
|
||||||
|
color: #e11d48;
|
||||||
|
opacity: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.version-wrapper {
|
.version-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
12
frontend/src/routes/(app)/+layout.ts
Normal file
12
frontend/src/routes/(app)/+layout.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { LayoutLoad } from './$types';
|
||||||
|
import { GetConfig } from "$lib/wailsjs/go/main/App";
|
||||||
|
|
||||||
|
export const load = (async () => {
|
||||||
|
try {
|
||||||
|
const config = await GetConfig();
|
||||||
|
return { data: config, error: null };
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load config:", e);
|
||||||
|
return { data: null, error: 'Failed to load config' };
|
||||||
|
}
|
||||||
|
}) satisfies LayoutLoad;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
import { GetViewerData, GetStartupFile, ReadEML } from '$lib/wailsjs/go/main/App';
|
import { GetViewerData, GetStartupFile, ReadEML, ReadMSG } from '$lib/wailsjs/go/main/App';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
|
import type { internal } from '$lib/wailsjs/go/models';
|
||||||
|
|
||||||
export const load: PageLoad = async () => {
|
export const load: PageLoad = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -18,7 +20,15 @@ export const load: PageLoad = async () => {
|
|||||||
// Check if opened with a file
|
// Check if opened with a file
|
||||||
const startupFile = await GetStartupFile();
|
const startupFile = await GetStartupFile();
|
||||||
if (startupFile) {
|
if (startupFile) {
|
||||||
const emlContent = await ReadEML(startupFile);
|
let emlContent: internal.EmailData;
|
||||||
|
|
||||||
|
if (startupFile.toLowerCase().endsWith(".msg")) {
|
||||||
|
const useExt = settingsStore.settings.useMsgConverter ?? true;
|
||||||
|
emlContent = await ReadMSG(startupFile, useExt);
|
||||||
|
} else {
|
||||||
|
emlContent = await ReadEML(startupFile);
|
||||||
|
}
|
||||||
|
|
||||||
if (emlContent) {
|
if (emlContent) {
|
||||||
emlContent.body = DOMPurify.sanitize(emlContent.body || "");
|
emlContent.body = DOMPurify.sanitize(emlContent.body || "");
|
||||||
return { email: emlContent };
|
return { email: emlContent };
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
selectedLanguage: "it",
|
selectedLanguage: "it",
|
||||||
useBuiltinPreview: true,
|
useBuiltinPreview: true,
|
||||||
useBuiltinPDFViewer: true,
|
useBuiltinPDFViewer: true,
|
||||||
|
useMsgConverter: true,
|
||||||
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
|
previewFileSupportedTypes: ["jpg", "jpeg", "png"],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
useBuiltinPreview: !!s.useBuiltinPreview,
|
useBuiltinPreview: !!s.useBuiltinPreview,
|
||||||
useBuiltinPDFViewer:
|
useBuiltinPDFViewer:
|
||||||
s.useBuiltinPDFViewer ?? defaults.useBuiltinPDFViewer ?? true,
|
s.useBuiltinPDFViewer ?? defaults.useBuiltinPDFViewer ?? true,
|
||||||
|
useMsgConverter: s.useMsgConverter ?? defaults.useMsgConverter ?? true,
|
||||||
previewFileSupportedTypes:
|
previewFileSupportedTypes:
|
||||||
s.previewFileSupportedTypes || defaults.previewFileSupportedTypes || [],
|
s.previewFileSupportedTypes || defaults.previewFileSupportedTypes || [],
|
||||||
};
|
};
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
(a.selectedLanguage ?? "") === (b.selectedLanguage ?? "") &&
|
(a.selectedLanguage ?? "") === (b.selectedLanguage ?? "") &&
|
||||||
!!a.useBuiltinPreview === !!b.useBuiltinPreview &&
|
!!a.useBuiltinPreview === !!b.useBuiltinPreview &&
|
||||||
!!a.useBuiltinPDFViewer === !!b.useBuiltinPDFViewer &&
|
!!a.useBuiltinPDFViewer === !!b.useBuiltinPDFViewer &&
|
||||||
|
!!a.useMsgConverter === !!b.useMsgConverter &&
|
||||||
JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
|
JSON.stringify(a.previewFileSupportedTypes?.sort()) ===
|
||||||
JSON.stringify(b.previewFileSupportedTypes?.sort())
|
JSON.stringify(b.previewFileSupportedTypes?.sort())
|
||||||
);
|
);
|
||||||
@@ -363,6 +366,35 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="space-y-1">
|
||||||
|
<Card.Title>{m.settings_msg_converter_title()}</Card.Title>
|
||||||
|
<Card.Description
|
||||||
|
>{m.settings_msg_converter_description()}</Card.Description
|
||||||
|
>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="space-y-4">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-4 rounded-lg border bg-card p-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label class="text-base">
|
||||||
|
{m.settings_msg_converter_label()}
|
||||||
|
</Label>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{m.settings_msg_converter_hint()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
bind:checked={form.useMsgConverter}
|
||||||
|
class="cursor-pointer hover:cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
{#if $dangerZoneEnabled}
|
{#if $dangerZoneEnabled}
|
||||||
<Card.Root class="border-destructive/50 bg-destructive/15">
|
<Card.Root class="border-destructive/50 bg-destructive/15">
|
||||||
<Card.Header class="space-y-1">
|
<Card.Header class="space-y-1">
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -3,8 +3,8 @@ module emly
|
|||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DusanKasan/parsemail v1.2.0
|
|
||||||
github.com/jaypipes/ghw v0.21.2
|
github.com/jaypipes/ghw v0.21.2
|
||||||
|
github.com/richardlehane/mscfb v1.0.6
|
||||||
github.com/wailsapp/wails/v2 v2.11.0
|
github.com/wailsapp/wails/v2 v2.11.0
|
||||||
golang.org/x/sys v0.40.0
|
golang.org/x/sys v0.40.0
|
||||||
golang.org/x/text v0.22.0
|
golang.org/x/text v0.22.0
|
||||||
@@ -29,6 +29,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/richardlehane/msoleps v1.0.3 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/samber/lo v1.49.1 // indirect
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -1,5 +1,3 @@
|
|||||||
github.com/DusanKasan/parsemail v1.2.0 h1:CrzTL1nuPLxB41aO4zE/Tzc9GVD8jjifUftlbTKQQl4=
|
|
||||||
github.com/DusanKasan/parsemail v1.2.0/go.mod h1:B9lfMbpVe4DMqPImAOCGti7KEwasnRTrKKn66iQefVs=
|
|
||||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -53,6 +51,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||||
|
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||||
|
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
|
||||||
|
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[Setup]
|
[Setup]
|
||||||
AppName=EMLy
|
AppName=EMLy
|
||||||
AppVersion=1.1.4
|
AppVersion=1.2.2
|
||||||
DefaultDirName={autopf}\EMLy
|
DefaultDirName={autopf}\EMLy
|
||||||
OutputBaseFilename=EMLy_Installer
|
OutputBaseFilename=EMLy_Installer_1.2.2
|
||||||
ArchitecturesInstallIn64BitMode=x64compatible
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
; Request administrative privileges for HKA to write to HKLM if needed,
|
; Request administrative privileges for HKA to write to HKLM if needed,
|
||||||
@@ -15,21 +15,28 @@ UninstallDisplayIcon={app}\EMLy.exe
|
|||||||
; Source path relative to this .iss file (assuming it is in the "installer" folder and build is in "../build")
|
; Source path relative to this .iss file (assuming it is in the "installer" folder and build is in "../build")
|
||||||
Source: "..\build\bin\EMLy.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\build\bin\EMLy.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\build\bin\config.ini"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\build\bin\config.ini"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "..\build\bin\signed_msg.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
|
||||||
[Registry]
|
[Registry]
|
||||||
; 1. Register the .eml extension and point it to our internal ProgID "EMLy.EML"
|
; 1. Register the .eml extension and point it to our internal ProgID "EMLy.EML"
|
||||||
Root: HKA; Subkey: "Software\Classes\.eml"; ValueType: string; ValueName: ""; ValueData: "EMLy.EML"; Flags: uninsdeletevalue
|
Root: HKA; Subkey: "Software\Classes\.eml"; ValueType: string; ValueName: ""; ValueData: "EMLy.EML"; Flags: uninsdeletevalue
|
||||||
|
Root: HKA; Subkey: "Software\Classes\.msg"; ValueType: string; ValueName: ""; ValueData: "EMLy.MSG"; Flags: uninsdeletevalue
|
||||||
|
|
||||||
; 2. Define the ProgID with a readable name and icon
|
; 2. Define the ProgID with a readable name and icon
|
||||||
Root: HKA; Subkey: "Software\Classes\EMLy.EML"; ValueType: string; ValueName: ""; ValueData: "EMLy Email Message"; Flags: uninsdeletekey
|
Root: HKA; Subkey: "Software\Classes\EMLy.EML"; ValueType: string; ValueName: ""; ValueData: "EMLy Email Message"; Flags: uninsdeletekey
|
||||||
Root: HKA; Subkey: "Software\Classes\EMLy.EML\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\EMLy.exe,0"
|
Root: HKA; Subkey: "Software\Classes\EMLy.EML\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\EMLy.exe,0"
|
||||||
|
|
||||||
|
Root: HKA; Subkey: "Software\Classes\EMLy.MSG"; ValueType: string; ValueName: ""; ValueData: "EMLy Outlook Message"; Flags: uninsdeletekey
|
||||||
|
Root: HKA; Subkey: "Software\Classes\EMLy.MSG\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\EMLy.exe,0"
|
||||||
|
|
||||||
; 3. Define the open command
|
; 3. Define the open command
|
||||||
; "%1" passes the file path to the application
|
; "%1" passes the file path to the application
|
||||||
Root: HKA; Subkey: "Software\Classes\EMLy.EML\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\EMLy.exe"" ""%1"""
|
Root: HKA; Subkey: "Software\Classes\EMLy.EML\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\EMLy.exe"" ""%1"""
|
||||||
|
Root: HKA; Subkey: "Software\Classes\EMLy.MSG\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\EMLy.exe"" ""%1"""
|
||||||
|
|
||||||
; Optional: Add "Open with EMLy" to context menu explicitly (though file association typically handles the double click)
|
; Optional: Add "Open with EMLy" to context menu explicitly (though file association typically handles the double click)
|
||||||
Root: HKA; Subkey: "Software\Classes\EMLy.EML\shell\open"; ValueType: string; ValueName: "FriendlyAppName"; ValueData: "EMLy"
|
Root: HKA; Subkey: "Software\Classes\EMLy.EML\shell\open"; ValueType: string; ValueName: "FriendlyAppName"; ValueData: "EMLy"
|
||||||
|
Root: HKA; Subkey: "Software\Classes\EMLy.MSG\shell\open"; ValueType: string; ValueName: "FriendlyAppName"; ValueData: "EMLy"
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{autoprograms}\EMLy"; Filename: "{app}\EMLy.exe"
|
Name: "{autoprograms}\EMLy"; Filename: "{app}\EMLy.exe"
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -23,6 +23,7 @@ func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceDa
|
|||||||
log.Println("user opened second from", secondInstanceData.WorkingDirectory)
|
log.Println("user opened second from", secondInstanceData.WorkingDirectory)
|
||||||
runtime.WindowUnminimise(a.ctx)
|
runtime.WindowUnminimise(a.ctx)
|
||||||
runtime.WindowShow(a.ctx)
|
runtime.WindowShow(a.ctx)
|
||||||
|
log.Println("launchArgs", secondInstanceArgs)
|
||||||
go runtime.EventsEmit(a.ctx, "launchArgs", secondInstanceArgs)
|
go runtime.EventsEmit(a.ctx, "launchArgs", secondInstanceArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +63,9 @@ func main() {
|
|||||||
if strings.HasSuffix(strings.ToLower(arg), ".eml") {
|
if strings.HasSuffix(strings.ToLower(arg), ".eml") {
|
||||||
app.StartupFilePath = arg
|
app.StartupFilePath = arg
|
||||||
}
|
}
|
||||||
|
if strings.HasSuffix(strings.ToLower(arg), ".msg") {
|
||||||
|
app.StartupFilePath = arg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create application with options
|
// Create application with options
|
||||||
|
|||||||
Reference in New Issue
Block a user