Files
EMLy/DOCUMENTATION.md

22 KiB

EMLy Application Documentation

EMLy is a desktop email viewer application built for 3gIT, designed to open and display .eml and .msg email files on Windows. It provides a modern, user-friendly interface for viewing email content, attachments, and metadata.

Table of Contents

  1. Architecture Overview
  2. Technology Stack
  3. Project Structure
  4. Backend (Go)
  5. Frontend (SvelteKit)
  6. State Management
  7. Internationalization (i18n)
  8. UI Components
  9. Key Features
  10. Build & Development

Architecture Overview

EMLy is built using the Wails v2 framework, which combines a Go backend with a web-based frontend. This architecture allows:

  • Go Backend: Handles file operations, Windows API calls, email parsing, and system interactions
  • Web Frontend: Provides the user interface using SvelteKit with Svelte 5
  • Bridge: Wails automatically generates TypeScript bindings for Go functions, enabling seamless communication
┌─────────────────────────────────────────────────────────┐
│                    EMLy Application                      │
├─────────────────────────────────────────────────────────┤
│  Frontend (SvelteKit + Svelte 5)                        │
│  ├── Routes & Pages                                     │
│  ├── UI Components (shadcn-svelte)                      │
│  ├── State Management (Svelte 5 Runes)                  │
│  └── i18n (ParaglideJS)                                 │
├─────────────────────────────────────────────────────────┤
│  Wails Bridge (Auto-generated TypeScript bindings)      │
├─────────────────────────────────────────────────────────┤
│  Backend (Go)                                           │
│  ├── App Logic (app.go)                                 │
│  ├── Email Parsing (backend/utils/mail/)                │
│  ├── Windows APIs (screenshot, debugger detection)      │
│  └── File Operations                                    │
└─────────────────────────────────────────────────────────┘

Technology Stack

Backend

  • Go 1.21+: Primary backend language
  • Wails v2: Desktop application framework
  • Windows Registry API: For checking default file handlers
  • GDI/User32 APIs: For screenshot functionality

Frontend

  • SvelteKit: Application framework
  • Svelte 5: UI framework with Runes ($state, $derived, $effect, $props)
  • TypeScript: Type-safe JavaScript
  • shadcn-svelte: UI component library
  • Tailwind CSS: Utility-first CSS framework
  • ParaglideJS: Internationalization
  • Lucide Icons: Icon library
  • Bun: JavaScript runtime and package manager

Project Structure

EMLy/
├── app.go                    # Main application logic
├── main.go                   # Application entry point
├── logger.go                 # Logging utilities
├── wails.json                # Wails configuration
├── backend/
│   └── utils/
│       ├── mail/
│       │   ├── eml_reader.go     # EML file parsing
│       │   ├── msg_reader.go     # MSG file parsing
│       │   ├── mailparser.go     # MIME email parsing
│       │   └── file_dialog.go    # File dialog utilities
│       ├── screenshot_windows.go  # Windows screenshot capture
│       ├── debug_windows.go       # Debugger detection
│       ├── ini-reader.go          # Configuration file parsing
│       ├── machine-identifier.go  # System info collection
│       └── file-metadata.go       # File metadata utilities
├── frontend/
│   ├── src/
│   │   ├── routes/               # SvelteKit routes
│   │   │   ├── +layout.svelte    # Root layout
│   │   │   ├── +error.svelte     # Error page
│   │   │   ├── (app)/            # Main app route group
│   │   │   │   ├── +layout.svelte    # App layout with sidebar
│   │   │   │   ├── +page.svelte      # Mail viewer page
│   │   │   │   └── settings/
│   │   │   │       └── +page.svelte  # Settings page
│   │   │   ├── image/            # Image viewer route
│   │   │   │   ├── +layout.svelte
│   │   │   │   └── +page.svelte
│   │   │   └── pdf/              # PDF viewer route
│   │   │       ├── +layout.svelte
│   │   │       └── +page.svelte
│   │   ├── lib/
│   │   │   ├── components/       # Svelte components
│   │   │   │   ├── MailViewer.svelte
│   │   │   │   ├── SidebarApp.svelte
│   │   │   │   ├── UnsavedBar.svelte
│   │   │   │   └── ui/           # shadcn-svelte components
│   │   │   ├── stores/           # State management
│   │   │   │   ├── app.ts
│   │   │   │   ├── mail-state.svelte.ts
│   │   │   │   └── settings.svelte.ts
│   │   │   ├── paraglide/        # i18n runtime
│   │   │   ├── wailsjs/          # Auto-generated Go bindings
│   │   │   ├── types/            # TypeScript types
│   │   │   └── utils/            # Utility functions
│   │   └── messages/             # i18n translation files
│   │       ├── en.json
│   │       └── it.json
│   ├── static/                   # Static assets
│   └── package.json
└── config.ini                    # Application configuration

