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
- Architecture Overview
- Technology Stack
- Project Structure
- Backend (Go)
- Frontend (SvelteKit)
- State Management
- Internationalization (i18n)
- UI Components
- Key Features
- 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:
- Initializes the logger
- Parses command-line arguments for:
.emlor.msgfiles to open on startup--view-image=<path>for image viewer mode--view-pdf=<path>for PDF viewer mode
- Configures Wails with window options
- Sets up single-instance lock to prevent multiple main windows
- Binds the
Appstruct 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
FindWindowWto locate window by title - Uses
DwmGetWindowAttributefor 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:
-
Custom Titlebar: Windows-style titlebar with minimize/maximize/close buttons
- Draggable for window movement
- Double-click to maximize/restore
-
Sidebar Provider: Collapsible navigation sidebar
-
Footer Bar: Quick access icons for:
- Toggle sidebar
- Navigate to home
- Navigate to settings
- Open bug report dialog
- Reload application
-
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
-
Toast Notifications: Using svelte-sonner
-
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 fileMailViewer.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:
-
Language Settings
- Radio buttons for English/Italian
- Triggers full page reload on change
-
Export/Import Settings
- Export current settings to JSON file
- Import settings from JSON file
-
Preview Page Settings
- Supported image types (JPG, JPEG, PNG)
- Toggle built-in image viewer
- Toggle built-in PDF viewer
-
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 translationsit.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:
- Captures screenshot when dialog opens
- Collects user input (name, email, description)
- Includes current mail file if loaded
- Gathers system information
- Creates ZIP archive in temp folder
- 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 methodsgo/models.ts- TypeScript types for Go structsruntime/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.