<!DOCTYPE html><html lang="en" class=""> <!-- 'class' is controlled by JS for dark mode --><head><meta charset="utf-8" /><meta content="width=device-width, initial-scale=1.0" name="viewport" /><title>AR TEMPMAIL</title><!-- Tailwind CSS --><script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script><!-- Fonts --><link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&amp;display=swap" rel="stylesheet" /><link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" /><!-- Tailwind Config --><script>tailwind.config ={darkMode:"class",theme:{extend:{colors:{// Unified color palette from your designs"primary":"#D90429", // Main Red"background-light":"#F7F7F7", // App Background (Light)"card-light":"#FFFFFF", // Card/Surface (Light)"background-dark":"#111111", // App Background (Dark)"card-dark":"#1E1E1E", // Card/Surface (Dark)"text-light":"#2D3748", // Main Text (Light)"text-light-secondary":"#6B7280", // Secondary Text (Light)"text-dark":"#E2E8F0", // Main Text (Dark)"text-dark-secondary":"#94a3b8", // Secondary Text (Dark)"border-light":"#E2E8F0","border-dark":"#333333",},fontFamily:{"display":["Space Grotesk", "sans-serif"]},borderRadius:{"DEFAULT":"1rem","lg":"1.5rem","xl":"2rem","full":"9999px"},keyframes:{toastIn:{'0%':{transform:'translateY(100%)', opacity:'0'},'100%':{transform:'translateY(0)', opacity:'1'},},toastOut:{'0%':{transform:'translateY(0)', opacity:'1'},'100%':{transform:'translateY(100%)', opacity:'0'},}},animation:{toastIn:'toastIn 0.3s ease-out forwards',toastOut:'toastOut 0.3s ease-in forwards',}},},}</script><!-- Custom Styles --><style>.material-symbols-outlined{font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24}body{@apply bg-background-light dark:bg-background-dark font-display text-text-light dark:text-text-dark;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;}#email-detail-body a{color:#D90429;text-decoration:underline;}#email-detail-body p{margin-bottom:1rem;}.no-scrollbar::-webkit-scrollbar{display:none;}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none;}.spinner{border:2px solid rgba(255,255,255,0.3);border-radius:50%;border-top-color:#fff;width:1.25rem;height:1.25rem;animation:spin 1s ease-in-out infinite;}.spinner-dark{border-color:rgba(217, 4, 41, 0.3);border-top-color:#D90429;}@keyframes spin{to{transform:rotate(360deg);}}</style></head><body class="bg-background-light dark:bg-background-dark"><!-- =========== Splash Screen =========== --><div id="splash-screen" class="relative flex h-screen w-full flex-col items-center justify-center p-4 bg-card-light dark:bg-background-dark"><!-- Main Content Area --><div class="flex flex-col items-center justify-center text-center flex-grow"><!-- App Logo --><h1 class="text-text-light dark:text-text-dark tracking-tight text-5xl md:text-6xl font-bold leading-tight"><span class="text-primary">AR</span> TEMPMAIL</h1><!-- Tagline --><p class="text-text-light/80 dark:text-text-dark/80 text-base md:text-lg font-normal leading-normal pt-3">No Login, No Signup, Just Privacy.</p></div><!-- Loading Indicator --><div class="w-full max-w-xs pb-10"><div class="flex flex-col gap-3"><div class="rounded-full bg-text-light/10 dark:bg-text-dark/10"><div id="splash-loading-bar" class="h-1.5 rounded-full bg-primary transition-all duration-500" style="width:0%;"></div></div></div></div></div><!-- =========== Home Screen =========== --><div id="home-screen" class="hidden flex min-h-screen w-full flex-col p-4 sm:p-6"><header class="flex w-full items-center justify-betweenpb-6"><div class="w-10"></div> <!-- Spacer --><h1 class="text-2xl font-bold tracking-tight text-center"><span class="text-primary">AR</span> TEMPMAIL</h1><button id="dark-mode-toggle" class="flex h-10 w-10 items-center justify-center rounded-full bg-card-light dark:bg-card-dark shadow-sm text-text-light/80 dark:text-text-dark/80"><!-- Icon will be set by JS --></button></header><main class="flex flex-1 flex-col gap-6"><!-- Actions --><div class="grid grid-cols-2 gap-3"><button id="change-mail-btn" class="flex h-[46px] w-full items-center justify-center gap-2 rounded-lg bg-primary py-3 text-base font-bold text-white shadow-lg shadow-primary/30"><span class="material-symbols-outlined text-xl">autorenew</span><span>Change Mail</span></button><button id="delete-mail-btn" class="flex h-[46px] w-full items-center justify-center gap-2 rounded-lg border border-primary/50 bg-transparent py-3 text-base font-bold text-primary"><span class="material-symbols-outlined text-xl">delete</span><span>Delete Mail</span></button></div><!-- Email Address Card --><div class="rounded-lg bg-card-light p-4 dark:bg-card-dark shadow-sm"><p class="mb-2 text-sm text-text-light/70 dark:text-text-dark-secondary">Your temporary email address:</p><div class="flex items-center gap-2 rounded-md border border-border-light bg-background-light p-3 dark:border-border-dark dark:bg-background-dark"><span class="material-symbols-outlined text-base text-primary">alter-nate_email</span><p id="email-display" class="flex-1 truncate font-medium">loading...</p><button id="copy-btn" class="flex items-center gap-1.5 rounded-full bg-primary/10 px-3 py-1.5 text-sm font-medium text-primary"><span id="copy-icon" class="material-symbols-outlined text-base">content_copy</span><span id="copy-text">Copy</span></button></div></div><!-- Timer Card --><div class="flex items-center justify-betweengap-4 rounded-lg bg-card-light p-4 dark:bg-card-dark shadow-sm"><div class="flex flex-col"><p class="text-sm text-text-light/70 dark:text-text-dark-secondary">Email lifetime</p><p id="timer-display" class="text-xl font-bold text-primary">--:--</p></div><button id="extend-timer-btn" class="flex items-center justify-center gap-2 rounded-lg border border-primary/50 bg-transparent px-4 py-2 font-medium text-primary"><span class="material-symbols-outlined">timer</span>Extend</button></div><!-- Inbox Card --><div class="flex flex-1 flex-col overflow-hidden rounded-lg bg-card-light shadow-sm dark:bg-card-dark"><div class="flex items-center justify-betweenborder-b border-border-light p-4 dark:border-border-dark"><div class="flex items-center gap-2"><h2 class="text-lg font-bold">Inbox</h2><div id="inbox-count-badge" class="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-sm font-bold text-white">0</div></div><button id="refresh-inbox-btn" class="flex h-8 w-8 items-center justify-center rounded-full text-text-light/70 hover:bg-background-light dark:text-text-dark/70 dark:hover:bg-background-dark"><span class="material-symbols-outlined">refresh</span></button></div><!-- Inbox List --><div id="inbox-list-container" class="flex-1 space-y-1 overflow-y-auto p-2 no-scrollbar"><!-- JS will populate this --><!-- Empty State --><div id="inbox-empty-state" class="flex h-full flex-col items-center justify-center p-8 text-center"><span class="material-symbols-outlined text-5xl text-text-light/30 dark:text-text-dark/30">inbox</span><p class="mt-2 font-medium text-text-light/80 dark:text-text-dark/80">Your inbox is empty</p><p class="text-sm text-text-light/60 dark:text-text-dark-secondary">Waiting for incoming emails...</p></div></div></div></main></div><!-- =========== Email Detail Screen =========== --><div id="email-detail-screen" class="hidden relative flex h-full min-h-screen w-full flex-col bg-card-light dark:bg-card-dark"><header class="flex items-center justify-betweenp-4 sticky top-0 z-10 bg-card-light/80 dark:bg-card-dark/80 backdrop-blur-sm"><button id="email-detail-back" class="flex size-10 items-center justify-center rounded-full text-text-light dark:text-text-dark"><span class="material-symbols-outlined text-2xl">arrow_back</span></button><h1 class="text-lg font-bold">Email Details</h1><div class="w-10"></div> <!-- Spacer --></header><main class="flex-1 flex-col overflow-y-auto no-scrollbar"><div class="p-4"><div class="mb-6 flex flex-col gap-1 border-b border-border-light dark:border-border-dark pb-6"><h2 id="email-detail-subject" class="text-xl font-bold text-text-light dark:text-text-dark"></h2><div class="flex items-center gap-3 pt-2"><div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary"><span id="email-detail-sender-initial" class="text-xl font-bold"></span></div><div class="flex flex-col"><p id="email-detail-sender" class="font-bold text-text-light dark:text-text-dark"></p><p id="email-detail-timestamp" class="text-sm text-text-light-secondary dark:text-text-dark-secondary"></p></div></div></div><!-- Email Body --><div id="email-detail-body" class="flex-1 text-base leading-relaxed text-text-light-secondary dark:text-text-dark-secondary"><!-- JS will populate this --></div></div></main><footer class="sticky bottom-0 z-10 bg-card-light/80 dark:bg-card-dark/80 p-4 backdrop-blur-sm"><div class="flex w-full flex-col items-stretch gap-3"><button id="email-detail-delete" class="flex h-14 w-full cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-full bg-primary text-white text-base font-bold leading-normal tracking-wide"><span class="material-symbols-outlined">delete</span><span>Delete Message</span></button></div></footer></div><!-- Toast Notification --><div id="toast" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 hidden w-11/12 max-w-sm rounded-lg bg-primary p-3 text-white shadow-lg text-center"><span id="toast-message"></span></div><!-- JavaScript Logic --><script type="module">// API Base URL:https://api.mail.tmconst API_BASE_URL = "https://api.mail.tm";// --- State ---// Will store{accountId:"...", address:"...", token:"..."}let currentEmailData = -null;let inboxMessages = [];let timerInterval = -null;let timeLeft = 600;// 10 minutes (UI timer)let inboxCheckInterval = -null;let isTimerRunning = false;// --- DOM Elements ---const splashScreen = document.getElementById("splash-screen");const loadingBar = document.getElementById("splash-loading-bar");const homeScreen = document.getElementById("home-screen");const emailDetailScreen = document.getElementById("email-detail-screen");// Homeconst darkModeToggle = document.getElementById("dark-mode-toggle");const changeMailBtn = document.getElementById("change-mail-btn");const deleteMailBtn = document.getElementById("delete-mail-btn");const emailDisplay = document.getElementById("email-display");const copyBtn = document.getElementById("copy-btn");const copyIcon = document.getElementById("copy-icon");const copyText = document.getElementById("copy-text");const timerDisplay = document.getElementById("timer-display");const extendTimerBtn = document.getElementById("extend-timer-btn");const inboxCountBadge = document.getElementById("inbox-count-badge");const refreshInboxBtn = document.getElementById("refresh-inbox-btn");const inboxListContainer = document.getElementById("inbox-list-container");const inboxEmptyState = document.getElementById("inbox-empty-state");// Detailconst detailBackBtn = document.getElementById("email-detail-back");const detailSubject = document.getElementById("email-detail-subject");const detailSenderInitial = document.getElementById("email-detail-sender-initial");const detailSender = document.getElementById("email-detail-sender");const detailTimestamp = document.getElementById("email-detail-timestamp");const detailBody = document.getElementById("email-detail-body");const detailDeleteBtn = document.getElementById("email-detail-delete");// Toastconst toast = document.getElementById("toast");const toastMessage = document.getElementById("toast-message");// --- Functions ---/-** * Wrapper for fetch to include a timeout. */async function fetchWithTimeout(url, options ={}, timeout = 8000){const controller = new AbortController();const id = setTimeout(() => controller.abort(), timeout);try{const response = await fetch(url,{...options,signal:controller.signal});clearTimeout(id);return response;} catch (err){clearTimeout(id);if (err.name === 'AbortError'){throw new Error('Request timed out');}throw err;}}/-** * Initializes the application. */async function initApp(){// 1. Setup Dark Mode (default light)setupDarkMode();// 2. Simulate Splash loadingloadingBar.style.width = "30%";// 3. Get new emailawait new Promise(res => setTimeout(res, 500));// Short delayloadingBar.style.width = "60%";const success = await getNewEmail();if (!success){// Failed to get email, stay on splashloadingBar.style.width = "0%";// Reset barshowToast("Failed to connect. Retrying...", "error");setTimeout(initApp, 3000);// Retry after 3 secondsreturn;}// 4. Finish loading and show homeloadingBar.style.width = "100%";await new Promise(res => setTimeout(res, 500));showScreen('home-screen');// Timer starts when screen is shown (see showScreen)// 5. Start auto-checking inboxif (inboxCheckInterval) clearInterval(inboxCheckInterval);inboxCheckInterval = setInterval(checkInbox, 10000);// Check every 10 seconds}/-** * Shows a specific screen and hides others. * @param{'splash-screen' | 'home-screen' | 'email-detail-screen'} screenId*/function showScreen(screenId){splashScreen.classList.add('hidden');homeScreen.classList.add('hidden');emailDetailScreen.classList.add('hidden');document.getElementById(screenId).classList.remove('hidden');// Start timer only when home screen is visible and timer isn't already runningif (screenId === 'home-screen' && !isTimerRunning){startTimer();}}/-** * Generates a random string. */function randomString(length){let result = '';const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';for (let i = 0;i < length;i++){result += chars.charAt(Math.floor(Math.random() * chars.length));}return result;}/-** * Fetches a new random email address. * @returns{Promise<boolean>} True if successful, false otherwise. */async function getNewEmail(){try{// 1. Get an available domainconst domainResponse = await fetchWithTimeout(${API_BASE_URL}/domains);if (!domainResponse.ok) throw new Error('Failed to get domain');const domains = await domainResponse.json();const domain = domains['hydra:member'][0].domain;// Get first domain// 2. Create random address and passwordconst address = artempmail${randomString(6)}@${domain};const password = randomString(12);// 3. Create the accountconst createResponse = await fetchWithTimeout(${API_BASE_URL}/accounts,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({address, password})});if (!createResponse.ok) throw new Error('Failed to create account');const accountData = await createResponse.json();const accountId = accountData.id;// 4. Get the auth tokenconst tokenResponse = await fetchWithTimeout(${API_BASE_URL}/token,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({address, password})});if (!tokenResponse.ok) throw new Error('Failed to get token');const tokenData = await tokenResponse.json();const token = tokenData.token;// 5. Store data and update UIcurrentEmailData ={accountId, address, token};emailDisplay.textContent = currentEmailData.address;inboxMessages = [];updateInboxUI();resetTimer();showToast("New email address generated!");return true;} catch (err){console.error("Failed to get new email:", err);showToast(err.message || "Error generating email", "error");return false;}}/-** * Checks the inbox for new messages. */async function checkInbox(){if (!currentEmailData || !currentEmailData.token) return;try{const response = await fetchWithTimeout(${API_BASE_URL}/messages,{headers:{'Authorization':Bearer ${currentEmailData.token}}});if (!response.ok){// Handle token expiryif (response.status === 401){showToast("Session expired. Getting new email...", "error");getNewEmail();}throw new Error("Failed to check inbox");}const data = await response.json();inboxMessages = data['hydra:member'] || [];updateInboxUI();} catch (err){console.error("Failed to check inbox:", err);}}/-** * Updates the inbox list UI with current messages. */function updateInboxUI(){inboxCountBadge.textContent = inboxMessages.length;if (inboxMessages.length === 0){inboxEmptyState.classList.remove('hidden');inboxListContainer.innerHTML = '';// Clear old messagesinboxListContainer.appendChild(inboxEmptyState);// Add empty state backreturn;}inboxEmptyState.classList.add('hidden');inboxListContainer.innerHTML = '';// Clear list// Sort messages by date, newest firstinboxMessages.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));inboxMessages.forEach(msg =>{const msgElement = document.createElement('div');msgElement.className = "message-item flex flex-col gap-1 rounded-md p-3 hover:bg-background-light dark:hover:bg-background-dark cursor-pointer";msgElement.dataset.id = msg.id;const senderName = msg.from?.name || msg.from?.address || 'Unknown Sender';const previewText = msg.intro ? msg.intro + '...' :'Click to read message...';msgElement.innerHTML = <div class="flex items-center justify-between "><p class="font-bold ${senderName.toLowerCase().includes('social') ? 'text-primary' :''}">${escapeHTML(senderName)}</p><p class="text-xs text-text-light/60 dark:text-text-dark-secondary">${formatDate(msg.createdAt)}</p></div><p class="font-medium">${escapeHTML(msg.subject)}</p><p class="truncate text-sm text-text-light/70 dark:text-text-dark-secondary">${escapeHTML(previewText)}</p>;msgElement.addEventListener('click', () => openEmail(msg.id));inboxListContainer.appendChild(msgElement);});}/-** * Opens a specific email by its ID. * @param{string} messageId*/async function openEmail(messageId){if (!currentEmailData) return;showToast("Loading email...");try{const response = await fetchWithTimeout(${API_BASE_URL}/messages/${messageId},{headers:{'Authorization':Bearer ${currentEmailData.token}}});if (!response.ok) throw new Error("Failed to load email");const data = await response.json();const senderName = data.from?.name || data.from?.address || 'Unknown Sender';detailSubject.textContent = data.subject || "(No Subject)";detailSender.textContent = senderName;detailSenderInitial.textContent = senderName[0].toUpperCase();detailTimestamp.textContent = formatDate(data.createdAt, true);detailBody.innerHTML = data.html?.[0] || data.text || "(No content)";// Store message ID for deletiondetailDeleteBtn.dataset.id = messageId;showScreen('email-detail-screen');} catch (err){console.error("Failed to open email:", err);showToast("Error loading email", "error");}}/-** * Starts the 10-minute countdown timer. */function startTimer(){if (timerInterval) clearInterval(timerInterval);isTimerRunning = true;timerInterval = setInterval(() =>{timeLeft--;updateTimerDisplay();if (timeLeft <= 0){clearInterval(timerInterval);isTimerRunning = false;showToast("Timer expired! Extend or get new email.", "error");}}, 1000);updateTimerDisplay();}/-** * Resets the timer to 10 minutes. */function resetTimer(){timeLeft = 600;// 10 minutesif (isTimerRunning){startTimer();// Restart if already running}updateTimerDisplay();// Update display immediately}/-** * Adds 10 minutes to the timer. */async function extendTimer(){timeLeft = 600;// Reset to 10 minutesif (!isTimerRunning){startTimer();}updateTimerDisplay();showToast("Timer reset to 10 minutes!");}/-** * Updates the timer display in MM:SS format. */function updateTimerDisplay(){const minutes = Math.floor(timeLeft / 60).toString().padStart(2, '0');const seconds = (timeLeft % 60).toString().padStart(2, '0');timerDisplay.textContent = ${minutes}:${seconds};}/-** * Copies the current email address to the clipboard. */function copyToClipboard(){if (!currentEmailData || !currentEmailData.address) return;const tempInput = document.createElement("textarea");tempInput.value = currentEmailData.address;document.body.appendChild(tempInput);tempInput.select();try{document.-execCommand("copy");copyText.textContent = "Copied!";copyIcon.textContent = "check";setTimeout(() =>{copyText.textContent = "Copy";copyIcon.textContent = "content_copy";}, 2000);} catch (err){console.error("Failed to copy:", err);showToast("Failed to copy!", "error");}document.body.removeChild(tempInput);}/-** * Sets up the dark mode toggle, defaulting to light. */function setupDarkMode(){if (localStorage.theme === 'dark'){document.documentElement.classList.add('dark');darkModeToggle.innerHTML = <span class="material-symbols-outlined">light_mode</span>;} else{document.documentElement.classList.remove('dark');darkModeToggle.innerHTML = <span class="material-symbols-outlined">dark_mode</span>;localStorage.theme = 'light';}darkModeToggle.addEventListener('click', () =>{if (document.documentElement.classList.toggle('dark')){localStorage.theme = 'dark';darkModeToggle.innerHTML = <span class="material-symbols-outlined">light_mode</span>;} else{localStorage.theme = 'light';darkModeToggle.innerHTML = <span class="material-symbols-outlined">dark_mode</span>;}});}/-** * Shows a toast notification. * @param{string} message* @param{'info' | 'error'} type*/function showToast(message, type = 'info'){toastMessage.textContent = message;toast.classList.toggle('bg-primary', type === 'info');toast.classList.toggle('bg-red-600', type === 'error');toast.classList.remove('hidden');toast.style.animation = 'toastIn 0.3s ease-out forwards';setTimeout(() =>{toast.style.animation = 'toastOut 0.3s ease-in forwards';setTimeout(() => toast.classList.add('hidden'), 300);}, 3000);}/-** * Formats a date string or timestamp. * @param{string | number} dateValue* @param{boolean} [full=false]*/function formatDate(dateValue, full = false){try{const date = new Date(dateValue);const now = new Date();const diffMs = now - date;const diffMins = Math.round(diffMs / 60000);if (full){return date.toLocaleString();}if (diffMins < 1) return "Just now";if (diffMins < 60) return ${diffMins} min ago;if (diffMins < 1440) return ${Math.floor(diffMins / 60)}h ago;return date.toLocaleDateString();} catch (e){return String(dateValue);}}/-** * Escapes HTML to prevent XSS. * @param{string} str*/function escapeHTML(str){if (!str) return "";return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');}/-** * Adds a loading spinner to a button. * @param{HTMLElement} button* @param{boolean} isDarkSpinner*/function showSpinner(button, isDarkSpinner = false){button.disabled = true;const originalContent = button.innerHTML;button.innerHTML = <div class="spinner ${isDarkSpinner ? 'spinner-dark' :''}"></div>;// Return a function to restore the buttonreturn () =>{button.innerHTML = originalContent;button.disabled = false;};}// --- Event Listeners ---window.addEventListener('load', initApp);// HomechangeMailBtn.addEventListener('click', async () =>{const restore = showSpinner(changeMailBtn);await getNewEmail();restore();});deleteMailBtn.addEventListener('click', async () =>{if (!currentEmailData) return;const restore = showSpinner(deleteMailBtn, true);showToast("Deleting this email...");try{await fetchWithTimeout(${API_BASE_URL}/accounts/${currentEmailData.accountId},{method:'delete ',headers:{'Authorization':Bearer ${currentEmailData.token}}});} catch (err){console.error("Failed to delete account, but getting new one anyway.", err);}await getNewEmail();restore();});copyBtn.addEventListener('click', copyToClipboard);extendTimerBtn.addEventListener('click', extendTimer);refreshInboxBtn.addEventListener('click', () =>{showToast("Refreshing inbox...");checkInbox();});// DetaildetailBackBtn.addEventListener('click', () => showScreen('home-screen'));detailDeleteBtn.addEventListener('click', async () =>{const messageId = detailDeleteBtn.dataset.id;if (!messageId || !currentEmailData) return;showToast("Deleting message...");try{// This API uses PATCH to set seen=true, we'll just delete from UI// The API doesn't support delete/messages/{id} in the same way// So we will just simulate it locally.showToast("Message deleted!");// Remove from local cacheinboxMessages = inboxMessages.filter(m => m.id != messageId);updateInboxUI();showScreen('home-screen');} catch (err){console.error("Failed to delete:", err);showToast("Error deleting message.", "error");}});</script></body></html>