Files
EMLy/DOCUMENTATION.md

36 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 - Modular Architecture)                    │
│  ├── app.go          - Core struct & lifecycle          │
│  ├── app_mail.go     - Email parsing (EML/MSG/PEC)      │
│  ├── app_viewer.go   - Viewer window management         │
│  ├── app_screenshot.go - Window capture                 │
│  ├── app_bugreport.go  - Bug reporting system           │
│  ├── app_settings.go   - Settings import/export         │
│  ├── app_system.go     - Windows system utilities       │
│  ├── app_update.go     - Self-hosted update system      │
│  └── backend/utils/    - Shared utilities               │
└─────────────────────────────────────────────────────────┘

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                    # Core App struct, lifecycle, and configuration
├── app_mail.go               # Email reading methods (EML, MSG, PEC)
├── app_viewer.go             # Viewer window management (image, PDF, EML)
├── app_screenshot.go         # Screenshot capture functionality
├── app_bugreport.go          # Bug report creation and submission
├── app_heartbeat.go          # Bug report API heartbeat check
├── app_settings.go           # Settings import/export
├── app_system.go             # Windows system utilities (registry, encoding)
├── 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
│   │   │       └── mail/         # Email utilities (modular)
│   │   │           ├── index.ts          # Barrel export
│   │   │           ├── constants.ts      # IFRAME_UTIL_HTML, CONTENT_TYPES, etc.
│   │   │           ├── data-utils.ts     # arrayBufferToBase64, createDataUrl
│   │   │           ├── attachment-handlers.ts  # openPDFAttachment, openImageAttachment
│   │   │           └── email-loader.ts   # loadEmailFromPath, processEmailBody
│   │   └── 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. The code is organized into multiple files for maintainability.

Key Properties

type App struct {
    ctx                 context.Context  // Wails application context
    StartupFilePath     string           // File opened via command line
    CurrentMailFilePath string           // Currently loaded mail file
    openImagesMux       sync.Mutex       // Mutex for image viewer tracking
    openImages          map[string]bool  // Track open image viewers
    openPDFsMux         sync.Mutex       // Mutex for PDF viewer tracking
    openPDFs            map[string]bool  // Track open PDF viewers
    openEMLsMux         sync.Mutex       // Mutex for EML viewer tracking
    openEMLs            map[string]bool  // Track open EML viewers
}

Backend File Organization

The Go backend is split into logical files:

File Purpose
app.go Core App struct, constructor, lifecycle methods (startup/shutdown), configuration
app_mail.go Email reading: ReadEML, ReadMSG, ReadPEC, ReadMSGOSS, ShowOpenFileDialog
app_viewer.go Viewer windows: OpenImageWindow, OpenPDFWindow, OpenEMLWindow, OpenPDF, OpenImage, GetViewerData
app_screenshot.go Screenshots: TakeScreenshot, SaveScreenshot, SaveScreenshotAs
app_bugreport.go Bug reports: CreateBugReportFolder, SubmitBugReport, zipFolder
app_heartbeat.go API heartbeat: CheckBugReportAPI
app_settings.go Settings I/O: ExportSettings, ImportSettings
app_system.go System utilities: CheckIsDefaultEMLHandler, OpenDefaultAppsSettings, ConvertToUTF8, OpenFolderInExplorer
app_update.go Update system: CheckForUpdates, DownloadUpdate, InstallUpdate, GetUpdateStatus

Core Methods by Category

Lifecycle & Configuration (app.go)

Method Description
startup(ctx) Wails startup callback, saves context
shutdown(ctx) Wails shutdown callback for cleanup
QuitApp() Terminates the application
GetConfig() Returns application configuration from config.ini
SaveConfig(cfg) Saves configuration to config.ini
GetStartupFile() Returns file path passed via command line
SetCurrentMailFilePath() Updates the current mail file path
GetMachineData() Returns system information
IsDebuggerRunning() Checks if a debugger is attached

Email Reading (app_mail.go)

Method Description
ReadEML(path) Parses a standard .eml file
ReadMSG(path, useExternal) Parses a Microsoft .msg file
ReadPEC(path) Parses PEC (Italian certified email) files
ShowOpenFileDialog() Opens native file picker for EML/MSG files

Viewer Windows (app_viewer.go)

Method Description
OpenImageWindow(data, filename) Opens image in built-in viewer
OpenPDFWindow(data, filename) Opens PDF in built-in viewer
OpenEMLWindow(data, filename) Opens EML attachment in new EMLy window
OpenImage(data, filename) Opens image with system default app
OpenPDF(data, filename) Opens PDF with system default app
GetViewerData() Returns viewer data for viewer mode detection

Screenshots (app_screenshot.go)

Method Description
TakeScreenshot() Captures window screenshot as base64 PNG
SaveScreenshot() Saves screenshot to temp directory
SaveScreenshotAs() Opens save dialog for screenshot

Bug Reports (app_bugreport.go)