Backend (Go)

Entry Point (main.go)

The application starts in main.go, which:

  1. Initializes the logger
  2. Parses command-line arguments for:
    • .eml or .msg files to open on startup
    • --view-image=<path> for image viewer mode
    • --view-pdf=<path> for PDF viewer mode
  3. Configures Wails with window options
  4. Sets up single-instance lock to prevent multiple main windows
  5. Binds the App struct for frontend access
// Single instance with unique ID based on mode
uniqueId := "emly-app-lock"
if strings.Contains(arg, "--view-image") {
    uniqueId = "emly-viewer-" + arg
    windowTitle = "EMLy Image Viewer"
}

Application Core (app.go)

The App struct is the main application controller, exposed to the frontend via Wails bindings.

Key Properties

type App struct {
    ctx                 context.Context
    StartupFilePath     string           // File opened via command line
    CurrentMailFilePath string           // Currently loaded mail file
    openImages          map[string]bool  // Track open image viewers
    openPDFs            map[string]bool  // Track open PDF viewers
    openEMLs            map[string]bool  // Track open EML viewers
}

Core Methods

Method Description
GetConfig() Returns application configuration from config.ini
GetStartupFile() Returns file path passed via command line
SetCurrentMailFilePath() Updates the current mail file path
ReadEML(path) Parses an EML file and returns email data
ReadMSG(path) Parses an MSG file and returns email data
ReadPEC(path) Parses PEC (Italian certified email) files
ShowOpenFileDialog() Opens native file picker for EML/MSG files
OpenImageWindow(data, filename) Opens image in new viewer window
OpenPDFWindow(data, filename) Opens PDF in new viewer window
OpenEMLWindow(data, filename) Opens EML attachment in new window
TakeScreenshot() Captures window screenshot as base64 PNG
SubmitBugReport(input) Creates bug report with screenshot and system info
ExportSettings(json) Exports settings to JSON file
ImportSettings() Imports settings from JSON file
IsDebuggerRunning() Checks if a debugger is attached
QuitApp() Terminates the application

Email Parsing (backend/utils/mail/)

EML Reader (eml_reader.go)

Reads standard .eml files using the mailparser.go MIME parser.

MSG Reader (msg_reader.go)

Handles Microsoft Outlook .msg files using external conversion.

Mail Parser (mailparser.go)

A comprehensive MIME email parser that handles:

  • Multipart messages (mixed, alternative, related)
  • Text and HTML bodies
  • Attachments with proper content-type detection
  • Embedded files (inline images)
  • Various content transfer encodings (base64, quoted-printable, 7bit, 8bit)

The EmailData structure returned to the frontend:

type EmailData struct {
    Subject     string
    From        string
    To          []string
    Cc          []string
    Bcc         []string
    Body        string          // HTML or text body
    Attachments []AttachmentData
    IsPec       bool            // Italian certified email
}

