<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>EML Reader & Viewer</title>
    <!-- Tailwind CSS (Play CDN) -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Alpine.js (reaktywność) - ładowany z defer -->
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="bg-gray-50 h-screen flex flex-col font-sans overflow-hidden" x-data="emlApp()">

    <!-- TOPBAR: Adres URL i przycisk pobierania -->
    <header class="bg-white border-b border-gray-200 px-6 py-4 flex flex-col sm:flex-row items-center justify-between gap-4 shrink-0">
        <div class="flex items-center gap-3">
            <span class="text-2xl">✉️</span>
            <h1 class="text-xl font-bold text-gray-800">EML Viewer</h1>
        </div>
        <div class="flex w-full sm:w-auto max-w-xl gap-2 grow justify-end">
            <input 
                type="url" 
                x-model="targetUrl" 
                placeholder="Wpisz URL do listy plików EML (np. http://localhost:8000/)" 
                class="border border-gray-300 rounded px-3 py-1.5 text-sm w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button 
                @click="fetchEmlList()" 
                :disabled="isLoadingList"
                class="bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm px-4 py-1.5 rounded transition disabled:opacity-50 whitespace-nowrap"
            >
                <span x-show="!isLoadingList">Pobierz listę</span>
                <span x-show="isLoadingList">Ładowanie...</span>
            </button>
        </div>
    </header>

    <!-- GŁÓWNY PANEL: Układ dwukolumnowy -->
    <main class="flex flex-1 overflow-hidden">
        
        <!-- LEWA KOLUMNA: Lista plików -->
        <section class="w-80 border-r border-gray-200 bg-white flex flex-col shrink-0">
            <div class="p-4 border-b border-gray-100 bg-gray-50">
                <span class="text-xs font-bold uppercase tracking-wider text-gray-500">Wykryte pliki EML</span>
                <span class="bg-blue-100 text-blue-800 text-xs px-2 py-0.5 rounded-full ml-2 font-semibold" x-text="emlFiles.length">0</span>
            </div>
            
            <div class="flex-1 overflow-y-auto divide-y divide-gray-100">
                <!-- Loader listy -->
                <template x-if="isLoadingList">
                    <div class="p-6 text-center text-sm text-gray-500">Skanowanie strony w poszukiwaniu plików...</div>
                </template>

                <!-- Brak wyników -->
                <template x-if="!isLoadingList && emlFiles.length === 0">
                    <div class="p-6 text-center text-sm text-gray-400">Brak plików EML. Wpisz URL i kliknij pobieranie.</div>
                </template>

                <!-- Elementy listy -->
                <template x-for="fileUrl in emlFiles" :key="fileUrl">
                    <button 
                        @click="selectEml(fileUrl)"
                        class="w-full text-left p-4 hover:bg-blue-50/50 transition flex flex-col gap-1 focus:outline-none focus:bg-blue-50"
                        :class="selectedEmlUrl === fileUrl ? 'bg-blue-50 border-l-4 border-blue-600 pl-3' : ''"
                    >
                        <span class="text-sm font-medium text-gray-700 truncate w-full" x-text="getFileName(fileUrl)"></span>
                        <span class="text-xs text-gray-400 truncate w-full" x-text="fileUrl"></span>
                    </button>
                </template>
            </div>
        </section>

        <!-- PRAWA KOLUMNA: Podgląd maila -->
        <section class="flex-1 bg-white flex flex-col overflow-hidden">
            
            <!-- Loader szczegółów maila -->
            <template x-if="isLoadingMail">
                <div class="flex-1 flex flex-col items-center justify-center text-gray-500">
                    <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 mb-4"></div>
                    <p>Pobieranie i dekodowanie wiadomości...</p>
                </div>
            </template>

            <!-- Stan początkowy (gdy nic nie wybrano) -->
            <template x-if="!isLoadingMail && !selectedEmlContent">
                <div class="flex-1 flex flex-col items-center justify-center text-gray-400 p-8 text-center">
                    <span class="text-5xl mb-4">📂</span>
                    <p class="text-lg font-medium">Nie wybrano wiadomości</p>
                    <p class="text-sm max-w-xs mt-1">Wybierz plik EML z listy po lewej stronie, aby wyświetlić jego zawartość.</p>
                </div>
            </template>

            <!-- Widok wiadomości -->
            <template x-if="!isLoadingMail && selectedEmlContent">
                <div class="flex flex-1 flex-col overflow-hidden">
                    
                    <!-- Nagłówki wiadomości -->
                    <div class="p-6 border-b border-gray-200 bg-gray-50/50 shrink-0">
                        <h2 class="text-xl font-bold text-gray-900 mb-3" x-text="selectedEmlContent.subject || '(Bez tematu)'"></h2>
                        <div class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-sm">
                            <span class="text-gray-400 font-medium">Od:</span>
                            <span class="text-gray-800" x-text="formatAddress(selectedEmlContent.from)"></span>
                            
                            <span class="text-gray-400 font-medium">Do:</span>
                            <span class="text-gray-800" x-text="formatAddress(selectedEmlContent.to)"></span>
                            
                            <span class="text-gray-400 font-medium">Data:</span>
                            <span class="text-gray-800" x-text="selectedEmlContent.date ? new Date(selectedEmlContent.date).toLocaleString() : 'Brak daty'"></span>
                        </div>

                        <!-- Załączniki (jeśli istnieją) -->
                        <template x-if="selectedEmlContent.attachments && selectedEmlContent.attachments.length > 0">
                            <div class="mt-4 pt-4 border-t border-gray-200">
                                <span class="text-xs font-bold text-gray-500 uppercase tracking-wider block mb-2">Załączniki</span>
                                <div class="flex flex-wrap gap-2">
                                    <template x-for="att in selectedEmlContent.attachments" :key="att.filename">
                                        <button 
                                            @click="downloadAttachment(att)"
                                            class="bg-white border border-gray-200 hover:border-blue-400 rounded px-3 py-1.5 text-xs font-medium text-gray-700 flex items-center gap-1.5 shadow-sm transition"
                                        >
                                            📎 <span x-text="att.filename || 'Załącznik bez nazwy'"></span>
                                        </button>
                                    </template>
                                </div>
                            </div>
                        </template>
                    </div>

                    <!-- Treść wiadomości renderowana w izolowanym iframe -->
                    <div class="flex-1 bg-white p-4 overflow-hidden flex">
                        <iframe 
                            class="w-full h-full border-0"
                            :srcdoc="getEmailBody(selectedEmlContent)"
                            sandbox="allow-popups allow-popups-to-escape-sandbox"
                        ></iframe>
                    </div>
                </div>
            </template>
        </section>
    </main>

    <!-- Powiadomienie o błędzie -->
    <template x-if="errorMessage">
        <div class="fixed bottom-4 right-4 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-lg max-w-md z-50 flex items-start gap-2">
            <div class="flex-1">
                <p class="font-bold text-sm">Wystąpił błąd</p>
                <p class="text-xs mt-1" x-text="errorMessage"></p>
            </div>
            <button @click="errorMessage = null" class="text-red-500 hover:text-red-700 font-bold text-lg leading-none">&times;</button>
        </div>
    </template>

    <!-- Standardowy skrypt gwarantujący poprawne wpięcie w cykl życia Alpine.js -->
    <script>
        document.addEventListener('alpine:init', () => {
            Alpine.data('emlApp', () => ({
                targetUrl: 'http://localhost:8000/',
                emlFiles: [],
                selectedEmlUrl: null,
                selectedEmlContent: null,
                
                isLoadingList: false,
                isLoadingMail: false,
                errorMessage: null,

                async fetchEmlList() {
                    this.isLoadingList = true;
                    this.errorMessage = null;
                    this.emlFiles = [];
                    
                    try {
                        const response = await fetch(this.targetUrl);
                        if (!response.ok) {
                            throw new Error(`Serwer odpowiedział kodem ${response.status}: ${response.statusText}`);
                        }
                        const htmlText = await response.text();

                        const parser = new DOMParser();
                        const doc = parser.parseFromString(htmlText, 'text/html');
                        const links = Array.from(doc.querySelectorAll('a'));

                        this.emlFiles = links
                            .map(a => a.getAttribute('href'))
                            .filter(href => href && href.toLowerCase().endsWith('.eml'))
                            .map(href => {
                                return new URL(href, this.targetUrl).href;
                            });

                        if (this.emlFiles.length === 0) {
                            this.errorMessage = "Nie znaleziono żadnych plików .eml na tej stronie. Upewnij się, że serwer zwraca poprawną listę plików w tagach <a href='...'>";
                        }
                    } catch (err) {
                        this.errorMessage = `Błąd podczas pobierania listy: ${err.message}. Zweryfikuj CORS oraz czy URL jest poprawny.`;
                        console.error(err);
                    } finally {
                        this.isLoadingList = false;
                    }
                },

                async selectEml(url) {
                    this.selectedEmlUrl = url;
                    this.isLoadingMail = true;
                    this.selectedEmlContent = null;
                    this.errorMessage = null;

                    try {
                        const response = await fetch(url);
                        if (!response.ok) {
                            throw new Error(`Błąd pobierania pliku: ${response.status}`);
                        }
                        const arrayBuffer = await response.arrayBuffer();

                        // Dynamiczny, asynchroniczny import modułu PostalMime w locie
                        const { default: PostalMime } = await import('https://cdn.jsdelivr.net/npm/postal-mime@2.2.0/+esm');
                        const parser = new PostalMime();
                        const parsedEmail = await parser.parse(arrayBuffer);

                        this.selectedEmlContent = parsedEmail;
                    } catch (err) {
                        this.errorMessage = `Nie udało się załadować wiadomości: ${err.message}`;
                        console.error(err);
                    } finally {
                        this.isLoadingMail = false;
                    }
                },

                getFileName(url) {
                    try {
                        const decoded = decodeURIComponent(url);
                        return decoded.substring(decoded.lastIndexOf('/') + 1);
                    } catch {
                        return url;
                    }
                },

                formatAddress(addressObj) {
                    if (!addressObj) return '(Brak danych)';
                    const list = Array.isArray(addressObj) ? addressObj : [addressObj];
                    return list.map(addr => {
                        if (addr.name) {
                            return `${addr.name} <${addr.address}>`;
                        }
                        return addr.address;
                    }).join(', ');
                },

                getEmailBody(email) {
                    if (!email) return '';
                    if (email.html) {
                        return `<meta http-equiv="Content-Security-Policy" content="script-src 'none';">` + email.html;
                    }
                    const escapedText = email.text
                        .replace(/&/g, "&amp;")
                        .replace(/</g, "&lt;")
                        .replace(/>/g, "&gt;");
                    return `<pre style="font-family: sans-serif; white-space: pre-wrap; word-break: break-all;">${escapedText}</pre>`;
                },

                downloadAttachment(attachment) {
                    try {
                        const blob = new Blob([attachment.content], { type: attachment.mimeType });
                        const blobUrl = URL.createObjectURL(blob);
                        
                        const link = document.createElement('a');
                        link.href = blobUrl;
                        link.download = attachment.filename || 'download';
                        document.body.appendChild(link);
                        link.click();
                        
                        document.body.removeChild(link);
                        URL.revokeObjectURL(blobUrl);
                    } catch (err) {
                        this.errorMessage = `Nie udało się pobrać załącznika: ${err.message}`;
                    }
                }
            }));
        });
    </script>
</body>
</html>
Comments
Login to add comments.