Method Description
CreateBugReportFolder() Creates folder with screenshot and mail file
SubmitBugReport(input) Creates complete bug report with ZIP archive, attempts server upload
UploadBugReport(folderPath, input) Uploads bug report files to configured API server via multipart POST
CheckBugReportAPI() Checks if the bug report API is reachable via /health endpoint (3s timeout)

Settings (app_settings.go)

Method Description
ExportSettings(json) Exports settings to JSON file
ImportSettings() Imports settings from JSON file

System Utilities (app_system.go)

Method Description
CheckIsDefaultEMLHandler() Checks if EMLy is default for .eml files
OpenDefaultAppsSettings() Opens Windows default apps settings
ConvertToUTF8(string) Converts string to valid UTF-8
OpenFolderInExplorer(path) Opens folder in Windows Explorer

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>
}

Frontend Mail Utilities (lib/utils/mail/)

The frontend email handling code is organized into modular utility files:

File Purpose
index.ts Barrel export for all mail utilities
constants.ts Constants: IFRAME_UTIL_HTML, CONTENT_TYPES, PEC_FILES, EMAIL_EXTENSIONS
data-utils.ts Data conversion: arrayBufferToBase64, createDataUrl, looksLikeBase64, tryDecodeBase64
attachment-handlers.ts Attachment opening: openPDFAttachment, openImageAttachment, openEMLAttachment
email-loader.ts Email loading: loadEmailFromPath, openAndLoadEmail, processEmailBody, isEmailFile

Key Functions

Data Utilities (data-utils.ts)

// Convert ArrayBuffer to base64 string
function arrayBufferToBase64(buffer: unknown): string;

// Create data URL for file downloads
function createDataUrl(contentType: string, base64Data: string): string;

// Check if string looks like base64 encoded content
function looksLikeBase64(content: string): boolean;

// Attempt to decode base64, returns null on failure
function tryDecodeBase64(content: string): string | null;

Attachment Handlers (attachment-handlers.ts)

// Open PDF using built-in or external viewer based on settings
async function openPDFAttachment(base64Data: string, filename: string): Promise<AttachmentHandlerResult>;

// Open image using built-in or external viewer based on settings
async function openImageAttachment(base64Data: string, filename: string): Promise<AttachmentHandlerResult>;

// Open EML attachment in new EMLy window
async function openEMLAttachment(base64Data: string, filename: string): Promise<AttachmentHandlerResult>;

Email Loader (email-loader.ts)

// Load email from file path, handles EML/MSG/PEC detection
async function loadEmailFromPath(filePath: string): Promise<LoadEmailResult>;

// Open file dialog and load selected email
async function openAndLoadEmail(): Promise<LoadEmailResult>;

// Process email body (decode base64, fix encoding)
async function processEmailBody(body: string): Promise<string>;

// Check if file path is a valid email file
function isEmailFile(filePath: string): boolean;

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. Checks if the bug report API is online via heartbeat (CheckBugReportAPI)
  7. If online, attempts to upload to the bug report API server
  8. Falls back to local ZIP if server is offline or upload fails
  9. Shows server confirmation with report ID, or local path with upload warning

Heartbeat Check (app_heartbeat.go)

Before uploading a bug report, the app sends a GET request to {BUGREPORT_API_URL}/health with a 3-second timeout. If the API doesn't respond with status 200, the upload is skipped entirely and only the local ZIP is created. The CheckBugReportAPI() method is also exposed to the frontend for UI status checks.

Bug Report API Server

A separate API server (server/ directory) receives bug reports:

  • Stack: Bun.js + ElysiaJS + MySQL 8
  • Deployment: Docker Compose (docker compose up -d from server/)
  • Auth: Static API key for clients (X-API-Key), static admin key (X-Admin-Key)
  • Rate limiting: HWID-based, configurable (default 5 reports per 24h)
  • Logging: Structured file logging to logs/api.log with format [date] - [time] - [source] - message
  • Endpoints: POST /api/bug-reports (client), GET/DELETE /api/admin/bug-reports (admin)

Bug Report Dashboard

A web dashboard (dashboard/ directory) for browsing, triaging, and downloading bug reports:

  • Stack: SvelteKit (Svelte 5) + TailwindCSS v4 + Drizzle ORM + Bun.js
  • Deployment: Docker service in server/docker-compose.yml, port 3001
  • Database: Connects directly to the same MySQL database via Drizzle ORM (read/write)
  • Features:
    • Paginated reports list with status filter and search (hostname, user, name, email)
    • Report detail view with metadata, description, system info (collapsible JSON), and file list
    • Status management (new → in_review → resolved → closed)
    • Inline screenshot preview for attached screenshots
    • Individual file download and bulk ZIP download (all files + report metadata)
    • Report deletion with confirmation dialog
    • Dark mode UI matching EMLy's aesthetic
  • Authentication: Session-based auth with Lucia v3 + Drizzle ORM adapter
    • Default admin account: username admin, password admin (seeded on first migration)
    • Password hashing with argon2 via @node-rs/argon2
    • Session cookies with automatic refresh
    • Role-based access: admin and user roles
  • User Management: Admin-only /users page for creating/deleting dashboard users
  • Development: cd dashboard && bun install && bun dev (localhost:3001)

