From e7d1850a63da36d2b5fb61589fa53302ff093cc4 Mon Sep 17 00:00:00 2001 From: Flavio Fois Date: Wed, 4 Feb 2026 23:25:20 +0100 Subject: [PATCH] feat: update SDK and GUI versions, add debugger protection settings - Updated SDK_DECODER_SEMVER to "1.3.0" and GUI_SEMVER to "1.2.4" in config.ini. - Updated MailViewer component to handle PDF already open error and improved iframe handling. - Removed deprecated useMsgConverter setting from settings page. - Added IsDebuggerRunning function to check for attached debuggers and quit the app if detected. - Enhanced PDF viewer to prevent infinite loading and improved error handling. Co-Authored-By: Laky-64 --- app.go | 11 +- backend/utils/debug_windows.go | 11 + backend/utils/mail/eml_pec_reader.go | 121 --- backend/utils/mail/eml_reader.go | 112 ++- backend/utils/{ => mail}/mailparser.go | 39 +- backend/utils/mail/msg_parser.go | 261 ------ backend/utils/mail/msg_reader.go | 813 +++++++++++++++--- config.ini | 6 +- frontend/.gitignore | 3 + frontend/messages/en.json | 6 +- frontend/messages/it.json | 8 +- frontend/package.json.md5 | 2 +- .../components/dashboard/MailViewer.svelte | 84 +- frontend/src/lib/stores/settings.svelte.ts | 4 +- frontend/src/lib/types.d.ts | 2 +- frontend/src/lib/wailsjs/go/main/App.d.ts | 2 + frontend/src/lib/wailsjs/go/main/App.js | 44 +- frontend/src/routes/(app)/+layout.svelte | 40 +- frontend/src/routes/(app)/+page.ts | 3 +- .../src/routes/(app)/settings/+page.svelte | 61 +- frontend/src/routes/pdf/+layout.svelte | 9 +- frontend/src/routes/pdf/+page.svelte | 114 +-- go.mod | 2 - go.sum | 4 - 24 files changed, 1053 insertions(+), 709 deletions(-) create mode 100644 backend/utils/debug_windows.go delete mode 100644 backend/utils/mail/eml_pec_reader.go rename backend/utils/{ => mail}/mailparser.go (94%) delete mode 100644 backend/utils/mail/msg_parser.go diff --git a/app.go b/app.go index c525781..d8b2cba 100644 --- a/app.go +++ b/app.go @@ -101,12 +101,12 @@ func (a *App) ReadMSG(filePath string, useExternalConverter bool) (*internal.Ema if useExternalConverter { return internal.ReadMsgFile(filePath) } - return internal.OSSReadMsgFile(filePath) + return internal.ReadMsgFile(filePath) } // ReadMSGOSS reads a .msg file and returns the email data func (a *App) ReadMSGOSS(filePath string) (*internal.EmailData, error) { - return internal.OSSReadMsgFile(filePath) + return internal.ReadMsgFile(filePath) } // ShowOpenFileDialog shows the file open dialog for EML files @@ -497,3 +497,10 @@ func (a *App) OpenDefaultAppsSettings() error { cmd := exec.Command("cmd", "/c", "start", "ms-settings:defaultapps") return cmd.Start() } + +func (a *App) IsDebuggerRunning() bool { + if a == nil { + return false + } + return utils.IsDebugged() +} diff --git a/backend/utils/debug_windows.go b/backend/utils/debug_windows.go new file mode 100644 index 0000000..486582f --- /dev/null +++ b/backend/utils/debug_windows.go @@ -0,0 +1,11 @@ +package utils + +import "syscall" + +// IsDebugged reports whether a debugger is attached (Windows). +func IsDebugged() bool { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + isDebuggerPresent := kernel32.NewProc("IsDebuggerPresent") + ret, _, _ := isDebuggerPresent.Call() + return ret != 0 +} diff --git a/backend/utils/mail/eml_pec_reader.go b/backend/utils/mail/eml_pec_reader.go deleted file mode 100644 index bcf15a7..0000000 --- a/backend/utils/mail/eml_pec_reader.go +++ /dev/null @@ -1,121 +0,0 @@ -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 -} diff --git a/backend/utils/mail/eml_reader.go b/backend/utils/mail/eml_reader.go index 009176b..f6b8167 100644 --- a/backend/utils/mail/eml_reader.go +++ b/backend/utils/mail/eml_reader.go @@ -1,6 +1,7 @@ package internal import ( + "bytes" "fmt" "io" "net/mail" @@ -8,8 +9,6 @@ import ( "strings" "unicode/utf8" - "emly/backend/utils" - "golang.org/x/text/encoding/charmap" "golang.org/x/text/transform" ) @@ -39,7 +38,7 @@ func ReadEmlFile(filePath string) (*EmailData, error) { } defer file.Close() - email, err := utils.Parse(file) + email, err := Parse(file) if err != nil { return nil, fmt.Errorf("failed to parse email: %w", err) } @@ -122,3 +121,110 @@ func convertToUTF8(s string) string { } return decoded } + +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 := 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 := 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 +} diff --git a/backend/utils/mailparser.go b/backend/utils/mail/mailparser.go similarity index 94% rename from backend/utils/mailparser.go rename to backend/utils/mail/mailparser.go index a3c3bee..0383488 100644 --- a/backend/utils/mailparser.go +++ b/backend/utils/mail/mailparser.go @@ -1,11 +1,10 @@ -package utils +package internal import ( "bytes" "encoding/base64" "fmt" "io" - "io/ioutil" "mime" "mime/multipart" "mime/quotedprintable" @@ -14,11 +13,13 @@ import ( "time" ) -const contentTypeMultipartMixed = "multipart/mixed" -const contentTypeMultipartAlternative = "multipart/alternative" -const contentTypeMultipartRelated = "multipart/related" -const contentTypeTextHtml = "text/html" -const contentTypeTextPlain = "text/plain" +const ( + contentTypeMultipartMixed = "multipart/mixed" + contentTypeMultipartAlternative = "multipart/alternative" + contentTypeMultipartRelated = "multipart/related" + contentTypeTextHtml = "text/html" + contentTypeTextPlain = "text/plain" +) // Parse an email message read from io.Reader into parsemail.Email struct func Parse(r io.Reader) (email Email, err error) { @@ -46,10 +47,10 @@ func Parse(r io.Reader) (email Email, err error) { case contentTypeMultipartRelated: email.TextBody, email.HTMLBody, email.EmbeddedFiles, err = parseMultipartRelated(msg.Body, params["boundary"]) case contentTypeTextPlain: - message, _ := ioutil.ReadAll(msg.Body) + message, _ := io.ReadAll(msg.Body) email.TextBody = strings.TrimSuffix(string(message[:]), "\n") case contentTypeTextHtml: - message, _ := ioutil.ReadAll(msg.Body) + message, _ := io.ReadAll(msg.Body) email.HTMLBody = strings.TrimSuffix(string(message[:]), "\n") default: email.Content, err = decodeContent(msg.Body, msg.Header.Get("Content-Transfer-Encoding")) @@ -122,14 +123,14 @@ func parseMultipartRelated(msg io.Reader, boundary string) (textBody, htmlBody s switch contentType { case contentTypeTextPlain: - ppContent, err := ioutil.ReadAll(part) + ppContent, err := io.ReadAll(part) if err != nil { return textBody, htmlBody, embeddedFiles, err } textBody += strings.TrimSuffix(string(ppContent[:]), "\n") case contentTypeTextHtml: - ppContent, err := ioutil.ReadAll(part) + ppContent, err := io.ReadAll(part) if err != nil { return textBody, htmlBody, embeddedFiles, err } @@ -179,14 +180,14 @@ func parseMultipartAlternative(msg io.Reader, boundary string) (textBody, htmlBo switch contentType { case contentTypeTextPlain: - ppContent, err := ioutil.ReadAll(part) + ppContent, err := io.ReadAll(part) if err != nil { return textBody, htmlBody, embeddedFiles, err } textBody += strings.TrimSuffix(string(ppContent[:]), "\n") case contentTypeTextHtml: - ppContent, err := ioutil.ReadAll(part) + ppContent, err := io.ReadAll(part) if err != nil { return textBody, htmlBody, embeddedFiles, err } @@ -249,14 +250,14 @@ func parseMultipartMixed(msg io.Reader, boundary string) (textBody, htmlBody str return textBody, htmlBody, attachments, embeddedFiles, err } } else if contentType == contentTypeTextPlain { - ppContent, err := ioutil.ReadAll(part) + ppContent, err := io.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) + ppContent, err := io.ReadAll(part) if err != nil { return textBody, htmlBody, attachments, embeddedFiles, err } @@ -354,21 +355,21 @@ 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) + b, err := io.ReadAll(decoded) if err != nil { return nil, err } return bytes.NewReader(b), nil case "7bit": - dd, err := ioutil.ReadAll(content) + dd, err := io.ReadAll(content) if err != nil { return nil, err } return bytes.NewReader(dd), nil case "8bit", "binary", "": - dd, err := ioutil.ReadAll(content) + dd, err := io.ReadAll(content) if err != nil { return nil, err } @@ -376,7 +377,7 @@ func decodeContent(content io.Reader, encoding string) (io.Reader, error) { return bytes.NewReader(dd), nil case "quoted-printable": decoded := quotedprintable.NewReader(content) - dd, err := ioutil.ReadAll(decoded) + dd, err := io.ReadAll(decoded) if err != nil { return nil, err } diff --git a/backend/utils/mail/msg_parser.go b/backend/utils/mail/msg_parser.go deleted file mode 100644 index 559f4fc..0000000 --- a/backend/utils/mail/msg_parser.go +++ /dev/null @@ -1,261 +0,0 @@ -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 -} diff --git a/backend/utils/mail/msg_reader.go b/backend/utils/mail/msg_reader.go index 76037c9..af75c87 100644 --- a/backend/utils/mail/msg_reader.go +++ b/backend/utils/mail/msg_reader.go @@ -1,148 +1,741 @@ package internal import ( + "bufio" + "bytes" "encoding/base64" - "encoding/json" + "encoding/binary" + "errors" "fmt" + "io" + "mime" + "mime/multipart" + "mime/quotedprintable" + "net/textproto" "os" - "os/exec" - "path/filepath" "strings" - "time" + "unicode/utf16" ) -// ReadMsgFile reads a .msg file using the native Go parser. -func ReadMsgFile(filePath string) (*EmailData, error) { - return ReadMsgPecFile(filePath) +const ( + cfbSignature = 0xE11AB1A1E011CFD0 + miniStreamCutoff = 4096 + maxRegularSector = 0xFFFFFFFA + noStream = 0xFFFFFFFF + difatInHeader = 109 + directoryEntrySize = 128 +) + +const ( + pidTagSubject = 0x0037 + pidTagConversationTopic = 0x0070 + pidTagMessageClass = 0x001A + pidTagBody = 0x1000 + pidTagBodyHTML = 0x1013 + pidTagSenderName = 0x0C1A + pidTagSenderEmailAddress = 0x0C1F + pidTagSentRepresentingName = 0x0042 + pidTagSentRepresentingAddr = 0x0065 + pidTagDisplayTo = 0x0E04 + pidTagDisplayCc = 0x0E03 + pidTagDisplayBcc = 0x0E02 + pidTagAttachFilename = 0x3704 + pidTagAttachLongFilename = 0x3707 + pidTagAttachData = 0x3701 + pidTagAttachMimeTag = 0x370E + propTypeString8 = 0x001E + propTypeString = 0x001F + propTypeBinary = 0x0102 +) + +type cfbHeader struct { + Signature uint64 + CLSID [16]byte + MinorVersion uint16 + MajorVersion uint16 + ByteOrder uint16 + SectorShift uint16 + MiniSectorShift uint16 + Reserved1 [6]byte + TotalSectors uint32 + FATSectors uint32 + FirstDirectorySector uint32 + TransactionSignature uint32 + MiniStreamCutoff uint32 + FirstMiniFATSector uint32 + MiniFATSectors uint32 + FirstDIFATSector uint32 + DIFATSectors uint32 + DIFAT [109]uint32 } -func OSSReadMsgFile(filePath string) (*EmailData, error) { - return parseMsgFile(filePath) +type directoryEntry struct { + Name [64]byte + NameLen uint16 + ObjectType uint8 + ColorFlag uint8 + LeftSiblingID uint32 + RightSiblingID uint32 + ChildID uint32 + CLSID [16]byte + StateBits uint32 + CreationTime uint64 + ModifiedTime uint64 + StartingSectorLoc uint32 + StreamSize uint64 } -// 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() +type dirNode struct { + Index int + Entry directoryEntry + Name string + Children []*dirNode +} + +type cfbReader struct { + reader io.ReaderAt + header cfbHeader + sectorSize int + fat []uint32 + miniFAT []uint32 + dirEntries []directoryEntry + root *dirNode + nodesByIdx map[int]*dirNode + miniStream []byte +} + +func ReadMsgFile(path string) (*EmailData, error) { + f, err := os.Open(path) if err != nil { - return nil, fmt.Errorf("failed to get executable path: %w", err) + return nil, 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)) + defer func(f *os.File) { + _ = f.Close() + }(f) + _, err = f.Stat() if err != nil { - return nil, fmt.Errorf("failed to create temp file: %w", err) + return nil, err } - tempPath := tempFile.Name() - tempFile.Close() // Close immediately, exe will write to it - // defer os.Remove(tempPath) // Cleanup + return Read(f) +} - // 3. Run signed_msg.exe - // 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() +func Read(r io.ReaderAt) (*EmailData, error) { + cfb, err := newCFBReader(r) if err != nil { - return nil, fmt.Errorf("signed_msg.exe failed: %s, output: %s", err, string(output)) + return nil, err + } + return parseMessage(cfb) +} + +func newCFBReader(r io.ReaderAt) (*cfbReader, error) { + cfb := &cfbReader{reader: r, nodesByIdx: make(map[int]*dirNode)} + + headerData := make([]byte, 512) + if _, err := r.ReadAt(headerData, 0); err != nil { + return nil, err } - // 4. Read JSON output - jsonData, err := os.ReadFile(tempPath) - if err != nil { - return nil, fmt.Errorf("failed to read json output: %w", err) + buf := bytes.NewReader(headerData) + if err := binary.Read(buf, binary.LittleEndian, &cfb.header); err != nil { + return nil, 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 cfb.header.Signature != cfbSignature { + return nil, errors.New("invalid MSG file") } - if err := json.Unmarshal(jsonData, &pecJson); err != nil { - return nil, fmt.Errorf("failed to parse json output: %w", err) + cfb.sectorSize = 1 << cfb.header.SectorShift + + if err := cfb.readFAT(); err != nil { + return nil, err + } + if err := cfb.readDirectories(); err != nil { + return nil, err + } + cfb.buildTree() + + if cfb.header.FirstMiniFATSector < maxRegularSector { + _ = cfb.readMiniFAT() + } + if len(cfb.dirEntries) > 0 && cfb.dirEntries[0].StreamSize > 0 { + _ = cfb.readMiniStream() } - // 6. Check for postacert.eml to determine if it is a PEC - var foundPostacert bool - var hasDatiCert, hasSmime bool + return cfb, nil +} - // We'll prepare attachments listing at the same time - var attachments []EmailAttachment +func (cfb *cfbReader) sectorOffset(sector uint32) int64 { + return int64(sector+1) * int64(cfb.sectorSize) +} - for _, att := range pecJson.Attachments { - attData, err := base64.StdEncoding.DecodeString(att.Data) +func (cfb *cfbReader) readSector(sector uint32) ([]byte, error) { + data := make([]byte, cfb.sectorSize) + _, err := cfb.reader.ReadAt(data, cfb.sectorOffset(sector)) + return data, err +} + +func (cfb *cfbReader) readFAT() error { + var fatSectors []uint32 + for i := 0; i < difatInHeader && i < int(cfb.header.FATSectors); i++ { + if cfb.header.DIFAT[i] < maxRegularSector { + fatSectors = append(fatSectors, cfb.header.DIFAT[i]) + } + } + + if cfb.header.DIFATSectors > 0 && cfb.header.FirstDIFATSector < maxRegularSector { + difatSector := cfb.header.FirstDIFATSector + for i := uint32(0); i < cfb.header.DIFATSectors && difatSector < maxRegularSector; i++ { + data, err := cfb.readSector(difatSector) + if err != nil { + return err + } + entriesPerSector := cfb.sectorSize/4 - 1 + for j := 0; j < entriesPerSector && len(fatSectors) < int(cfb.header.FATSectors); j++ { + sector := binary.LittleEndian.Uint32(data[j*4:]) + if sector < maxRegularSector { + fatSectors = append(fatSectors, sector) + } + } + difatSector = binary.LittleEndian.Uint32(data[entriesPerSector*4:]) + } + } + + entriesPerSector := cfb.sectorSize / 4 + cfb.fat = make([]uint32, 0, len(fatSectors)*entriesPerSector) + for _, sector := range fatSectors { + data, err := cfb.readSector(sector) if err != nil { - fmt.Printf("Failed to decode attachment %s: %v\n", att.Filename, err) + return err + } + for i := 0; i < entriesPerSector; i++ { + cfb.fat = append(cfb.fat, binary.LittleEndian.Uint32(data[i*4:])) + } + } + return nil +} + +func (cfb *cfbReader) readDirectories() error { + entriesPerSector := cfb.sectorSize / directoryEntrySize + sector := cfb.header.FirstDirectorySector + + for sector < maxRegularSector { + data, err := cfb.readSector(sector) + if err != nil { + return err + } + for i := 0; i < entriesPerSector; i++ { + var entry directoryEntry + buf := bytes.NewReader(data[i*directoryEntrySize:]) + if err := binary.Read(buf, binary.LittleEndian, &entry); err != nil { + return err + } + cfb.dirEntries = append(cfb.dirEntries, entry) + } + if int(sector) >= len(cfb.fat) { + break + } + sector = cfb.fat[sector] + } + return nil +} + +func (cfb *cfbReader) buildTree() { + for i := range cfb.dirEntries { + entry := &cfb.dirEntries[i] + if entry.ObjectType == 0 { + continue + } + node := &dirNode{Index: i, Entry: *entry, Name: getDirName(entry)} + cfb.nodesByIdx[i] = node + } + + if node, ok := cfb.nodesByIdx[0]; ok { + cfb.root = node + } + + for _, node := range cfb.nodesByIdx { + if node.Entry.ChildID != noStream { + cfb.collectChildren(node, int(node.Entry.ChildID)) + } + } +} + +func (cfb *cfbReader) collectChildren(parent *dirNode, startIdx int) { + var traverse func(idx int) + traverse = func(idx int) { + if idx < 0 || idx >= len(cfb.dirEntries) || uint32(idx) == noStream { + return + } + child, ok := cfb.nodesByIdx[idx] + if !ok { + return + } + if child.Entry.LeftSiblingID != noStream { + traverse(int(child.Entry.LeftSiblingID)) + } + parent.Children = append(parent.Children, child) + if child.Entry.RightSiblingID != noStream { + traverse(int(child.Entry.RightSiblingID)) + } + } + traverse(startIdx) +} + +func (cfb *cfbReader) readMiniFAT() error { + entriesPerSector := cfb.sectorSize / 4 + sector := cfb.header.FirstMiniFATSector + for sector < maxRegularSector { + data, err := cfb.readSector(sector) + if err != nil { + return err + } + for i := 0; i < entriesPerSector; i++ { + cfb.miniFAT = append(cfb.miniFAT, binary.LittleEndian.Uint32(data[i*4:])) + } + if int(sector) >= len(cfb.fat) { + break + } + sector = cfb.fat[sector] + } + return nil +} + +func (cfb *cfbReader) readMiniStream() error { + root := cfb.dirEntries[0] + cfb.miniStream = make([]byte, 0, root.StreamSize) + sector := root.StartingSectorLoc + remaining := int64(root.StreamSize) + + for sector < maxRegularSector && remaining > 0 { + data, err := cfb.readSector(sector) + if err != nil { + return err + } + toRead := int64(cfb.sectorSize) + if toRead > remaining { + toRead = remaining + } + cfb.miniStream = append(cfb.miniStream, data[:toRead]...) + remaining -= toRead + if int(sector) >= len(cfb.fat) { + break + } + sector = cfb.fat[sector] + } + return nil +} + +func (cfb *cfbReader) readStream(entry *directoryEntry) ([]byte, error) { + if entry.StreamSize == 0 { + return nil, nil + } + if entry.StreamSize < miniStreamCutoff { + return cfb.readMiniStreamData(entry) + } + return cfb.readRegularStream(entry) +} + +func (cfb *cfbReader) readMiniStreamData(entry *directoryEntry) ([]byte, error) { + miniSectorSize := 1 << cfb.header.MiniSectorShift + data := make([]byte, 0, entry.StreamSize) + sector := entry.StartingSectorLoc + remaining := int64(entry.StreamSize) + + for sector < maxRegularSector && remaining > 0 { + offset := int(sector) * miniSectorSize + if offset >= len(cfb.miniStream) { + break + } + toRead := miniSectorSize + if int64(toRead) > remaining { + toRead = int(remaining) + } + end := offset + toRead + if end > len(cfb.miniStream) { + end = len(cfb.miniStream) + } + data = append(data, cfb.miniStream[offset:end]...) + remaining -= int64(toRead) + if int(sector) >= len(cfb.miniFAT) { + break + } + sector = cfb.miniFAT[sector] + } + return data, nil +} + +func (cfb *cfbReader) readRegularStream(entry *directoryEntry) ([]byte, error) { + data := make([]byte, 0, entry.StreamSize) + sector := entry.StartingSectorLoc + remaining := int64(entry.StreamSize) + + for sector < maxRegularSector && remaining > 0 { + sectorData, err := cfb.readSector(sector) + if err != nil { + return nil, err + } + toRead := int64(cfb.sectorSize) + if toRead > remaining { + toRead = remaining + } + data = append(data, sectorData[:toRead]...) + remaining -= toRead + if int(sector) >= len(cfb.fat) { + break + } + sector = cfb.fat[sector] + } + return data, nil +} + +func (cfb *cfbReader) readNodeStream(node *dirNode) ([]byte, error) { + return cfb.readStream(&node.Entry) +} + +func getDirName(entry *directoryEntry) string { + if entry.NameLen <= 2 { + return "" + } + nameBytes := entry.Name[:entry.NameLen-2] + runes := make([]rune, 0, len(nameBytes)/2) + for i := 0; i < len(nameBytes); i += 2 { + r := rune(binary.LittleEndian.Uint16(nameBytes[i:])) + if r != 0 { + runes = append(runes, r) + } + } + return string(runes) +} + +func parseMessage(cfb *cfbReader) (*EmailData, error) { + if cfb.root == nil { + return nil, errors.New("no root directory") + } + + props := make(map[uint32][]byte) + + for _, child := range cfb.root.Children { + if child.Entry.ObjectType == 2 && strings.HasPrefix(child.Name, "__substg1.0_") { + propID, propType := parsePropertyName(child.Name) + if propID != 0 { + data, _ := cfb.readNodeStream(child) + if data != nil { + props[(propID<<16)|uint32(propType)] = data + } + } + } + } + + email := &EmailData{} + + email.Subject = getPropString(props, pidTagSubject) + if email.Subject == "" { + email.Subject = getPropString(props, pidTagConversationTopic) + } + + email.Body = getPropString(props, pidTagBodyHTML) + if email.Body == "" { + email.Body = getPropBinary(props, pidTagBodyHTML) + } + if email.Body == "" { + email.Body = textToHTML(getPropString(props, pidTagBody)) + } + + from := getPropString(props, pidTagSenderName) + if from == "" { + from = getPropString(props, pidTagSentRepresentingName) + } + fromEmail := getPropString(props, pidTagSenderEmailAddress) + if fromEmail == "" { + fromEmail = getPropString(props, pidTagSentRepresentingAddr) + } + if fromEmail != "" { + email.From = fmt.Sprintf("%s <%s>", from, fromEmail) + } else { + email.From = from + } + + email.To = splitRecipients(getPropString(props, pidTagDisplayTo)) + email.Cc = splitRecipients(getPropString(props, pidTagDisplayCc)) + email.Bcc = splitRecipients(getPropString(props, pidTagDisplayBcc)) + + msgClass := getPropString(props, pidTagMessageClass) + email.IsPec = strings.Contains(strings.ToLower(msgClass), "smime") || + strings.Contains(strings.ToLower(email.Subject), "posta certificata") + + for _, child := range cfb.root.Children { + if strings.HasPrefix(child.Name, "__attach_version1.0_#") { + att := parseAttachment(cfb, child) + if att != nil { + if strings.HasPrefix(att.ContentType, "multipart/") { + innerAtts := extractMIMEAttachments(att.Data) + if len(innerAtts) > 0 { + email.HasInnerEmail = true + email.Attachments = append(email.Attachments, innerAtts...) + } + } else { + email.Attachments = append(email.Attachments, *att) + } + } + } + } + + return email, nil +} + +func parsePropertyName(name string) (uint32, uint16) { + if len(name) < 20 { + return 0, 0 + } + hexPart := name[12:] + if len(hexPart) < 8 { + return 0, 0 + } + var propID uint32 + var propType uint16 + _, _ = fmt.Sscanf(hexPart[:4], "%04X", &propID) + _, _ = fmt.Sscanf(hexPart[4:8], "%04X", &propType) + return propID, propType +} + +func getPropString(props map[uint32][]byte, propID uint32) string { + if data, ok := props[(propID<<16)|propTypeString]; ok { + return decodeUTF16(data) + } + if data, ok := props[(propID<<16)|propTypeString8]; ok { + return strings.TrimRight(string(data), "\x00") + } + return "" +} + +func getPropBinary(props map[uint32][]byte, propID uint32) string { + if data, ok := props[(propID<<16)|propTypeBinary]; ok { + return string(data) + } + return "" +} + +func textToHTML(text string) string { + if text == "" { + return "" + } + text = strings.ReplaceAll(text, "&", "&") + text = strings.ReplaceAll(text, "<", "<") + text = strings.ReplaceAll(text, ">", ">") + text = strings.ReplaceAll(text, "\r\n", "
") + text = strings.ReplaceAll(text, "\n", "
") + return text +} + +func decodeUTF16(data []byte) string { + if len(data) < 2 { + return "" + } + u16s := make([]uint16, len(data)/2) + for i := range u16s { + u16s[i] = binary.LittleEndian.Uint16(data[i*2:]) + } + for len(u16s) > 0 && u16s[len(u16s)-1] == 0 { + u16s = u16s[:len(u16s)-1] + } + return string(utf16.Decode(u16s)) +} + +func splitRecipients(s string) []string { + if s == "" { + return nil + } + parts := strings.Split(s, ";") + var result []string + for _, p := range parts { + p = strings.TrimSpace(p) + if p != "" { + result = append(result, p) + } + } + return result +} + +func parseAttachment(cfb *cfbReader, node *dirNode) *EmailAttachment { + att := &EmailAttachment{} + + for _, child := range node.Children { + if child.Entry.ObjectType != 2 || !strings.HasPrefix(child.Name, "__substg1.0_") { + continue + } + propID, propType := parsePropertyName(child.Name) + data, _ := cfb.readNodeStream(child) + if data == nil { continue } - filenameLower := strings.ToLower(att.Filename) - if filenameLower == "postacert.eml" { - foundPostacert = true + switch propID { + case pidTagAttachLongFilename: + if fn := decodePropertyString(data, propType); fn != "" { + att.Filename = fn + } + case pidTagAttachFilename: + if att.Filename == "" { + att.Filename = decodePropertyString(data, propType) + } + case pidTagAttachMimeTag: + att.ContentType = decodePropertyString(data, propType) + case pidTagAttachData: + att.Data = data } - 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 + if att.Filename == "" && att.Data == nil { + return 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 + return att +} + +func decodePropertyString(data []byte, propType uint16) string { + switch propType { + case propTypeString: + return decodeUTF16(data) + case propTypeString8: + return strings.TrimRight(string(data), "\x00") + } + return "" +} + +func extractMIMEAttachments(data []byte) []EmailAttachment { + data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) + data = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")) + + reader := bufio.NewReader(bytes.NewReader(data)) + tp := textproto.NewReader(reader) + headers, err := tp.ReadMIMEHeader() + if err != nil { + return nil + } + + contentType := headers.Get("Content-Type") + mediaType, params, _ := mime.ParseMediaType(contentType) + + if !strings.HasPrefix(mediaType, "multipart/") { + return nil + } + + boundary := params["boundary"] + if boundary == "" { + return nil + } + + body, _ := io.ReadAll(reader) + return parseMIMEParts(body, boundary) +} + +func parseMIMEParts(body []byte, boundary string) []EmailAttachment { + var attachments []EmailAttachment + mr := multipart.NewReader(bytes.NewReader(body), boundary) + + for { + part, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + break + } + + partBody, _ := io.ReadAll(part) + contentType := part.Header.Get("Content-Type") + mediaType, params, _ := mime.ParseMediaType(contentType) + encoding := part.Header.Get("Content-Transfer-Encoding") + + if strings.HasPrefix(mediaType, "multipart/") { + if b := params["boundary"]; b != "" { + attachments = append(attachments, parseMIMEParts(partBody, b)...) + } + continue + } + + if mediaType == "message/rfc822" { + filename := getFilename(part.Header, params) + if filename == "" { + filename = "email.eml" + } + attachments = append(attachments, EmailAttachment{ + Filename: filename, + ContentType: "message/rfc822", + Data: partBody, + }) + innerAtts := extractFromRFC822(partBody) + attachments = append(attachments, innerAtts...) + continue + } + + filename := getFilename(part.Header, params) + if filename == "" && mediaType == "application/pkcs7-signature" { + filename = "smime.p7s" + } + + if filename != "" { + decoded := decodeBody(partBody, encoding) + attachments = append(attachments, EmailAttachment{ + Filename: filename, + ContentType: mediaType, + Data: decoded, + }) + } + } + + return attachments +} + +func extractFromRFC822(data []byte) []EmailAttachment { + data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) + data = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")) + + reader := bufio.NewReader(bytes.NewReader(data)) + tp := textproto.NewReader(reader) + headers, err := tp.ReadMIMEHeader() + if err != nil { + return nil + } + + contentType := headers.Get("Content-Type") + mediaType, params, _ := mime.ParseMediaType(contentType) + + if !strings.HasPrefix(mediaType, "multipart/") { + return nil + } + + boundary := params["boundary"] + if boundary == "" { + return nil + } + + body, _ := io.ReadAll(reader) + return parseMIMEParts(body, boundary) +} + +func getFilename(header textproto.MIMEHeader, params map[string]string) string { + if cd := header.Get("Content-Disposition"); cd != "" { + _, dispParams, _ := mime.ParseMediaType(cd) + if fn := dispParams["filename"]; fn != "" { + return fn + } + } + return params["name"] +} + +func decodeBody(body []byte, encoding string) []byte { + switch strings.ToLower(encoding) { + case "base64": + decoded := make([]byte, base64.StdEncoding.DecodedLen(len(body))) + n, err := base64.StdEncoding.Decode(decoded, bytes.TrimSpace(body)) + if err == nil { + return decoded[:n] + } + case "quoted-printable": + decoded, err := io.ReadAll(quotedprintable.NewReader(bytes.NewReader(body))) + if err == nil { + return decoded + } + } + return body } diff --git a/config.ini b/config.ini index c954f14..83496b7 100644 --- a/config.ini +++ b/config.ini @@ -1,6 +1,6 @@ [EMLy] -SDK_DECODER_SEMVER="1.2.1-hotfix_1" -SDK_DECODER_RELEASE_CHANNEL="alpha" -GUI_SEMVER="1.2.2" +SDK_DECODER_SEMVER="1.3.0" +SDK_DECODER_RELEASE_CHANNEL="beta" +GUI_SEMVER="1.2.4" GUI_RELEASE_CHANNEL="beta" LANGUAGE="it" \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 81aec96..91c0895 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -25,3 +25,6 @@ vite.config.ts.timestamp-* # Paraglide src/lib/paraglide project.inlang/cache/ + +# Wails +/src/lib/wailsjs \ No newline at end of file diff --git a/frontend/messages/en.json b/frontend/messages/en.json index b00f221..32c8323 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -85,5 +85,9 @@ "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." + "mail_loading_msg_conversion": "Converting MSG file... This might take a while.", + "mail_pdf_already_open": "The PDF is already open in another window.", + "settings_danger_debugger_protection_label": "Enable attached debugger protection", + "settings_danger_debugger_protection_hint": "This will prevent the app from being debugged by an attached debugger.", + "settings_danger_debugger_protection_info": "Info: This actions are currently not configurable and is always enabled for private builds." } diff --git a/frontend/messages/it.json b/frontend/messages/it.json index b8f5b7b..7013dde 100644 --- a/frontend/messages/it.json +++ b/frontend/messages/it.json @@ -34,7 +34,7 @@ "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 Pericolosa", "settings_danger_zone_description": "Azioni avanzate. Procedere con cautela.", "settings_danger_devtools_label": "Apri DevTools", "settings_danger_devtools_hint": "A causa di limitazioni del framework, i DevTools devono essere aperti manualmente. Per aprire i DevTools, premi Ctrl+Shift+F12.", @@ -85,5 +85,9 @@ "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." + "mail_loading_msg_conversion": "Conversione file MSG in corso... Potrebbe richiedere del tempo.", + "mail_pdf_already_open": "Il file PDF è già aperto in una finestra separata.", + "settings_danger_debugger_protection_label": "Abilita protezione da debugger", + "settings_danger_debugger_protection_hint": "Questo impedirà che il debug dell'app venga eseguito da un debugger collegato.", + "settings_danger_debugger_protection_info": "Info: Questa azione non è attualmente configurabile ed è sempre abilitata per le build private." } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index c20eb6c..646b398 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -80dcc9e81c52df44518195ddfd550d26 \ No newline at end of file +3c4a64d0cfb34e86fac16fceae842e43 \ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/MailViewer.svelte b/frontend/src/lib/components/dashboard/MailViewer.svelte index 1b1952c..e1f018d 100644 --- a/frontend/src/lib/components/dashboard/MailViewer.svelte +++ b/frontend/src/lib/components/dashboard/MailViewer.svelte @@ -1,12 +1,11 @@ @@ -290,7 +276,7 @@ class="att-btn pdf" onclick={() => openPDFHandler(arrayBufferToBase64(att.data), att.filename)} > - + {att.filename} {:else if att.filename.toLowerCase().endsWith(".eml")} @@ -316,7 +302,7 @@ href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`} download={att.filename} > - + {att.filename} {:else} @@ -342,11 +328,11 @@ @@ -641,28 +627,6 @@ 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; diff --git a/frontend/src/lib/stores/settings.svelte.ts b/frontend/src/lib/stores/settings.svelte.ts index 71db66e..9dc42e4 100644 --- a/frontend/src/lib/stores/settings.svelte.ts +++ b/frontend/src/lib/stores/settings.svelte.ts @@ -5,9 +5,11 @@ import { getFromLocalStorage, saveToLocalStorage } from "$lib/utils/localStorage const STORAGE_KEY = "emly_gui_settings"; const defaults: EMLy_GUI_Settings = { - selectedLanguage: "en", + selectedLanguage: "it", useBuiltinPreview: true, + useBuiltinPDFViewer: true, previewFileSupportedTypes: ["jpg", "jpeg", "png"], + enableAttachedDebuggerProtection: true, }; class SettingsStore { diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts index 93cad07..e635eb3 100644 --- a/frontend/src/lib/types.d.ts +++ b/frontend/src/lib/types.d.ts @@ -6,8 +6,8 @@ interface EMLy_GUI_Settings { selectedLanguage: SupportedLanguages = "en" | "it"; useBuiltinPreview: boolean; useBuiltinPDFViewer?: boolean; - useMsgConverter?: boolean; previewFileSupportedTypes?: SupportedFileTypePreview[]; + enableAttachedDebuggerProtection?: boolean; } type SupportedLanguages = "en" | "it"; diff --git a/frontend/src/lib/wailsjs/go/main/App.d.ts b/frontend/src/lib/wailsjs/go/main/App.d.ts index 66a32d8..2db9d0a 100644 --- a/frontend/src/lib/wailsjs/go/main/App.d.ts +++ b/frontend/src/lib/wailsjs/go/main/App.d.ts @@ -18,6 +18,8 @@ export function GetStartupFile():Promise; export function GetViewerData():Promise; +export function IsDebuggerRunning():Promise; + export function OpenDefaultAppsSettings():Promise; export function OpenEMLWindow(arg1:string,arg2:string):Promise; diff --git a/frontend/src/lib/wailsjs/go/main/App.js b/frontend/src/lib/wailsjs/go/main/App.js index 9a176af..a1f4442 100644 --- a/frontend/src/lib/wailsjs/go/main/App.js +++ b/frontend/src/lib/wailsjs/go/main/App.js @@ -3,81 +3,85 @@ // This file is automatically generated. DO NOT EDIT export function CheckIsDefaultEMLHandler() { - return window['go']['main']['App']['CheckIsDefaultEMLHandler'](); + return ObfuscatedCall(0, []); } export function GetConfig() { - return window['go']['main']['App']['GetConfig'](); + return ObfuscatedCall(1, []); } export function GetImageViewerData() { - return window['go']['main']['App']['GetImageViewerData'](); + return ObfuscatedCall(2, []); } export function GetMachineData() { - return window['go']['main']['App']['GetMachineData'](); + return ObfuscatedCall(3, []); } export function GetPDFViewerData() { - return window['go']['main']['App']['GetPDFViewerData'](); + return ObfuscatedCall(4, []); } export function GetStartupFile() { - return window['go']['main']['App']['GetStartupFile'](); + return ObfuscatedCall(5, []); } export function GetViewerData() { - return window['go']['main']['App']['GetViewerData'](); + return ObfuscatedCall(6, []); +} + +export function IsDebuggerRunning() { + return ObfuscatedCall(7, []); } export function OpenDefaultAppsSettings() { - return window['go']['main']['App']['OpenDefaultAppsSettings'](); + return ObfuscatedCall(8, []); } export function OpenEMLWindow(arg1, arg2) { - return window['go']['main']['App']['OpenEMLWindow'](arg1, arg2); + return ObfuscatedCall(9, [arg1, arg2]); } export function OpenImage(arg1, arg2) { - return window['go']['main']['App']['OpenImage'](arg1, arg2); + return ObfuscatedCall(10, [arg1, arg2]); } export function OpenImageWindow(arg1, arg2) { - return window['go']['main']['App']['OpenImageWindow'](arg1, arg2); + return ObfuscatedCall(11, [arg1, arg2]); } export function OpenPDF(arg1, arg2) { - return window['go']['main']['App']['OpenPDF'](arg1, arg2); + return ObfuscatedCall(12, [arg1, arg2]); } export function OpenPDFWindow(arg1, arg2) { - return window['go']['main']['App']['OpenPDFWindow'](arg1, arg2); + return ObfuscatedCall(13, [arg1, arg2]); } export function QuitApp() { - return window['go']['main']['App']['QuitApp'](); + return ObfuscatedCall(14, []); } export function ReadEML(arg1) { - return window['go']['main']['App']['ReadEML'](arg1); + return ObfuscatedCall(15, [arg1]); } export function ReadMSG(arg1, arg2) { - return window['go']['main']['App']['ReadMSG'](arg1, arg2); + return ObfuscatedCall(16, [arg1, arg2]); } export function ReadMSGOSS(arg1) { - return window['go']['main']['App']['ReadMSGOSS'](arg1); + return ObfuscatedCall(17, [arg1]); } export function ReadPEC(arg1) { - return window['go']['main']['App']['ReadPEC'](arg1); + return ObfuscatedCall(18, [arg1]); } export function SaveConfig(arg1) { - return window['go']['main']['App']['SaveConfig'](arg1); + return ObfuscatedCall(19, [arg1]); } export function ShowOpenFileDialog() { - return window['go']['main']['App']['ShowOpenFileDialog'](); + return ObfuscatedCall(20, []); } diff --git a/frontend/src/routes/(app)/+layout.svelte b/frontend/src/routes/(app)/+layout.svelte index fe6baba..bc649af 100644 --- a/frontend/src/routes/(app)/+layout.svelte +++ b/frontend/src/routes/(app)/+layout.svelte @@ -11,7 +11,7 @@ import { Toaster } from "$lib/components/ui/sonner/index.js"; import AppSidebar from "$lib/components/SidebarApp.svelte"; import * as Sidebar from "$lib/components/ui/sidebar/index.js"; - import { dev } from '$app/environment'; + import { dev } from "$app/environment"; import { PanelRightClose, PanelRightOpen, @@ -30,9 +30,11 @@ Quit, } from "$lib/wailsjs/runtime/runtime"; import { RefreshCcwDot } from "@lucide/svelte"; + import { IsDebuggerRunning, QuitApp } from "$lib/wailsjs/go/main/App"; let versionInfo: utils.Config | null = $state(null); let isMaximized = $state(false); + let isDebugerOn: boolean = $state(false); async function syncMaxState() { isMaximized = await WindowIsMaximised(); @@ -67,9 +69,31 @@ } onMount(async () => { + if (browser) { + detectDebugging(); + setInterval(detectDebugging, 1000); + } + versionInfo = data.data as utils.Config; }); + function handleWheel(event: WheelEvent) { + if (event.ctrlKey) { + event.preventDefault(); + } + } + + async function detectDebugging() { + if (!browser) return; + if (isDebugerOn === true) return; // Prevent multiple detections + isDebugerOn = await IsDebuggerRunning(); + if (isDebugerOn) { + if(dev) toast.warning("Debugger is attached."); + await new Promise((resolve) => setTimeout(resolve, 5000)); + await QuitApp(); + } + } + let { data, children } = $props(); const THEME_KEY = "emly_theme"; @@ -101,7 +125,7 @@ syncMaxState(); -
+
{#if dev} - v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy.GUIReleaseChannel} (DEBUG BUILD) + v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy.GUIReleaseChannel} + (DEBUG BUILD) {:else} v{versionInfo?.EMLy.GUISemver}_{versionInfo?.EMLy.GUIReleaseChannel} {/if} @@ -123,12 +148,15 @@
GUI: v{versionInfo.EMLy.GUISemver} - ({versionInfo.EMLy.GUIReleaseChannel}) + ({versionInfo.EMLy.GUIReleaseChannel})
SDK: v{versionInfo.EMLy.SDKDecoderSemver} - ({versionInfo.EMLy.SDKDecoderReleaseChannel}) + ({versionInfo.EMLy.SDKDecoderReleaseChannel})
{/if} @@ -294,7 +322,7 @@ opacity: 0.4; } - .title version debug{ + .title version debug { color: #e11d48; opacity: 1; font-weight: 600; diff --git a/frontend/src/routes/(app)/+page.ts b/frontend/src/routes/(app)/+page.ts index 73ae303..bc74d4f 100644 --- a/frontend/src/routes/(app)/+page.ts +++ b/frontend/src/routes/(app)/+page.ts @@ -23,8 +23,7 @@ export const load: PageLoad = async () => { let emlContent: internal.EmailData; if (startupFile.toLowerCase().endsWith(".msg")) { - const useExt = settingsStore.settings.useMsgConverter ?? true; - emlContent = await ReadMSG(startupFile, useExt); + emlContent = await ReadMSG(startupFile, true); } else { emlContent = await ReadEML(startupFile); } diff --git a/frontend/src/routes/(app)/settings/+page.svelte b/frontend/src/routes/(app)/settings/+page.svelte index 6b64e43..eec679e 100644 --- a/frontend/src/routes/(app)/settings/+page.svelte +++ b/frontend/src/routes/(app)/settings/+page.svelte @@ -24,16 +24,19 @@ import * as m from "$lib/paraglide/messages"; import { setLocale } from "$lib/paraglide/runtime"; import { mailState } from "$lib/stores/mail-state.svelte.js"; + import { dev } from '$app/environment'; let { data } = $props(); let config = $derived(data.config); + let runningInDevMode: boolean = dev || false; + const defaults: EMLy_GUI_Settings = { selectedLanguage: "it", useBuiltinPreview: true, useBuiltinPDFViewer: true, - useMsgConverter: true, previewFileSupportedTypes: ["jpg", "jpeg", "png"], + enableAttachedDebuggerProtection: true, }; async function setLanguage( @@ -60,9 +63,10 @@ useBuiltinPreview: !!s.useBuiltinPreview, useBuiltinPDFViewer: s.useBuiltinPDFViewer ?? defaults.useBuiltinPDFViewer ?? true, - useMsgConverter: s.useMsgConverter ?? defaults.useMsgConverter ?? true, previewFileSupportedTypes: s.previewFileSupportedTypes || defaults.previewFileSupportedTypes || [], + enableAttachedDebuggerProtection: + s.enableAttachedDebuggerProtection ?? defaults.enableAttachedDebuggerProtection ?? true, }; } @@ -71,7 +75,7 @@ (a.selectedLanguage ?? "") === (b.selectedLanguage ?? "") && !!a.useBuiltinPreview === !!b.useBuiltinPreview && !!a.useBuiltinPDFViewer === !!b.useBuiltinPDFViewer && - !!a.useMsgConverter === !!b.useMsgConverter && + !!a.enableAttachedDebuggerProtection === !!b.enableAttachedDebuggerProtection && JSON.stringify(a.previewFileSupportedTypes?.sort()) === JSON.stringify(b.previewFileSupportedTypes?.sort()) ); @@ -366,36 +370,7 @@ - - - {m.settings_msg_converter_title()} - {m.settings_msg_converter_description()} - - -
-
-
- -

- {m.settings_msg_converter_hint()} -

-
- -
-
-
-
- - {#if $dangerZoneEnabled} + {#if $dangerZoneEnabled || dev} +
+
+ +
+ {m.settings_danger_debugger_protection_hint()} +
+
+ +
+
+ {m.settings_danger_debugger_protection_info()} +
+ +
GUI: {config ? `${config.GUISemver} (${config.GUIReleaseChannel})` diff --git a/frontend/src/routes/pdf/+layout.svelte b/frontend/src/routes/pdf/+layout.svelte index c9541c0..9ae9127 100644 --- a/frontend/src/routes/pdf/+layout.svelte +++ b/frontend/src/routes/pdf/+layout.svelte @@ -37,10 +37,17 @@ toggleMaximize(); } + function handleWheel(event: WheelEvent) { + if (event.ctrlKey) { + event.preventDefault(); + } + } + syncMaxState(); -
+ +
import { onMount } from "svelte"; - import type { PageData } from './$types'; + import type { PageData } from "./$types"; import { RotateCcw, RotateCw, ZoomIn, ZoomOut, - AlignHorizontalSpaceAround + AlignHorizontalSpaceAround, } from "@lucide/svelte"; import { sidebarOpen } from "$lib/stores/app"; import { toast } from "svelte-sonner"; import * as pdfjsLib from "pdfjs-dist"; import pdfWorker from "pdfjs-dist/build/pdf.worker.min.mjs?url"; - if (typeof Promise.withResolvers === 'undefined') { + if (typeof Promise.withResolvers === "undefined") { // @ts-ignore Promise.withResolvers = function () { let resolve, reject; @@ -36,7 +36,7 @@ let scale = $state(1.5); // Default scale let error = $state(""); let loading = $state(true); - + let pdfDoc = $state(null); let pageNum = $state(1); let totalPages = $state(0); @@ -53,7 +53,7 @@ const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); + bytes[i] = binaryString.charCodeAt(i); } pdfData = bytes; filename = result.filename; @@ -62,10 +62,10 @@ sidebarOpen.set(false); await loadPDF(); - } else { toast.error("No PDF data provided"); - error = "No PDF data provided. Please open this window from the main EMLy application."; + error = + "No PDF data provided. Please open this window from the main EMLy application."; loading = false; } } catch (e) { @@ -76,29 +76,30 @@ async function loadPDF() { if (!pdfData) return; - + // Set a timeout to prevent infinite loading const timeout = setTimeout(() => { - if (loading) { - loading = false; - error = "Timeout loading PDF. The worker might have failed to initialize."; - toast.error(error); - } + if (loading) { + loading = false; + error = + "Timeout loading PDF. The worker might have failed to initialize."; + toast.error(error); + } }, 10000); try { - const loadingTask = pdfjsLib.getDocument({ data: pdfData }); - pdfDoc = await loadingTask.promise; - totalPages = pdfDoc.numPages; - pageNum = 1; - await renderPage(pageNum); - loading = false; + const loadingTask = pdfjsLib.getDocument({ data: pdfData }); + pdfDoc = await loadingTask.promise; + totalPages = pdfDoc.numPages; + pageNum = 1; + await renderPage(pageNum); + loading = false; } catch (e) { - console.error(e); - error = "Error parsing PDF: " + e; - loading = false; + console.error(e); + error = "Error parsing PDF: " + e; + loading = false; } finally { - clearTimeout(timeout); + clearTimeout(timeout); } } @@ -106,36 +107,36 @@ if (!pdfDoc || !canvasRef) return; if (renderTask) { - await renderTask.promise.catch(() => {}); // Cancel previous render if any (though we wait usually) + await renderTask.promise.catch(() => {}); // Cancel previous render if any (though we wait usually) } try { - const page = await pdfDoc.getPage(num); - - // Calculate scale if needed or use current scale - // We apply rotation to the viewport - const viewport = page.getViewport({ scale: scale, rotation: rotation }); + const page = await pdfDoc.getPage(num); - const canvas = canvasRef; - const context = canvas.getContext('2d'); + // Calculate scale if needed or use current scale + // We apply rotation to the viewport + const viewport = page.getViewport({ scale: scale, rotation: rotation }); - if (!context) return; + const canvas = canvasRef; + const context = canvas.getContext("2d"); - canvas.height = viewport.height; - canvas.width = viewport.width; + if (!context) return; - const renderContext = { - canvasContext: context, - viewport: viewport - }; - - // Cast to any to avoid type mismatch with PDF.js definitions - await page.render(renderContext as any).promise; + canvas.height = viewport.height; + canvas.width = viewport.width; + + const renderContext = { + canvasContext: context, + viewport: viewport, + }; + + // Cast to any to avoid type mismatch with PDF.js definitions + await page.render(renderContext as any).promise; } catch (e: any) { - if (e.name !== 'RenderingCancelledException') { - console.error(e); - toast.error("Error rendering page: " + e.message); - } + if (e.name !== "RenderingCancelledException") { + console.error(e); + toast.error("Error rendering page: " + e.message); + } } } @@ -143,11 +144,13 @@ if (!pdfDoc || !canvasContainerRef) return; // We need to fetch page to get dimensions loading = true; - pdfDoc.getPage(pageNum).then(page => { - const containerWidth = canvasContainerRef!.clientWidth - 40; // padding - const viewport = page.getViewport({ scale: 1, rotation: rotation }); - scale = containerWidth / viewport.width; - renderPage(pageNum).then(() => { loading = false; }); + pdfDoc.getPage(pageNum).then((page) => { + const containerWidth = canvasContainerRef!.clientWidth - 40; // padding + const viewport = page.getViewport({ scale: 1, rotation: rotation }); + scale = containerWidth / viewport.width; + renderPage(pageNum).then(() => { + loading = false; + }); }); } @@ -157,7 +160,7 @@ const _deps = [scale, rotation]; if (pdfDoc) { - renderPage(pageNum); + renderPage(pageNum); } }); @@ -180,7 +183,6 @@ pageNum--; renderPage(pageNum); } -
@@ -345,15 +347,15 @@ padding: 20px; background: #333; /* Dark background for contrast */ } - + canvas { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); max-width: none; /* Allow canvas to be larger than container */ } ::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 10px; + height: 10px; } ::-webkit-scrollbar-track { @@ -372,4 +374,4 @@ ::-webkit-scrollbar-corner { background: transparent; } - \ No newline at end of file + diff --git a/go.mod b/go.mod index cb50373..e1e8842 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.24.4 require ( github.com/jaypipes/ghw v0.21.2 - github.com/richardlehane/mscfb v1.0.6 github.com/wailsapp/wails/v2 v2.11.0 golang.org/x/sys v0.40.0 golang.org/x/text v0.22.0 @@ -29,7 +28,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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/samber/lo v1.49.1 // indirect github.com/tkrajina/go-reflector v0.5.8 // indirect diff --git a/go.sum b/go.sum index d17463a..9c440d3 100644 --- a/go.sum +++ b/go.sum @@ -51,10 +51,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=