Screenshot Utility (backend/utils/screenshot_windows.go)

Captures the application window using Windows GDI APIs:

  • Uses FindWindowW to locate window by title
  • Uses DwmGetWindowAttribute for DPI-aware window bounds
  • Creates compatible DC and bitmap for capture
  • Returns image as base64-encoded PNG

Frontend (SvelteKit)

Route Structure

SvelteKit uses file-based routing. The (app) folder is a route group that applies the main app layout.

routes/
├── +layout.svelte          # Root layout (minimal)
├── +error.svelte           # Global error page
├── (app)/                  # Main app group
│   ├── +layout.svelte      # App layout with titlebar, sidebar, footer
│   ├── +layout.ts          # Server data loader
│   ├── +page.svelte        # Main mail viewer page
│   └── settings/
│       ├── +page.svelte    # Settings page
│       └── +layout.ts      # Settings data loader
├── image/                  # Standalone image viewer
│   ├── +layout.svelte
│   └── +page.svelte
└── pdf/                    # Standalone PDF viewer
    ├── +layout.svelte
    └── +page.svelte

Main App Layout ((app)/+layout.svelte)

The app layout provides:

  1. Custom Titlebar: Windows-style titlebar with minimize/maximize/close buttons

    • Draggable for window movement
    • Double-click to maximize/restore
  2. Sidebar Provider: Collapsible navigation sidebar

  3. Footer Bar: Quick access icons for:

    • Toggle sidebar
    • Navigate to home
    • Navigate to settings
    • Open bug report dialog
    • Reload application
  4. Bug Report Dialog: Complete bug reporting system with:

    • Screenshot capture on dialog open
    • Name, email, description fields
    • System info collection
    • Creates ZIP archive with all data
  5. Toast Notifications: Using svelte-sonner

  6. Debugger Protection: Detects attached debuggers and can quit if detected

Mail Viewer ((app)/+page.svelte + MailViewer.svelte)

The mail viewer is split into two parts:

  • +page.svelte: Page wrapper that initializes mail state from startup file
  • MailViewer.svelte: Core email viewing component

MailViewer Features

  • Empty State: Shows "Open EML/MSG File" button when no email loaded
  • Email Header: Displays subject, from, to, cc, bcc fields
  • PEC Badge: Shows green badge for Italian certified emails
  • Attachments Bar: Horizontal scrollable list of attachments with type-specific icons
  • Email Body: Rendered in sandboxed iframe for security
  • Loading Overlay: Shows spinner during file loading

Attachment Handling

// Different handlers based on file type
if (att.contentType.startsWith("image/")) {
    // Opens in built-in or external viewer based on settings
    await OpenImageWindow(base64Data, filename);
} else if (att.filename.toLowerCase().endsWith(".pdf")) {
    // Opens in built-in or external PDF viewer
    await OpenPDFWindow(base64Data, filename);
} else if (att.filename.toLowerCase().endsWith(".eml")) {
    // Opens in new EMLy instance
    await OpenEMLWindow(base64Data, filename);
} else {
    // Download as file
    <a href={dataUrl} download={filename}>...</a>
}

Settings Page ((app)/settings/+page.svelte)

Organized into cards with various configuration options:

  1. Language Settings

    • Radio buttons for English/Italian
    • Triggers full page reload on change
  2. Export/Import Settings

    • Export current settings to JSON file
    • Import settings from JSON file
  3. Preview Page Settings

    • Supported image types (JPG, JPEG, PNG)
    • Toggle built-in image viewer
    • Toggle built-in PDF viewer
  4. Danger Zone (Hidden by default, revealed by clicking settings 10 times rapidly)

    • Open DevTools hint
    • Reload application
    • Reset to defaults
    • Debugger protection toggle (disabled in production)
    • Version information display

Unsaved Changes Detection

The settings page tracks changes and shows a persistent toast when there are unsaved modifications:

$effect(() => {
    const dirty = !isSameSettings(normalizeSettings(form), lastSaved);
    unsavedChanges.set(dirty);
    if (dirty) {
        showUnsavedChangesToast({
            onSave: saveToStorage,
            onReset: resetToLastSaved,
        });
    }
});

State Management

EMLy uses a combination of Svelte 5 Runes and traditional Svelte stores.

Mail State (stores/mail-state.svelte.ts)

Uses Svelte 5's $state rune for reactive email data:

class MailState {
    currentEmail = $state<internal.EmailData | null>(null);

    setParams(email: internal.EmailData | null) {
        this.currentEmail = email;
    }

    clear() {
        this.currentEmail = null;
    }
}

export const mailState = new MailState();

Settings Store (stores/settings.svelte.ts)

Manages application settings with localStorage persistence:

class SettingsStore {
    settings = $state<EMLy_GUI_Settings>({ ...defaults });
    hasHydrated = $state(false);

    load() { /* Load from localStorage */ }
    save() { /* Save to localStorage */ }
    update(newSettings: Partial<EMLy_GUI_Settings>) { /* Merge and save */ }
    reset() { /* Reset to defaults */ }
}

Settings schema:

interface EMLy_GUI_Settings {
    selectedLanguage: "en" | "it";
    useBuiltinPreview: boolean;
    useBuiltinPDFViewer: boolean;
    previewFileSupportedTypes: string[];
    enableAttachedDebuggerProtection: boolean;
}

App Store (stores/app.ts)

Traditional Svelte writable stores for UI state:

export const dangerZoneEnabled = writable<boolean>(false);
export const unsavedChanges = writable<boolean>(false);
export const sidebarOpen = writable<boolean>(true);
export const bugReportDialogOpen = writable<boolean>(false);
export const events = writable<AppEvent[]>([]);

Internationalization (i18n)

EMLy uses ParaglideJS for compile-time type-safe translations.

Translation Files

Located in frontend/messages/:

  • en.json - English translations
  • it.json - Italian translations

Message Format

{
    "$schema": "https://inlang.com/schema/inlang-message-format",
    "mail_no_email_selected": "No email selected",
    "mail_open_eml_btn": "Open EML/MSG File",
    "settings_title": "Settings",
    "settings_language_english": "English",
    "settings_language_italian": "Italiano"
}

Usage in Components

import * as m from "$lib/paraglide/messages";

// In template
<h1>{m.settings_title()}</h1>
<button>{m.mail_open_eml_btn()}</button>

Changing Language

import { setLocale } from "$lib/paraglide/runtime";

await setLocale("it", { reload: false });
location.reload(); // Page reload required for full update

UI Components

EMLy uses shadcn-svelte, a port of shadcn/ui for Svelte. Components are located in frontend/src/lib/components/ui/.

Available Components

Component Usage
Button Primary buttons with variants (default, destructive, outline, ghost)
Card Container with header, content, footer sections
Dialog Modal dialogs for bug reports, confirmations
AlertDialog Confirmation dialogs with cancel/continue actions
Switch Toggle switches for boolean settings
Checkbox Multi-select checkboxes
RadioGroup Single-select options (language selection)
Label Form labels
Input Text input fields
Textarea Multi-line text input
Separator Visual dividers
Sidebar Collapsible navigation sidebar
Tooltip Hover tooltips
Sonner Toast notifications
Badge Status badges
Skeleton Loading placeholders

Custom Components

Component Purpose
MailViewer.svelte Email display with header, attachments, body
SidebarApp.svelte Navigation sidebar with menu items
UnsavedBar.svelte Unsaved changes notification bar

Key Features

1. Email Viewing

  • Parse and display EML and MSG files
  • Show email metadata (from, to, cc, bcc, subject)
  • Render HTML email bodies in sandboxed iframe
  • List and handle attachments with type-specific actions