Configuration (config.ini)

[EMLy]
BUGREPORT_API_URL="https://your-server.example.com"
BUGREPORT_API_KEY="your-api-key"

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

8. Self-Hosted Update System

Corporate Network Update Management - No third-party services required:

  • Network Share Integration: Check for updates from corporate file shares (UNC paths like \\server\emly-updates)
  • Version Manifest: JSON-based version.json controls what versions are available
  • Dual Channel Support: Separate stable and beta release channels
  • Manual or Automatic: Users can manually check, or app auto-checks on startup
  • Download & Verify: Downloads installers from network share with SHA256 checksum verification
  • One-Click Install: Auto-launches installer with UAC elevation, optionally quits app
  • UI Integration: Full update UI in Settings page with progress indicators
  • Event-Driven: Real-time status updates via Wails events

Configuration (config.ini)

[EMLy]
UPDATE_CHECK_ENABLED="true"      # Enable/disable update checking
UPDATE_PATH="\\server\updates"   # Network share or file:// path
UPDATE_AUTO_CHECK="true"         # Check on startup

Network Share Structure

\\server\emly-updates\
├── version.json                      # Update manifest
├── EMLy_Installer_1.5.0.exe          # Stable release installer
└── EMLy_Installer_1.5.1-beta.exe     # Beta release installer

version.json Format

{
  "stableVersion": "1.5.0",
  "betaVersion": "1.5.1-beta",
  "stableDownload": "EMLy_Installer_1.5.0.exe",
  "betaDownload": "EMLy_Installer_1.5.1-beta.exe",
  "sha256Checksums": {
    "EMLy_Installer_1.5.0.exe": "abc123...",
    "EMLy_Installer_1.5.1-beta.exe": "def456..."
  },
  "releaseNotes": {
    "1.5.0": "Bug fixes and performance improvements",
    "1.5.1-beta": "New feature preview"
  }
}

Update Flow

  1. Check: App reads version.json from configured network path
  2. Compare: Compares current version with available version for active channel (stable/beta)
  3. Notify: If update available, shows toast notification with action button
  4. Download: User clicks download, installer copied from network share to temp folder
  5. Verify: SHA256 checksum validated against manifest
  6. Install: User clicks install, app launches installer with UAC, optionally quits

Backend Methods (app_update.go)

Method Description
CheckForUpdates() Reads manifest from network share, compares versions
DownloadUpdate() Copies installer to temp folder, verifies checksum
InstallUpdate(quit) Launches installer with UAC elevation
GetUpdateStatus() Returns current update system state
loadUpdateManifest(path) Parses version.json from network share
compareSemanticVersions(v1, v2) Semantic version comparison
verifyChecksum(file, hash) SHA256 integrity verification
resolveUpdatePath(base, file) Handles UNC paths and file:// URLs

Deployment Workflow for IT Admins

  1. Build new version: wails build --upx
  2. Create installer: Run Inno Setup with installer/installer.iss
  3. Generate checksum: certutil -hashfile EMLy_Installer_1.5.0.exe SHA256
  4. Update manifest: Edit version.json with new version and checksum
  5. Deploy to share: Copy installer and manifest to \\server\emly-updates\
  6. Users notified: Apps auto-check within 5 seconds of startup (if enabled)

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

Dashboard Features

ZIP File Upload

The dashboard supports uploading .zip files created by EMLy's SubmitBugReport feature when the API upload fails. Accessible via the "Upload ZIP" button on the reports list page, it parses report.txt (name, email, description), system_info.txt (hostname, OS, HWID, IP), and imports all attached files (screenshots, mail files, localStorage, config) into the database as a new bug report.

API Endpoint: POST /api/reports/upload - Accepts multipart form data with a .zip file.

User Enable/Disable

Admins can temporarily disable user accounts without deleting them. Disabled users cannot log in and active sessions are invalidated. The user table has an enabled BOOLEAN column (default TRUE). Toggle is available in the Users management page. Restrictions: admins cannot disable themselves or other admin users.

Active Users / Presence Tracking

Real-time presence tracking using Server-Sent Events (SSE). Connected users are tracked in-memory with heartbeat updates every 15 seconds. The layout header shows avatar indicators for other active users with tooltips showing what they're viewing. The report detail page shows who else is currently viewing the same report.

Endpoints:

  • GET /api/presence - SSE stream for real-time presence updates
  • POST /api/presence/heartbeat - Client heartbeat with current page/report info

Client Store: $lib/stores/presence.svelte.ts - Svelte 5 reactive store managing SSE connection and heartbeats.


License & Credits

EMLy is developed by FOISX @ 3gIT.