Bläddra i källkod

feat: add Inbucket mailbox support and enhance password management in side panel

Jimmy 4 månader sedan
förälder
incheckning
8b0ffa330f
9 ändrade filer med 420 tillägg och 17 borttagningar
  1. 0 0
      .vscode/settings.json
  2. 73 11
      background.js
  3. 258 0
      content/inbucket-mail.js
  4. 13 5
      content/mail-163.js
  5. 2 1
      content/utils.js
  6. 8 0
      manifest.json
  7. 5 0
      sidepanel/sidepanel.css
  8. 12 0
      sidepanel/sidepanel.html
  9. 49 0
      sidepanel/sidepanel.js

+ 0 - 0
.vscode/settings.json


+ 73 - 11
background.js

@@ -8,6 +8,8 @@ const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
 const HUMAN_STEP_DELAY_MIN = 700;
 const HUMAN_STEP_DELAY_MAX = 2200;
 
+initializeSessionStorageAccess();
+
 // ============================================================
 // State Management (chrome.storage.session)
 // ============================================================
@@ -28,7 +30,9 @@ const DEFAULT_STATE = {
   tabRegistry: {},
   logs: [],
   vpsUrl: '',
+  customPassword: '',
   mailProvider: '163', // 'qq' or '163'
+  inbucketMailbox: '',
 };
 
 async function getState() {
@@ -36,6 +40,19 @@ async function getState() {
   return { ...DEFAULT_STATE, ...state };
 }
 
+async function initializeSessionStorageAccess() {
+  try {
+    if (chrome.storage?.session?.setAccessLevel) {
+      await chrome.storage.session.setAccessLevel({
+        accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS',
+      });
+      console.log(LOG_PREFIX, 'Enabled storage.session for content scripts');
+    }
+  } catch (err) {
+    console.warn(LOG_PREFIX, 'Failed to enable storage.session for content scripts:', err?.message || err);
+  }
+}
+
 async function setState(updates) {
   console.log(LOG_PREFIX, 'storage.set:', JSON.stringify(updates).slice(0, 200));
   await chrome.storage.session.set(updates);
@@ -53,18 +70,35 @@ async function setEmailState(email) {
   broadcastDataUpdate({ email });
 }
 
+async function setPasswordState(password) {
+  await setState({ password });
+  broadcastDataUpdate({ password });
+}
+
 async function resetState() {
   console.log(LOG_PREFIX, 'Resetting all state');
   // Preserve settings and persistent data across resets
-  const prev = await chrome.storage.session.get(['seenCodes', 'accounts', 'tabRegistry', 'vpsUrl', 'mailProvider']);
+  const prev = await chrome.storage.session.get([
+    'seenCodes',
+    'seenInbucketMailIds',
+    'accounts',
+    'tabRegistry',
+    'vpsUrl',
+    'customPassword',
+    'mailProvider',
+    'inbucketMailbox',
+  ]);
   await chrome.storage.session.clear();
   await chrome.storage.session.set({
     ...DEFAULT_STATE,
     seenCodes: prev.seenCodes || [],
+    seenInbucketMailIds: prev.seenInbucketMailIds || [],
     accounts: prev.accounts || [],
     tabRegistry: prev.tabRegistry || {},
     vpsUrl: prev.vpsUrl || '',
+    customPassword: prev.customPassword || '',
     mailProvider: prev.mailProvider || '163',
+    inbucketMailbox: prev.inbucketMailbox || '',
   });
 }
 
@@ -481,7 +515,9 @@ async function handleMessage(message, sender) {
     case 'SAVE_SETTING': {
       const updates = {};
       if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl;
+      if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword;
       if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider;
+      if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox;
       await setState(updates);
       return { ok: true };
     }
@@ -725,6 +761,7 @@ async function autoRunLoop(totalRuns) {
     const keepSettings = {
       vpsUrl: prevState.vpsUrl,
       mailProvider: prevState.mailProvider,
+      inbucketMailbox: prevState.inbucketMailbox,
       autoRunning: true,
     };
     await resetState();
@@ -881,16 +918,17 @@ async function executeStep3(state) {
     throw new Error('No email address. Paste email in Side Panel first.');
   }
 
-  // Generate a unique password for this account
-  const password = generatePassword();
-  await setState({ password });
+  const password = state.customPassword || generatePassword();
+  await setPasswordState(password);
 
   // Save account record
   const accounts = state.accounts || [];
   accounts.push({ email: state.email, password, createdAt: new Date().toISOString() });
   await setState({ accounts });
 
-  await addLog(`Step 3: Filling email ${state.email}, password generated (${password.length} chars)`);
+  await addLog(
+    `Step 3: Filling email ${state.email}, password ${state.customPassword ? 'customized' : 'generated'} (${password.length} chars)`
+  );
   await sendToContentScript('signup-page', {
     type: 'EXECUTE_STEP',
     step: 3,
@@ -908,18 +946,35 @@ function getMailConfig(state) {
   if (provider === '163') {
     return { source: 'mail-163', url: 'https://mail.163.com/js6/main.jsp?df=mail163_letter#module=mbox.ListModule%7C%7B%22fid%22%3A1%2C%22order%22%3A%22date%22%2C%22desc%22%3Atrue%7D', label: '163 Mail' };
   }
+  if (provider === 'inbucket') {
+    const mailbox = (state.inbucketMailbox || '').trim();
+    if (!mailbox) {
+      return { error: 'Inbucket mailbox name is empty.' };
+    }
+    return {
+      source: 'inbucket-mail',
+      url: `https://inbucket.j2to.de/m/${encodeURIComponent(mailbox)}/`,
+      label: `Inbucket Mailbox (${mailbox})`,
+      navigateOnReuse: true,
+    };
+  }
   return { source: 'qq-mail', url: 'https://wx.mail.qq.com/', label: 'QQ Mail' };
 }
 
 async function executeStep4(state) {
   const mail = getMailConfig(state);
+  if (mail.error) throw new Error(mail.error);
   await addLog(`Step 4: Opening ${mail.label}...`);
 
   // For mail tabs, only create if not alive — don't navigate (preserves login session)
   const alive = await isTabAlive(mail.source);
   if (alive) {
-    const tabId = await getTabId(mail.source);
-    await chrome.tabs.update(tabId, { active: true });
+    if (mail.navigateOnReuse) {
+      await reuseOrCreateTab(mail.source, mail.url);
+    } else {
+      const tabId = await getTabId(mail.source);
+      await chrome.tabs.update(tabId, { active: true });
+    }
   } else {
     await reuseOrCreateTab(mail.source, mail.url);
   }
@@ -930,8 +985,9 @@ async function executeStep4(state) {
     source: 'background',
     payload: {
       filterAfterTimestamp: state.flowStartTime || 0,
-      senderFilters: ['openai', 'noreply', 'verify', 'auth'],
+      senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'],
       subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm'],
+      targetEmail: state.email,
       maxAttempts: 20,
       intervalMs: 3000,
     },
@@ -1010,12 +1066,17 @@ async function executeStep6(state) {
 
 async function executeStep7(state) {
   const mail = getMailConfig(state);
+  if (mail.error) throw new Error(mail.error);
   await addLog(`Step 7: Opening ${mail.label}...`);
 
   const alive = await isTabAlive(mail.source);
   if (alive) {
-    const tabId = await getTabId(mail.source);
-    await chrome.tabs.update(tabId, { active: true });
+    if (mail.navigateOnReuse) {
+      await reuseOrCreateTab(mail.source, mail.url);
+    } else {
+      const tabId = await getTabId(mail.source);
+      await chrome.tabs.update(tabId, { active: true });
+    }
   } else {
     await reuseOrCreateTab(mail.source, mail.url);
   }
@@ -1026,8 +1087,9 @@ async function executeStep7(state) {
     source: 'background',
     payload: {
       filterAfterTimestamp: state.lastEmailTimestamp || state.flowStartTime || 0,
-      senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt'],
+      senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
       subjectFilters: ['verify', 'verification', 'code', '验证', 'confirm', 'login'],
+      targetEmail: state.email,
       maxAttempts: 20,
       intervalMs: 3000,
     },

+ 258 - 0
content/inbucket-mail.js

@@ -0,0 +1,258 @@
+// content/inbucket-mail.js — Content script for Inbucket polling (steps 4, 7)
+// Injected on: inbucket.j2to.de
+//
+// Supported page:
+// - /m/<mailbox>/
+
+const INBUCKET_PREFIX = '[MultiPage:inbucket-mail]';
+const isTopFrame = window === window.top;
+const SEEN_MAIL_IDS_KEY = 'seenInbucketMailIds';
+
+console.log(INBUCKET_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
+
+if (!isTopFrame) {
+  console.log(INBUCKET_PREFIX, 'Skipping child frame');
+} else {
+
+let seenMailIds = new Set();
+
+async function loadSeenMailIds() {
+  try {
+    const data = await chrome.storage.session.get(SEEN_MAIL_IDS_KEY);
+    if (Array.isArray(data[SEEN_MAIL_IDS_KEY])) {
+      seenMailIds = new Set(data[SEEN_MAIL_IDS_KEY]);
+      console.log(INBUCKET_PREFIX, `Loaded ${seenMailIds.size} previously seen mail ids`);
+    }
+  } catch (err) {
+    console.warn(INBUCKET_PREFIX, 'Session storage unavailable, using in-memory seen mail ids:', err?.message || err);
+  }
+}
+
+async function persistSeenMailIds() {
+  try {
+    await chrome.storage.session.set({ [SEEN_MAIL_IDS_KEY]: [...seenMailIds] });
+  } catch (err) {
+    console.warn(INBUCKET_PREFIX, 'Could not persist seen mail ids, continuing in-memory only:', err?.message || err);
+  }
+}
+
+loadSeenMailIds();
+
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message.type === 'POLL_EMAIL') {
+    resetStopState();
+    handlePollEmail(message.step, message.payload).then(result => {
+      sendResponse(result);
+    }).catch(err => {
+      if (isStopError(err)) {
+        log(`Step ${message.step}: Stopped by user.`, 'warn');
+        sendResponse({ stopped: true, error: err.message });
+        return;
+      }
+      reportError(message.step, err.message);
+      sendResponse({ error: err.message });
+    });
+    return true;
+  }
+});
+
+function normalizeText(value) {
+  return (value || '').replace(/\s+/g, ' ').trim().toLowerCase();
+}
+
+function extractVerificationCode(text) {
+  const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/);
+  if (matchCn) return matchCn[1];
+
+  const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
+  if (matchEn) return matchEn[1] || matchEn[2];
+
+  const match6 = text.match(/\b(\d{6})\b/);
+  if (match6) return match6[1];
+
+  return null;
+}
+
+function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail) {
+  const sender = normalizeText(mail.sender);
+  const subject = normalizeText(mail.subject);
+  const mailbox = normalizeText(mail.mailbox);
+  const combined = normalizeText(mail.combinedText);
+  const targetLocal = normalizeText((targetEmail || '').split('@')[0]);
+
+  const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
+  const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
+  const mailboxMatch = Boolean(targetLocal) && mailbox.includes(targetLocal);
+  const forwardedDuck = /duckduckgo|forward(?:ed)?\s*by/i.test(mail.combinedText);
+  const code = extractVerificationCode(mail.combinedText);
+  const keywordMatch = /openai|chatgpt|verify|verification|confirm|login|验证码|代码/.test(combined);
+
+  if (mailboxMatch) return { matched: true, mailboxMatch, code };
+  if (senderMatch || subjectMatch) return { matched: true, mailboxMatch: false, code };
+  if (code && (forwardedDuck || keywordMatch)) return { matched: true, mailboxMatch: false, code };
+
+  return { matched: false, mailboxMatch: false, code };
+}
+
+function findMailboxEntries() {
+  return document.querySelectorAll('.message-list-entry');
+}
+
+function getMailboxEntryId(entry, index = 0) {
+  const explicitId = entry.getAttribute('data-id') || entry.dataset?.id || '';
+  if (explicitId) return explicitId;
+
+  const subject = entry.querySelector('.subject')?.textContent?.trim() || '';
+  const sender = entry.querySelector('.from')?.textContent?.trim() || '';
+  const dateText = entry.querySelector('.date')?.textContent?.trim() || '';
+
+  return `mailbox:${index}:${normalizeText(subject)}|${normalizeText(sender)}|${normalizeText(dateText)}`;
+}
+
+function parseMailboxEntry(entry, index = 0) {
+  const subject = entry.querySelector('.subject')?.textContent?.trim() || '';
+  const sender = entry.querySelector('.from')?.textContent?.trim() || '';
+  const dateText = entry.querySelector('.date')?.textContent?.trim() || '';
+  const combinedText = [subject, sender, dateText].filter(Boolean).join(' ');
+
+  return {
+    entry,
+    dateText,
+    sender,
+    mailbox: '',
+    subject,
+    unread: entry.classList.contains('unseen'),
+    combinedText,
+    mailId: getMailboxEntryId(entry, index),
+  };
+}
+
+function getCurrentMailboxIds() {
+  const ids = new Set();
+  Array.from(findMailboxEntries()).forEach((entry, index) => {
+    ids.add(getMailboxEntryId(entry, index));
+  });
+  return ids;
+}
+
+async function refreshMailbox() {
+  const refreshButton = document.querySelector('button[alt="Refresh Mailbox"]');
+  if (!refreshButton) return;
+
+  simulateClick(refreshButton);
+  await sleep(800);
+}
+
+async function openMailboxEntry(entry) {
+  simulateClick(entry);
+
+  for (let i = 0; i < 20; i++) {
+    if (entry.classList.contains('selected') || document.querySelector('.message-header, .message-body, .button-bar')) {
+      return;
+    }
+    await sleep(150);
+  }
+}
+
+async function deleteCurrentMailboxMessage(step) {
+  try {
+    const deleteButton = await waitForElement('.button-bar button.danger', 5000);
+    simulateClick(deleteButton);
+    log(`Step ${step}: Deleted mailbox message`, 'ok');
+    await sleep(1200);
+  } catch (err) {
+    log(`Step ${step}: Failed to delete mailbox message: ${err.message}`, 'warn');
+  }
+}
+
+async function handleMailboxPollEmail(step, payload) {
+  const {
+    senderFilters = [],
+    subjectFilters = [],
+    maxAttempts = 20,
+    intervalMs = 3000,
+  } = payload || {};
+
+  log(`Step ${step}: Starting email poll on Inbucket mailbox page (max ${maxAttempts} attempts)`);
+
+  try {
+    await waitForElement('.message-list, .message-list-entry', 15000);
+    log(`Step ${step}: Mailbox page loaded`);
+  } catch {
+    throw new Error('Inbucket mailbox page did not load. Make sure /m/<mailbox>/ is open.');
+  }
+
+  const existingMailIds = getCurrentMailboxIds();
+  log(`Step ${step}: Snapshotted ${existingMailIds.size} existing mailbox messages`);
+
+  const FALLBACK_AFTER = 3;
+
+  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+    log(`Polling Inbucket mailbox... attempt ${attempt}/${maxAttempts}`);
+
+    if (attempt > 1) {
+      await refreshMailbox();
+    }
+
+    const entries = Array.from(findMailboxEntries()).map(parseMailboxEntry);
+    const useFallback = attempt > FALLBACK_AFTER;
+    const candidates = [];
+
+    for (const mail of entries) {
+      if (!mail.unread) continue;
+      if (seenMailIds.has(mail.mailId)) continue;
+      if (!useFallback && existingMailIds.has(mail.mailId)) continue;
+
+      const match = rowMatchesFilters(mail, senderFilters, subjectFilters, '');
+      if (!match.matched) continue;
+
+      candidates.push({ ...mail, code: match.code });
+    }
+
+    for (const mail of candidates) {
+      const code = mail.code || extractVerificationCode(mail.combinedText);
+      if (!code) continue;
+
+      await openMailboxEntry(mail.entry);
+      await deleteCurrentMailboxMessage(step);
+
+      seenMailIds.add(mail.mailId);
+      await persistSeenMailIds();
+
+      const source = existingMailIds.has(mail.mailId) ? 'fallback' : 'new';
+      log(
+        `Step ${step}: Code found: ${code} (${source}, sender: ${mail.sender || 'unknown'}, subject: ${(mail.subject || '').slice(0, 60)})`,
+        'ok'
+      );
+
+      return {
+        ok: true,
+        code,
+        emailTimestamp: Date.now(),
+        mailId: mail.mailId,
+      };
+    }
+
+    if (attempt === FALLBACK_AFTER + 1) {
+      log(`Step ${step}: No new mailbox messages yet, falling back to older matching messages`, 'warn');
+    }
+
+    if (attempt < maxAttempts) {
+      await sleep(intervalMs);
+    }
+  }
+
+  throw new Error(
+    `No matching verification email found in Inbucket mailbox after ${(maxAttempts * intervalMs / 1000).toFixed(0)}s. ` +
+    'Check the mailbox page manually.'
+  );
+}
+
+async function handlePollEmail(step, payload) {
+  if (!location.pathname.startsWith('/m/')) {
+    throw new Error('Inbucket now only supports mailbox pages like /m/<mailbox>/.');
+  }
+  return handleMailboxPollEmail(step, payload);
+}
+
+} // end of isTopFrame else block

+ 13 - 5
content/mail-163.js

@@ -20,19 +20,27 @@ if (!isTopFrame) {
 // Track codes we've already seen — persisted in chrome.storage.session to survive script re-injection
 let seenCodes = new Set();
 
-// Load previously seen codes on startup
-(async () => {
+async function loadSeenCodes() {
   try {
     const data = await chrome.storage.session.get('seenCodes');
     if (data.seenCodes && Array.isArray(data.seenCodes)) {
       seenCodes = new Set(data.seenCodes);
       console.log(MAIL163_PREFIX, `Loaded ${seenCodes.size} previously seen codes`);
     }
-  } catch {}
-})();
+  } catch (err) {
+    console.warn(MAIL163_PREFIX, 'Session storage unavailable, using in-memory seen codes:', err?.message || err);
+  }
+}
+
+// Load previously seen codes on startup
+loadSeenCodes();
 
 async function persistSeenCodes() {
-  await chrome.storage.session.set({ seenCodes: [...seenCodes] });
+  try {
+    await chrome.storage.session.set({ seenCodes: [...seenCodes] });
+  } catch (err) {
+    console.warn(MAIL163_PREFIX, 'Could not persist seen codes, continuing in-memory only:', err?.message || err);
+  }
 }
 
 // ============================================================

+ 2 - 1
content/utils.js

@@ -5,6 +5,7 @@ const SCRIPT_SOURCE = (() => {
   if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
   if (url.includes('mail.qq.com')) return 'qq-mail';
   if (url.includes('mail.163.com')) return 'mail-163';
+  if (url.includes('inbucket.j2to.de')) return 'inbucket-mail';
   if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
   if (url.includes('chatgpt.com')) return 'chatgpt';
   // VPS panel — detected dynamically since URL is configurable
@@ -328,7 +329,7 @@ async function humanPause(min = 250, max = 850) {
 
 // Auto-report ready on load
 // Skip ready signal from child iframes of mail pages to avoid overwriting the top frame's registration
-const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163') && window !== window.top;
+const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163' || SCRIPT_SOURCE === 'inbucket-mail') && window !== window.top;
 if (!_isMailChildFrame) {
   reportReady();
 }

+ 8 - 0
manifest.json

@@ -53,6 +53,14 @@
       ],
       "js": ["content/utils.js", "content/duck-mail.js"],
       "run_at": "document_idle"
+    },
+    {
+      "matches": [
+        "https://inbucket.j2to.de/*"
+      ],
+      "js": ["content/utils.js", "content/inbucket-mail.js"],
+      "all_frames": true,
+      "run_at": "document_idle"
     }
   ],
   "action": {

+ 5 - 0
sidepanel/sidepanel.css

@@ -301,6 +301,11 @@ header {
   padding-inline: 10px;
 }
 
+#btn-toggle-password {
+  min-width: 58px;
+  padding-inline: 10px;
+}
+
 .data-select {
   flex: 1;
   padding: 7px 10px;

+ 12 - 0
sidepanel/sidepanel.html

@@ -53,8 +53,13 @@
         <select id="select-mail-provider" class="data-select">
           <option value="163">163 Mail (mail.163.com)</option>
           <option value="qq">QQ Mail (wx.mail.qq.com)</option>
+          <option value="inbucket">Inbucket (inbucket.j2to.de/m/&lt;mailbox&gt;/)</option>
         </select>
       </div>
+      <div class="data-row" id="row-inbucket-mailbox" style="display:none;">
+        <span class="data-label">Mailbox</span>
+        <input type="text" id="input-inbucket-mailbox" class="data-input" placeholder="e.g. zju2001" />
+      </div>
       <div class="data-row">
         <span class="data-label">Email</span>
         <div class="data-inline">
@@ -62,6 +67,13 @@
           <button id="btn-fetch-email" class="btn btn-outline btn-sm" type="button">Auto</button>
         </div>
       </div>
+      <div class="data-row">
+        <span class="data-label">Password</span>
+        <div class="data-inline">
+          <input type="password" id="input-password" class="data-input" placeholder="Leave blank to auto-generate" />
+          <button id="btn-toggle-password" class="btn btn-outline btn-sm" type="button">Show</button>
+        </div>
+      </div>
       <div class="data-row">
         <span class="data-label">OAuth</span>
         <span id="display-oauth-url" class="data-value mono truncate">Waiting...</span>

+ 49 - 0
sidepanel/sidepanel.js

@@ -14,7 +14,9 @@ const displayLocalhostUrl = document.getElementById('display-localhost-url');
 const displayStatus = document.getElementById('display-status');
 const statusBar = document.getElementById('status-bar');
 const inputEmail = document.getElementById('input-email');
+const inputPassword = document.getElementById('input-password');
 const btnFetchEmail = document.getElementById('btn-fetch-email');
+const btnTogglePassword = document.getElementById('btn-toggle-password');
 const btnStop = document.getElementById('btn-stop');
 const btnReset = document.getElementById('btn-reset');
 const stepsProgress = document.getElementById('steps-progress');
@@ -24,6 +26,8 @@ const autoContinueBar = document.getElementById('auto-continue-bar');
 const btnClearLog = document.getElementById('btn-clear-log');
 const inputVpsUrl = document.getElementById('input-vps-url');
 const selectMailProvider = document.getElementById('select-mail-provider');
+const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
+const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
 const inputRunCount = document.getElementById('input-run-count');
 
 // ============================================================
@@ -77,12 +81,16 @@ async function restoreState() {
     if (state.email) {
       inputEmail.value = state.email;
     }
+    syncPasswordField(state);
     if (state.vpsUrl) {
       inputVpsUrl.value = state.vpsUrl;
     }
     if (state.mailProvider) {
       selectMailProvider.value = state.mailProvider;
     }
+    if (state.inbucketMailbox) {
+      inputInbucketMailbox.value = state.inbucketMailbox;
+    }
 
     if (state.stepStatuses) {
       for (const [step, status] of Object.entries(state.stepStatuses)) {
@@ -98,11 +106,21 @@ async function restoreState() {
 
     updateStatusDisplay(state);
     updateProgressCounter();
+    updateMailProviderUI();
   } catch (err) {
     console.error('Failed to restore state:', err);
   }
 }
 
+function syncPasswordField(state) {
+  inputPassword.value = state.customPassword || state.password || '';
+}
+
+function updateMailProviderUI() {
+  const useInbucket = selectMailProvider.value === 'inbucket';
+  rowInbucketMailbox.style.display = useInbucket ? '' : 'none';
+}
+
 // ============================================================
 // UI Updates
 // ============================================================
@@ -262,6 +280,10 @@ async function fetchDuckEmail() {
   }
 }
 
+function syncPasswordToggleLabel() {
+  btnTogglePassword.textContent = inputPassword.type === 'password' ? 'Show' : 'Hide';
+}
+
 // ============================================================
 // Button Handlers
 // ============================================================
@@ -286,6 +308,11 @@ btnFetchEmail.addEventListener('click', async () => {
   await fetchDuckEmail().catch(() => {});
 });
 
+btnTogglePassword.addEventListener('click', () => {
+  inputPassword.type = inputPassword.type === 'password' ? 'text' : 'password';
+  syncPasswordToggleLabel();
+});
+
 btnStop.addEventListener('click', async () => {
   btnStop.disabled = true;
   await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
@@ -355,13 +382,30 @@ inputVpsUrl.addEventListener('change', async () => {
   }
 });
 
+inputPassword.addEventListener('change', async () => {
+  await chrome.runtime.sendMessage({
+    type: 'SAVE_SETTING',
+    source: 'sidepanel',
+    payload: { customPassword: inputPassword.value },
+  });
+});
+
 selectMailProvider.addEventListener('change', async () => {
+  updateMailProviderUI();
   await chrome.runtime.sendMessage({
     type: 'SAVE_SETTING', source: 'sidepanel',
     payload: { mailProvider: selectMailProvider.value },
   });
 });
 
+inputInbucketMailbox.addEventListener('change', async () => {
+  await chrome.runtime.sendMessage({
+    type: 'SAVE_SETTING',
+    source: 'sidepanel',
+    payload: { inbucketMailbox: inputInbucketMailbox.value.trim() },
+  });
+});
+
 // ============================================================
 // Listen for Background broadcasts
 // ============================================================
@@ -381,6 +425,7 @@ chrome.runtime.onMessage.addListener((message) => {
       chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
       if (status === 'completed') {
         chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
+          syncPasswordField(state);
           if (state.oauthUrl) {
             displayOauthUrl.textContent = state.oauthUrl;
             displayOauthUrl.classList.add('has-value');
@@ -415,6 +460,9 @@ chrome.runtime.onMessage.addListener((message) => {
       if (message.payload.email) {
         inputEmail.value = message.payload.email;
       }
+      if (message.payload.password !== undefined) {
+        inputPassword.value = message.payload.password || '';
+      }
       if (message.payload.oauthUrl) {
         displayOauthUrl.textContent = message.payload.oauthUrl;
         displayOauthUrl.classList.add('has-value');
@@ -490,5 +538,6 @@ btnTheme.addEventListener('click', () => {
 
 initTheme();
 restoreState().then(() => {
+  syncPasswordToggleLabel();
   updateButtonStates();
 });