2. Attachment Handling

  • Images: Open in built-in viewer or external app
  • PDFs: Open in built-in viewer or external app
  • EML files: Open in new EMLy window
  • Other files: Download directly

3. Multi-Window Support

The app can spawn separate viewer windows:

  • Image viewer (--view-image=<path>)
  • PDF viewer (--view-pdf=<path>)
  • EML viewer (new app instance with file path)

Each viewer has a unique instance ID to allow multiple concurrent viewers.

4. Bug Reporting

Complete bug reporting system:

  1. Captures screenshot when dialog opens
  2. Collects user input (name, email, description)
  3. Includes current mail file if loaded
  4. Gathers system information
  5. Creates ZIP archive in temp folder
  6. Shows path and allows opening folder

5. Settings Management

  • Language selection (English/Italian)
  • Built-in viewer preferences
  • Supported file type configuration
  • Export/import settings as JSON
  • Reset to defaults

6. Security Features

  • Debugger detection and protection
  • Sandboxed iframe for email body
  • Single-instance lock for main window
  • Disabled link clicking in email body

7. PEC Support

Special handling for Italian Posta Elettronica Certificata (PEC):

  • Detects PEC emails
  • Shows signed mail badge
  • Handles P7S signature files
  • Processes daticert.xml metadata

Build & Development

Prerequisites

  • Go 1.21+
  • Node.js 18+ or Bun
  • Wails CLI v2

Development

# Install frontend dependencies
cd frontend && bun install

# Run in development mode
wails dev

Building

# Build for Windows
wails build -platform windows/amd64

# Output: build/bin/EMLy.exe

Configuration

wails.json configures the build:

{
    "name": "EMLy",
    "frontend:install": "bun install",
    "frontend:build": "bun run build",
    "frontend:dev:watcher": "bun run dev",
    "fileAssociations": [
        {
            "ext": "eml",
            "name": "Email Message",
            "description": "EML File"
        }
    ]
}

File Association

The app registers as a handler for .eml files via Windows file associations, configured in wails.json.


Wails Bindings

Wails automatically generates TypeScript bindings for Go functions. These are located in frontend/src/lib/wailsjs/.

Generated Files

  • go/main/App.ts - TypeScript functions calling Go methods
  • go/models.ts - TypeScript types for Go structs
  • runtime/runtime.ts - Wails runtime functions

Usage Example

import { ReadEML, ShowOpenFileDialog } from "$lib/wailsjs/go/main/App";
import type { internal } from "$lib/wailsjs/go/models";

// Open file dialog
const filePath = await ShowOpenFileDialog();

// Parse email
const email: internal.EmailData = await ReadEML(filePath);

Runtime Events

Wails provides event system for Go-to-JS communication:

import { EventsOn } from "$lib/wailsjs/runtime/runtime";

// Listen for second instance launch
EventsOn("launchArgs", (args: string[]) => {
    // Handle file opened from second instance
});

Error Handling

Frontend

Toast notifications for user-facing errors:

import { toast } from "svelte-sonner";
import * as m from "$lib/paraglide/messages";

try {
    await ReadEML(filePath);
} catch (error) {
    toast.error(m.mail_error_opening());
}

Backend

Errors are returned to frontend and logged:

func (a *App) ReadEML(filePath string) (*internal.EmailData, error) {
    data, err := internal.ReadEmlFile(filePath)
    if err != nil {
        Log("Failed to read EML:", err)
        return nil, err
    }
    return data, nil
}

Debugging

Development Mode

In dev mode (wails dev):

  • Hot reload enabled
  • Debug logs visible
  • DevTools accessible via Ctrl+Shift+F12
  • Danger Zone always visible in settings

Production

  • Debugger protection can terminate app if debugger detected
  • Danger Zone hidden by default
  • Access Danger Zone by clicking settings link 10 times within 4 seconds

License & Credits

EMLy is developed by FOISX @ 3gIT.