Sfoglia il codice sorgente

Add LuckMail openai mailbox management

Q3CC 4 mesi fa
parent
commit
84412c7ea4

File diff suppressed because it is too large
+ 923 - 2
background.js


+ 445 - 0
luckmail-utils.js

@@ -0,0 +1,445 @@
+(function luckmailUtilsModule(root, factory) {
+  if (typeof module !== 'undefined' && module.exports) {
+    module.exports = factory();
+    return;
+  }
+
+  root.LuckMailUtils = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createLuckmailUtils() {
+  const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com';
+  const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph';
+  const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
+  const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';
+  const LUCKMAIL_EMAIL_TYPES = ['self_built', 'ms_imap', 'ms_graph', 'google_variant'];
+
+  function firstNonEmptyString(values) {
+    for (const value of values) {
+      if (value === undefined || value === null) continue;
+      const normalized = String(value).trim();
+      if (normalized) return normalized;
+    }
+    return '';
+  }
+
+  function normalizeText(value) {
+    return String(value || '')
+      .replace(/\s+/g, ' ')
+      .trim()
+      .toLowerCase();
+  }
+
+  function normalizeTimestamp(value) {
+    if (!value) return 0;
+    if (typeof value === 'number' && Number.isFinite(value)) {
+      return value > 0 ? value : 0;
+    }
+
+    const rawValue = String(value || '').trim();
+    const utcLikeMatch = rawValue.match(
+      /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/
+    );
+    if (utcLikeMatch && !/[zZ]|[+\-]\d{2}:?\d{2}$/.test(rawValue)) {
+      const [, year, month, day, hour, minute, second = '0'] = utcLikeMatch;
+      return Date.UTC(
+        Number(year),
+        Number(month) - 1,
+        Number(day),
+        Number(hour),
+        Number(minute),
+        Number(second),
+        0
+      );
+    }
+
+    const timestamp = Date.parse(rawValue);
+    return Number.isFinite(timestamp) ? timestamp : 0;
+  }
+
+  function normalizeLuckmailBaseUrl(rawValue = '') {
+    const value = String(rawValue || '').trim();
+    if (!value) return DEFAULT_LUCKMAIL_BASE_URL;
+
+    try {
+      const parsed = new URL(value);
+      if (!['http:', 'https:'].includes(parsed.protocol)) {
+        return DEFAULT_LUCKMAIL_BASE_URL;
+      }
+      parsed.pathname = parsed.pathname.replace(/\/+$/, '');
+      parsed.search = '';
+      parsed.hash = '';
+      return parsed.toString().replace(/\/$/, '');
+    } catch {
+      return DEFAULT_LUCKMAIL_BASE_URL;
+    }
+  }
+
+  function normalizeLuckmailEmailType(rawValue = '') {
+    const normalized = String(rawValue || '').trim().toLowerCase();
+    return LUCKMAIL_EMAIL_TYPES.includes(normalized)
+      ? normalized
+      : DEFAULT_LUCKMAIL_EMAIL_TYPE;
+  }
+
+  function normalizeLuckmailProjectName(rawValue = '') {
+    return normalizeText(rawValue);
+  }
+
+  function extractLuckmailVerificationCode(text) {
+    const source = String(text || '');
+    const matchCn = source.match(/(?:代码为|验证码[^0-9]*?)[\s::]*(\d{6})/i);
+    if (matchCn) return matchCn[1];
+
+    const matchChatGPT = source.match(/your\s+chatgpt\s+code\s+is\s+(\d{6})/i);
+    if (matchChatGPT) return matchChatGPT[1];
+
+    const matchEn = source.match(/code(?:\s+is|[\s:])+(\d{6})/i);
+    if (matchEn) return matchEn[1];
+
+    const matchStandalone = source.match(/\b(\d{6})\b/);
+    return matchStandalone ? matchStandalone[1] : null;
+  }
+
+  function normalizeLuckmailTag(item = {}) {
+    const safeItem = item && typeof item === 'object' ? item : {};
+    return {
+      id: Number(safeItem.id) || 0,
+      name: firstNonEmptyString([safeItem.name, safeItem.tag_name]),
+      remark: firstNonEmptyString([safeItem.remark]),
+      limit_type: Number(safeItem.limit_type) || 0,
+      purchase_count: Number(safeItem.purchase_count) || 0,
+      created_at: firstNonEmptyString([safeItem.created_at]) || null,
+    };
+  }
+
+  function normalizeLuckmailTags(input) {
+    const list = Array.isArray(input?.list)
+      ? input.list
+      : (Array.isArray(input) ? input : []);
+    return list
+      .map((item) => normalizeLuckmailTag(item))
+      .filter((item) => item.id > 0 || item.name);
+  }
+
+  function normalizeLuckmailPurchase(item = {}) {
+    const safeItem = item && typeof item === 'object' ? item : {};
+    const projectName = firstNonEmptyString([safeItem.project_name, safeItem.project]);
+    return {
+      id: Number(safeItem.id) || 0,
+      email_address: firstNonEmptyString([safeItem.email_address, safeItem.address]),
+      token: firstNonEmptyString([safeItem.token]),
+      project_name: projectName,
+      project_code: normalizeLuckmailProjectName(projectName),
+      price: firstNonEmptyString([safeItem.price]) || '0.0000',
+      status: Number(safeItem.status) || 0,
+      tag_id: Number(safeItem.tag_id) || 0,
+      tag_name: firstNonEmptyString([safeItem.tag_name]),
+      user_disabled: Number(safeItem.user_disabled) || 0,
+      warranty_hours: Number(safeItem.warranty_hours) || 0,
+      warranty_until: firstNonEmptyString([safeItem.warranty_until]) || null,
+      created_at: firstNonEmptyString([safeItem.created_at]) || null,
+    };
+  }
+
+  function normalizeLuckmailPurchases(result) {
+    const list = Array.isArray(result?.purchases)
+      ? result.purchases
+      : (Array.isArray(result) ? result : []);
+    return list.map((item) => normalizeLuckmailPurchase(item));
+  }
+
+  function normalizeLuckmailPurchaseListPage(result = {}) {
+    const safeResult = result && typeof result === 'object' ? result : {};
+    const list = Array.isArray(safeResult.list)
+      ? safeResult.list
+      : (Array.isArray(safeResult.purchases) ? safeResult.purchases : []);
+    const total = Number(safeResult.total);
+    return {
+      list: list.map((item) => normalizeLuckmailPurchase(item)),
+      total: Number.isFinite(total) && total >= 0 ? total : 0,
+      page: Math.max(1, Number(safeResult.page) || 1),
+      page_size: Math.max(1, Number(safeResult.page_size || safeResult.pageSize) || list.length || 1),
+    };
+  }
+
+  function normalizeLuckmailPurchaseId(value) {
+    const numeric = Number(value);
+    if (!Number.isFinite(numeric) || numeric <= 0) {
+      return '';
+    }
+    return String(Math.floor(numeric));
+  }
+
+  function normalizeLuckmailUsedPurchases(rawValue = {}) {
+    if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
+      return {};
+    }
+
+    return Object.entries(rawValue).reduce((result, [key, value]) => {
+      const normalizedKey = normalizeLuckmailPurchaseId(key);
+      if (!normalizedKey) {
+        return result;
+      }
+      result[normalizedKey] = Boolean(value);
+      return result;
+    }, {});
+  }
+
+  function isLuckmailPurchaseForProject(purchase, projectCode = DEFAULT_LUCKMAIL_PROJECT_CODE) {
+    const normalizedPurchase = normalizeLuckmailPurchase(purchase);
+    return normalizeLuckmailProjectName(normalizedPurchase.project_name) === normalizeLuckmailProjectName(projectCode);
+  }
+
+  function normalizeLuckmailTokenMail(mail = {}) {
+    const safeMail = mail && typeof mail === 'object' ? mail : {};
+    const subject = firstNonEmptyString([safeMail.subject, safeMail.title]);
+    const body = firstNonEmptyString([safeMail.body, safeMail.body_text, safeMail.text]);
+    const htmlBody = firstNonEmptyString([safeMail.html_body, safeMail.body_html, safeMail.html]);
+    const from = firstNonEmptyString([safeMail.from, safeMail.sender]);
+    const verificationCode = firstNonEmptyString([safeMail.verification_code])
+      || extractLuckmailVerificationCode([subject, body, htmlBody, from].filter(Boolean).join(' '));
+
+    return {
+      message_id: firstNonEmptyString([safeMail.message_id, safeMail.id]),
+      from,
+      subject,
+      body,
+      html_body: htmlBody,
+      received_at: firstNonEmptyString([safeMail.received_at, safeMail.receivedAt, safeMail.created_at]),
+      verification_code: verificationCode || '',
+    };
+  }
+
+  function normalizeLuckmailTokenMails(input) {
+    const list = Array.isArray(input?.mails)
+      ? input.mails
+      : (Array.isArray(input) ? input : []);
+    return list.map((mail) => normalizeLuckmailTokenMail(mail));
+  }
+
+  function normalizeLuckmailTokenCode(result = {}) {
+    return {
+      email_address: firstNonEmptyString([result.email_address, result.address]),
+      project: firstNonEmptyString([result.project]),
+      has_new_mail: Boolean(result.has_new_mail),
+      verification_code: firstNonEmptyString([result.verification_code]) || null,
+      mail: result.mail ? normalizeLuckmailTokenMail(result.mail) : null,
+    };
+  }
+
+  function normalizeLuckmailMailCursor(cursor = {}) {
+    const safeCursor = cursor && typeof cursor === 'object' ? cursor : {};
+    return {
+      messageId: firstNonEmptyString([safeCursor.messageId, safeCursor.message_id]),
+      receivedAt: firstNonEmptyString([safeCursor.receivedAt, safeCursor.received_at]),
+    };
+  }
+
+  function buildLuckmailMailCursor(mail = {}) {
+    const normalizedMail = normalizeLuckmailTokenMail(mail);
+    return normalizeLuckmailMailCursor({
+      messageId: normalizedMail.message_id,
+      receivedAt: normalizedMail.received_at,
+    });
+  }
+
+  function buildLuckmailBaselineCursor(mails) {
+    const latestMail = normalizeLuckmailTokenMails(mails)
+      .sort((left, right) => {
+        const leftTimestamp = normalizeTimestamp(left.received_at);
+        const rightTimestamp = normalizeTimestamp(right.received_at);
+        if (leftTimestamp !== rightTimestamp) {
+          return rightTimestamp - leftTimestamp;
+        }
+        return String(right.message_id || '').localeCompare(String(left.message_id || ''));
+      })[0] || null;
+
+    return latestMail ? buildLuckmailMailCursor(latestMail) : null;
+  }
+
+  function isLuckmailMailNewerThanCursor(mail = {}, cursor = {}) {
+    const normalizedMail = normalizeLuckmailTokenMail(mail);
+    const normalizedCursor = normalizeLuckmailMailCursor(cursor);
+
+    if (!normalizedCursor.messageId && !normalizedCursor.receivedAt) {
+      return true;
+    }
+
+    if (normalizedMail.message_id && normalizedCursor.messageId && normalizedMail.message_id === normalizedCursor.messageId) {
+      return false;
+    }
+
+    const mailTimestamp = normalizeTimestamp(normalizedMail.received_at);
+    const cursorTimestamp = normalizeTimestamp(normalizedCursor.receivedAt);
+
+    if (mailTimestamp && cursorTimestamp) {
+      if (mailTimestamp > cursorTimestamp) return true;
+      if (mailTimestamp < cursorTimestamp) return false;
+      return Boolean(
+        normalizedMail.message_id
+        && normalizedCursor.messageId
+        && normalizedMail.message_id !== normalizedCursor.messageId
+      );
+    }
+
+    if (normalizedMail.message_id && normalizedCursor.messageId) {
+      return normalizedMail.message_id !== normalizedCursor.messageId;
+    }
+
+    return !cursorTimestamp || Boolean(mailTimestamp && mailTimestamp > cursorTimestamp);
+  }
+
+  function isLuckmailPurchaseExpired(purchase, now = Date.now()) {
+    const normalizedPurchase = normalizeLuckmailPurchase(purchase);
+    const expiresAt = normalizeTimestamp(normalizedPurchase.warranty_until);
+    return Boolean(expiresAt && expiresAt <= Number(now || 0));
+  }
+
+  function isLuckmailPurchaseDisabled(purchase) {
+    return Number(normalizeLuckmailPurchase(purchase).user_disabled) === 1;
+  }
+
+  function isLuckmailPurchasePreserved(purchase, options = {}) {
+    const normalizedPurchase = normalizeLuckmailPurchase(purchase);
+    const expectedTagId = Number(options.preserveTagId) || 0;
+    const expectedTagName = normalizeText(options.preserveTagName || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME);
+
+    if (expectedTagId > 0 && normalizedPurchase.tag_id === expectedTagId) {
+      return true;
+    }
+
+    return Boolean(normalizedPurchase.tag_name && normalizeText(normalizedPurchase.tag_name) === expectedTagName);
+  }
+
+  function isLuckmailPurchaseReusable(purchase, options = {}) {
+    const normalizedPurchase = normalizeLuckmailPurchase(purchase);
+    const usedPurchases = normalizeLuckmailUsedPurchases(options.usedPurchases);
+    const purchaseId = normalizeLuckmailPurchaseId(normalizedPurchase.id);
+
+    if (!isLuckmailPurchaseForProject(normalizedPurchase, options.projectCode || DEFAULT_LUCKMAIL_PROJECT_CODE)) {
+      return false;
+    }
+    if (!normalizedPurchase.email_address || !normalizedPurchase.token) {
+      return false;
+    }
+    if (isLuckmailPurchaseDisabled(normalizedPurchase)) {
+      return false;
+    }
+    if (purchaseId && usedPurchases[purchaseId]) {
+      return false;
+    }
+    if (isLuckmailPurchasePreserved(normalizedPurchase, options)) {
+      return false;
+    }
+    if (isLuckmailPurchaseExpired(normalizedPurchase, options.now || Date.now())) {
+      return false;
+    }
+    return true;
+  }
+
+  function filterReusableLuckmailPurchases(purchases, options = {}) {
+    const list = Array.isArray(purchases)
+      ? purchases
+      : normalizeLuckmailPurchaseListPage(purchases).list;
+    return list
+      .map((purchase) => normalizeLuckmailPurchase(purchase))
+      .filter((purchase) => isLuckmailPurchaseReusable(purchase, options));
+  }
+
+  function pickReusableLuckmailPurchase(purchases, options = {}) {
+    return filterReusableLuckmailPurchases(purchases, options)[0] || null;
+  }
+
+  function mailMatchesLuckmailFilters(mail, filters = {}) {
+    const normalizedMail = normalizeLuckmailTokenMail(mail);
+    const afterTimestamp = normalizeTimestamp(filters.afterTimestamp);
+    const receivedAt = normalizeTimestamp(normalizedMail.received_at);
+    if (afterTimestamp && receivedAt && receivedAt < afterTimestamp) {
+      return null;
+    }
+
+    const senderFilters = (filters.senderFilters || []).map(normalizeText).filter(Boolean);
+    const subjectFilters = (filters.subjectFilters || []).map(normalizeText).filter(Boolean);
+    const excludedCodes = new Set((filters.excludeCodes || []).filter(Boolean));
+    const combinedText = [
+      normalizedMail.subject,
+      normalizedMail.from,
+      normalizedMail.body,
+      normalizedMail.html_body,
+    ].filter(Boolean).join(' ');
+    const combinedTextNormalized = normalizeText(combinedText);
+    const senderNormalized = normalizeText(normalizedMail.from);
+    const subjectNormalized = normalizeText(normalizedMail.subject);
+    const code = normalizedMail.verification_code || extractLuckmailVerificationCode(combinedText);
+
+    if (!code || excludedCodes.has(code)) {
+      return null;
+    }
+
+    const senderMatch = senderFilters.length === 0
+      ? true
+      : senderFilters.some((item) => senderNormalized.includes(item) || combinedTextNormalized.includes(item));
+    const subjectMatch = subjectFilters.length === 0
+      ? true
+      : subjectFilters.some((item) => subjectNormalized.includes(item) || combinedTextNormalized.includes(item));
+
+    if (!senderMatch && !subjectMatch) {
+      return null;
+    }
+
+    return {
+      code,
+      mail: normalizedMail,
+      receivedAt,
+    };
+  }
+
+  function pickLuckmailVerificationMail(mails, filters = {}) {
+    const matches = normalizeLuckmailTokenMails(mails)
+      .map((mail) => mailMatchesLuckmailFilters(mail, filters))
+      .filter(Boolean)
+      .sort((left, right) => {
+        if (left.receivedAt !== right.receivedAt) {
+          return right.receivedAt - left.receivedAt;
+        }
+        return String(right.mail.message_id || '').localeCompare(String(left.mail.message_id || ''));
+      });
+
+    return matches[0] || null;
+  }
+
+  return {
+    DEFAULT_LUCKMAIL_BASE_URL,
+    DEFAULT_LUCKMAIL_EMAIL_TYPE,
+    DEFAULT_LUCKMAIL_PROJECT_CODE,
+    DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
+    LUCKMAIL_EMAIL_TYPES,
+    buildLuckmailBaselineCursor,
+    buildLuckmailMailCursor,
+    extractLuckmailVerificationCode,
+    filterReusableLuckmailPurchases,
+    isLuckmailMailNewerThanCursor,
+    isLuckmailPurchaseDisabled,
+    isLuckmailPurchaseExpired,
+    isLuckmailPurchaseForProject,
+    isLuckmailPurchasePreserved,
+    isLuckmailPurchaseReusable,
+    normalizeLuckmailBaseUrl,
+    normalizeLuckmailEmailType,
+    normalizeLuckmailMailCursor,
+    normalizeLuckmailProjectName,
+    normalizeLuckmailPurchase,
+    normalizeLuckmailPurchaseId,
+    normalizeLuckmailPurchaseListPage,
+    normalizeLuckmailPurchases,
+    normalizeLuckmailTag,
+    normalizeLuckmailTags,
+    normalizeLuckmailTokenCode,
+    normalizeLuckmailTokenMail,
+    normalizeLuckmailTokenMails,
+    normalizeLuckmailUsedPurchases,
+    normalizeText,
+    normalizeTimestamp,
+    pickLuckmailVerificationMail,
+    pickReusableLuckmailPurchase,
+  };
+});

+ 158 - 0
sidepanel/sidepanel.css

@@ -698,6 +698,164 @@ header {
   margin-top: 10px;
 }
 
+.luckmail-card {
+  margin-top: 10px;
+}
+
+.luckmail-manager-card {
+  border: 1px solid var(--border-subtle);
+  border-radius: var(--radius-sm);
+  background: color-mix(in srgb, var(--bg-base) 84%, transparent);
+  padding: 10px;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.luckmail-summary {
+  color: var(--text-secondary);
+  font-size: 12px;
+  line-height: 1.6;
+}
+
+.luckmail-toolbar {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.luckmail-search {
+  flex: 1;
+}
+
+.luckmail-filter {
+  flex: 0 0 130px;
+}
+
+.luckmail-bulkbar {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+}
+
+.luckmail-select-all {
+  flex: 0 0 auto;
+}
+
+.luckmail-bulk-actions {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+}
+
+.luckmail-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  max-height: 360px;
+  overflow: auto;
+  padding-right: 4px;
+}
+
+.luckmail-item {
+  border: 1px solid var(--border);
+  background: var(--bg-base);
+  border-radius: var(--radius-sm);
+  padding: 10px;
+  display: grid;
+  grid-template-columns: auto minmax(0, 1fr) auto;
+  gap: 10px;
+  align-items: flex-start;
+}
+
+.luckmail-item.is-current {
+  border-color: var(--blue);
+  box-shadow: 0 0 0 1px var(--blue-glow);
+}
+
+.luckmail-item-check {
+  margin-top: 4px;
+}
+
+.luckmail-item-main {
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.luckmail-item-email-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.luckmail-item-email {
+  font-weight: 600;
+  color: var(--text-primary);
+  word-break: break-all;
+}
+
+.luckmail-item-meta,
+.luckmail-item-details {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.luckmail-item-details {
+  color: var(--text-secondary);
+  font-size: 12px;
+}
+
+.luckmail-item-actions {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+}
+
+.luckmail-tag {
+  display: inline-flex;
+  align-items: center;
+  padding: 2px 8px;
+  border-radius: 999px;
+  font-size: 11px;
+  background: var(--bg-surface);
+  color: var(--text-secondary);
+}
+
+.luckmail-tag.used {
+  background: var(--orange-soft);
+  color: var(--orange);
+}
+
+.luckmail-tag.active {
+  background: var(--green-soft);
+  color: var(--green);
+}
+
+.luckmail-tag.current {
+  background: var(--blue-soft);
+  color: var(--blue);
+}
+
+.luckmail-tag.disabled {
+  background: var(--red-soft);
+  color: var(--red);
+}
+
+.luckmail-empty {
+  color: var(--text-muted);
+  font-size: 12px;
+  border: 1px dashed var(--border);
+  border-radius: var(--radius-sm);
+  padding: 12px;
+  text-align: center;
+}
+
 .hotmail-actions-row .data-label {
   visibility: hidden;
 }

+ 73 - 0
sidepanel/sidepanel.html

@@ -154,6 +154,7 @@
           <select id="select-mail-provider" class="data-select">
             <option value="custom">自定义邮箱</option>
             <option value="hotmail-api">Hotmail(本地助手)</option>
+            <option value="luckmail-api">LuckMail(API 购邮)</option>
             <option value="163">163 邮箱 (mail.163.com)</option>
             <option value="163-vip">163 VIP 邮箱 (vip.163.com)</option>
             <option value="qq">QQ 邮箱 (wx.mail.qq.com)</option>
@@ -316,6 +317,77 @@
         <div id="hotmail-accounts-list" class="hotmail-accounts-list"></div>
       </div>
     </div>
+    <div id="luckmail-section" class="data-card luckmail-card" style="display:none;">
+      <div class="section-mini-header">
+        <div class="section-mini-copy">
+          <span class="section-label">LuckMail 配置</span>
+        </div>
+      </div>
+      <div class="data-row">
+        <span class="data-label">API Key</span>
+        <input type="password" id="input-luckmail-api-key" class="data-input mono" placeholder="请输入 LuckMail API Key" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">Base URL</span>
+        <input type="text" id="input-luckmail-base-url" class="data-input mono" placeholder="https://mails.luckyous.com" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">邮箱类型</span>
+        <select id="select-luckmail-email-type" class="data-select">
+          <option value="self_built">自建邮箱</option>
+          <option value="ms_imap">微软 IMAP</option>
+          <option value="ms_graph">微软 Graph</option>
+          <option value="google_variant">谷歌变种</option>
+        </select>
+      </div>
+      <div class="data-row">
+        <span class="data-label">域名</span>
+        <input type="text" id="input-luckmail-domain" class="data-input mono" placeholder="可选,留空则由 LuckMail 默认处理" />
+      </div>
+      <div class="data-row">
+        <span class="data-label">项目</span>
+        <span class="data-value mono">openai</span>
+      </div>
+      <div class="luckmail-manager-card">
+        <div class="section-mini-header">
+          <div class="section-mini-copy">
+            <span class="section-label">LuckMail 邮箱列表</span>
+            <span class="data-value">仅显示项目为 <span class="mono">openai</span> 的邮箱</span>
+          </div>
+          <div class="section-mini-actions">
+            <button id="btn-luckmail-refresh" class="btn btn-ghost btn-xs" type="button">刷新</button>
+            <button id="btn-luckmail-disable-used" class="btn btn-ghost btn-xs" type="button">禁用已用</button>
+          </div>
+        </div>
+        <div id="luckmail-summary" class="luckmail-summary">加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。</div>
+        <div class="luckmail-toolbar">
+          <input id="input-luckmail-search" class="data-input luckmail-search" type="text" placeholder="搜索邮箱 / 标签 / 项目" />
+          <select id="select-luckmail-filter" class="data-select luckmail-filter">
+            <option value="all">全部</option>
+            <option value="reusable">可复用</option>
+            <option value="used">已用</option>
+            <option value="unused">未用</option>
+            <option value="preserved">保留</option>
+            <option value="disabled">已禁用</option>
+          </select>
+        </div>
+        <div class="luckmail-bulkbar">
+          <label class="option-toggle luckmail-select-all" for="checkbox-luckmail-select-all">
+            <input type="checkbox" id="checkbox-luckmail-select-all" />
+            <span id="luckmail-selection-summary">已选 0 个</span>
+          </label>
+          <div class="luckmail-bulk-actions">
+            <button id="btn-luckmail-bulk-used" class="btn btn-outline btn-xs" type="button">标记已用</button>
+            <button id="btn-luckmail-bulk-unused" class="btn btn-outline btn-xs" type="button">标记未用</button>
+            <button id="btn-luckmail-bulk-preserve" class="btn btn-outline btn-xs" type="button">保留</button>
+            <button id="btn-luckmail-bulk-unpreserve" class="btn btn-outline btn-xs" type="button">取消保留</button>
+            <button id="btn-luckmail-bulk-disable" class="btn btn-outline btn-xs" type="button">禁用</button>
+            <button id="btn-luckmail-bulk-enable" class="btn btn-outline btn-xs" type="button">启用</button>
+          </div>
+        </div>
+        <div id="luckmail-list" class="luckmail-list"></div>
+      </div>
+    </div>
     <div id="status-bar" class="status-bar">
       <div class="status-dot"></div>
       <span id="display-status">就绪</span>
@@ -432,6 +504,7 @@
   <div id="toast-container"></div>
   <input id="input-import-settings-file" type="file" accept=".json,application/json" hidden />
   <script src="../hotmail-utils.js"></script>
+  <script src="../luckmail-utils.js"></script>
   <script src="update-service.js"></script>
   <script src="sidepanel.js"></script>
 </body>

+ 626 - 13
sidepanel/sidepanel.js

@@ -69,6 +69,7 @@ const btnMailLogin = document.getElementById('btn-mail-login');
 const rowEmailGenerator = document.getElementById('row-email-generator');
 const selectEmailGenerator = document.getElementById('select-email-generator');
 const hotmailSection = document.getElementById('hotmail-section');
+const luckmailSection = document.getElementById('luckmail-section');
 const rowHotmailServiceMode = document.getElementById('row-hotmail-service-mode');
 const hotmailServiceModeButtons = Array.from(document.querySelectorAll('[data-hotmail-service-mode]'));
 const rowHotmailRemoteBaseUrl = document.getElementById('row-hotmail-remote-base-url');
@@ -88,6 +89,24 @@ const btnDeleteAllHotmailAccounts = document.getElementById('btn-delete-all-hotm
 const btnToggleHotmailList = document.getElementById('btn-toggle-hotmail-list');
 const hotmailListShell = document.getElementById('hotmail-list-shell');
 const hotmailAccountsList = document.getElementById('hotmail-accounts-list');
+const inputLuckmailApiKey = document.getElementById('input-luckmail-api-key');
+const inputLuckmailBaseUrl = document.getElementById('input-luckmail-base-url');
+const selectLuckmailEmailType = document.getElementById('select-luckmail-email-type');
+const inputLuckmailDomain = document.getElementById('input-luckmail-domain');
+const btnLuckmailRefresh = document.getElementById('btn-luckmail-refresh');
+const btnLuckmailDisableUsed = document.getElementById('btn-luckmail-disable-used');
+const luckmailSummary = document.getElementById('luckmail-summary');
+const inputLuckmailSearch = document.getElementById('input-luckmail-search');
+const selectLuckmailFilter = document.getElementById('select-luckmail-filter');
+const checkboxLuckmailSelectAll = document.getElementById('checkbox-luckmail-select-all');
+const luckmailSelectionSummary = document.getElementById('luckmail-selection-summary');
+const btnLuckmailBulkUsed = document.getElementById('btn-luckmail-bulk-used');
+const btnLuckmailBulkUnused = document.getElementById('btn-luckmail-bulk-unused');
+const btnLuckmailBulkPreserve = document.getElementById('btn-luckmail-bulk-preserve');
+const btnLuckmailBulkUnpreserve = document.getElementById('btn-luckmail-bulk-unpreserve');
+const btnLuckmailBulkDisable = document.getElementById('btn-luckmail-bulk-disable');
+const btnLuckmailBulkEnable = document.getElementById('btn-luckmail-bulk-enable');
+const luckmailList = document.getElementById('luckmail-list');
 const rowEmailPrefix = document.getElementById('row-email-prefix');
 const labelEmailPrefix = document.getElementById('label-email-prefix');
 const inputEmailPrefix = document.getElementById('input-email-prefix');
@@ -145,6 +164,9 @@ const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 15;
 const AUTO_RUN_FALLBACK_RISK_RECOMMENDED_THREAD_INTERVAL_MINUTES = 5;
 const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
 const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
+const LUCKMAIL_PROVIDER = 'luckmail-api';
+const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com';
+const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph';
 const DISPLAY_TIMEZONE = 'Asia/Shanghai';
 
 let latestState = null;
@@ -182,8 +204,20 @@ const upsertHotmailAccountInList = window.HotmailUtils?.upsertHotmailAccountInLi
 const filterHotmailAccountsByUsage = window.HotmailUtils?.filterHotmailAccountsByUsage;
 const getHotmailBulkActionLabel = window.HotmailUtils?.getHotmailBulkActionLabel;
 const getHotmailListToggleLabel = window.HotmailUtils?.getHotmailListToggleLabel;
+const normalizeLuckmailTimestampValue = window.LuckMailUtils?.normalizeTimestamp
+  || ((value) => {
+    const timestamp = Date.parse(String(value || ''));
+    return Number.isFinite(timestamp) ? timestamp : 0;
+  });
 const HOTMAIL_LIST_EXPANDED_STORAGE_KEY = 'multipage-hotmail-list-expanded';
 const sidepanelUpdateService = window.SidepanelUpdateService;
+const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = window.LuckMailUtils?.DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME || '保留';
+
+let lastRenderedLuckmailPurchases = [];
+let luckmailSelectedPurchaseIds = new Set();
+let luckmailSearchTerm = '';
+let luckmailFilterMode = 'all';
+let luckmailRefreshQueued = false;
 
 btnAutoCancelSchedule?.remove();
 const MAIL_PROVIDER_LOGIN_CONFIGS = {
@@ -933,6 +967,10 @@ function collectSettingsPayload() {
     hotmailServiceMode: getSelectedHotmailServiceMode(),
     hotmailRemoteBaseUrl: inputHotmailRemoteBaseUrl.value.trim(),
     hotmailLocalBaseUrl: inputHotmailLocalBaseUrl.value.trim(),
+    luckmailApiKey: inputLuckmailApiKey.value,
+    luckmailBaseUrl: normalizeLuckmailBaseUrl(inputLuckmailBaseUrl.value),
+    luckmailEmailType: normalizeLuckmailEmailType(selectLuckmailEmailType.value),
+    luckmailDomain: inputLuckmailDomain.value.trim(),
     cloudflareDomain: selectedCloudflareDomain,
     cloudflareDomains: domains,
     autoRunSkipFailures: inputAutoSkipFailures.checked,
@@ -1206,7 +1244,7 @@ function applySettingsState(state) {
   inputSub2ApiPassword.value = state?.sub2apiPassword || '';
   inputSub2ApiGroup.value = state?.sub2apiGroupName || '';
   const restoredMailProvider = isCustomMailProvider(state?.mailProvider)
-    || ['hotmail-api', '163', '163-vip', 'qq', 'inbucket', '2925'].includes(String(state?.mailProvider || '').trim())
+    || ['hotmail-api', 'luckmail-api', '163', '163-vip', 'qq', 'inbucket', '2925'].includes(String(state?.mailProvider || '').trim())
     ? String(state?.mailProvider || '163').trim()
     : (String(state?.emailGenerator || '').trim().toLowerCase() === 'custom'
       || String(state?.emailGenerator || '').trim().toLowerCase() === 'manual'
@@ -1220,6 +1258,10 @@ function applySettingsState(state) {
   setHotmailServiceMode(state?.hotmailServiceMode);
   inputHotmailRemoteBaseUrl.value = state?.hotmailRemoteBaseUrl || '';
   inputHotmailLocalBaseUrl.value = state?.hotmailLocalBaseUrl || '';
+  inputLuckmailApiKey.value = state?.luckmailApiKey || '';
+  inputLuckmailBaseUrl.value = normalizeLuckmailBaseUrl(state?.luckmailBaseUrl);
+  selectLuckmailEmailType.value = normalizeLuckmailEmailType(state?.luckmailEmailType);
+  inputLuckmailDomain.value = state?.luckmailDomain || '';
   renderCloudflareDomainOptions(state?.cloudflareDomain || '');
   setCloudflareDomainEditMode(false, { clearInput: true });
   inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures);
@@ -1237,6 +1279,9 @@ function applySettingsState(state) {
   updateFallbackThreadIntervalInputState();
   updatePanelModeUI();
   updateMailProviderUI();
+  if (isLuckmailProvider(state?.mailProvider)) {
+    queueLuckmailPurchaseRefresh();
+  }
   updateButtonStates();
 }
 
@@ -1486,6 +1531,37 @@ function isCustomMailProvider(provider = selectMailProvider.value) {
   return String(provider || '').trim().toLowerCase() === 'custom';
 }
 
+function isLuckmailProvider(provider = selectMailProvider.value) {
+  return String(provider || '').trim().toLowerCase() === LUCKMAIL_PROVIDER;
+}
+
+function normalizeLuckmailBaseUrl(value = '') {
+  const trimmed = String(value || '').trim();
+  if (!trimmed) {
+    return DEFAULT_LUCKMAIL_BASE_URL;
+  }
+
+  try {
+    const parsed = new URL(trimmed);
+    if (!['http:', 'https:'].includes(parsed.protocol)) {
+      return DEFAULT_LUCKMAIL_BASE_URL;
+    }
+    parsed.pathname = parsed.pathname.replace(/\/+$/, '');
+    parsed.search = '';
+    parsed.hash = '';
+    return parsed.toString().replace(/\/$/, '');
+  } catch {
+    return DEFAULT_LUCKMAIL_BASE_URL;
+  }
+}
+
+function normalizeLuckmailEmailType(value = '') {
+  const normalized = String(value || '').trim().toLowerCase();
+  return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(normalized)
+    ? normalized
+    : DEFAULT_LUCKMAIL_EMAIL_TYPE;
+}
+
 function getSelectedEmailGenerator() {
   const generator = String(selectEmailGenerator.value || '').trim().toLowerCase();
   if (generator === 'cloudflare') {
@@ -1546,6 +1622,417 @@ function getCurrentHotmailEmail(state = latestState) {
   return String(getCurrentHotmailAccount(state)?.email || '').trim();
 }
 
+function getCurrentLuckmailPurchase(state = latestState) {
+  return state?.currentLuckmailPurchase || null;
+}
+
+function getCurrentLuckmailEmail(state = latestState) {
+  return String(getCurrentLuckmailPurchase(state)?.email_address || '').trim();
+}
+
+function getLuckmailUsedPurchases(state = latestState) {
+  const rawValue = state?.luckmailUsedPurchases;
+  if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
+    return {};
+  }
+
+  return Object.entries(rawValue).reduce((result, [key, value]) => {
+    const numeric = Number(key);
+    if (!Number.isFinite(numeric) || numeric <= 0) {
+      return result;
+    }
+    result[String(Math.floor(numeric))] = Boolean(value);
+    return result;
+  }, {});
+}
+
+function normalizeLuckmailProjectName(value = '') {
+  return String(value || '').trim().toLowerCase();
+}
+
+function getLuckmailPreserveTagName(state = latestState) {
+  return String(state?.luckmailPreserveTagName || '').trim() || DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME;
+}
+
+function formatLuckmailDateTime(value) {
+  const timestamp = normalizeLuckmailTimestampValue(value);
+  if (!timestamp) {
+    return String(value || '').trim() || '未知';
+  }
+  return new Date(timestamp).toLocaleString('zh-CN', {
+    hour12: false,
+    timeZone: DISPLAY_TIMEZONE,
+  });
+}
+
+function normalizeLuckmailSearchText(value) {
+  return String(value || '').trim().toLowerCase();
+}
+
+function getFilteredLuckmailPurchases(purchases = lastRenderedLuckmailPurchases) {
+  const searchTerm = normalizeLuckmailSearchText(luckmailSearchTerm);
+  return (Array.isArray(purchases) ? purchases : []).filter((purchase) => {
+    const matchesFilter = (() => {
+      switch (luckmailFilterMode) {
+        case 'reusable': return Boolean(purchase.reusable);
+        case 'used': return Boolean(purchase.used);
+        case 'unused': return !purchase.used;
+        case 'preserved': return Boolean(purchase.preserved);
+        case 'disabled': return Boolean(purchase.disabled);
+        default: return true;
+      }
+    })();
+
+    if (!matchesFilter) return false;
+    if (!searchTerm) return true;
+
+    const haystack = [
+      purchase.email_address,
+      purchase.project_name,
+      purchase.tag_name,
+      purchase.used ? '已用 used' : '未用 unused',
+      purchase.preserved ? '保留 preserved' : '',
+      purchase.disabled ? '已禁用 disabled' : '',
+      purchase.reusable ? '可复用 reusable' : '',
+    ].join(' ').toLowerCase();
+
+    return haystack.includes(searchTerm);
+  });
+}
+
+function pruneLuckmailSelection(purchases = lastRenderedLuckmailPurchases) {
+  const existingIds = new Set((Array.isArray(purchases) ? purchases : []).map((purchase) => String(purchase.id)));
+  luckmailSelectedPurchaseIds = new Set([...luckmailSelectedPurchaseIds].filter((id) => existingIds.has(id)));
+}
+
+function updateLuckmailBulkUI(visiblePurchases = getFilteredLuckmailPurchases()) {
+  if (!checkboxLuckmailSelectAll || !luckmailSelectionSummary) {
+    return;
+  }
+
+  const visibleIds = visiblePurchases.map((purchase) => String(purchase.id));
+  const selectedVisibleCount = visibleIds.filter((id) => luckmailSelectedPurchaseIds.has(id)).length;
+  const hasVisible = visibleIds.length > 0;
+  const hasSelection = luckmailSelectedPurchaseIds.size > 0;
+
+  checkboxLuckmailSelectAll.checked = hasVisible && selectedVisibleCount === visibleIds.length;
+  checkboxLuckmailSelectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleIds.length;
+  checkboxLuckmailSelectAll.disabled = !hasVisible;
+  luckmailSelectionSummary.textContent = `已选 ${luckmailSelectedPurchaseIds.size} 个(当前显示 ${visibleIds.length} 个)`;
+
+  if (btnLuckmailBulkUsed) btnLuckmailBulkUsed.disabled = !hasSelection;
+  if (btnLuckmailBulkUnused) btnLuckmailBulkUnused.disabled = !hasSelection;
+  if (btnLuckmailBulkPreserve) btnLuckmailBulkPreserve.disabled = !hasSelection;
+  if (btnLuckmailBulkUnpreserve) btnLuckmailBulkUnpreserve.disabled = !hasSelection;
+  if (btnLuckmailBulkDisable) btnLuckmailBulkDisable.disabled = !hasSelection;
+  if (btnLuckmailBulkEnable) btnLuckmailBulkEnable.disabled = !hasSelection;
+}
+
+function setLuckmailLoadingState(loading, summary = '') {
+  if (btnLuckmailRefresh) btnLuckmailRefresh.disabled = loading;
+  if (btnLuckmailDisableUsed) btnLuckmailDisableUsed.disabled = loading;
+  if (inputLuckmailSearch) inputLuckmailSearch.disabled = loading;
+  if (selectLuckmailFilter) selectLuckmailFilter.disabled = loading;
+  if (checkboxLuckmailSelectAll) checkboxLuckmailSelectAll.disabled = loading || getFilteredLuckmailPurchases().length === 0;
+  if (btnLuckmailBulkUsed) btnLuckmailBulkUsed.disabled = loading || luckmailSelectedPurchaseIds.size === 0;
+  if (btnLuckmailBulkUnused) btnLuckmailBulkUnused.disabled = loading || luckmailSelectedPurchaseIds.size === 0;
+  if (btnLuckmailBulkPreserve) btnLuckmailBulkPreserve.disabled = loading || luckmailSelectedPurchaseIds.size === 0;
+  if (btnLuckmailBulkUnpreserve) btnLuckmailBulkUnpreserve.disabled = loading || luckmailSelectedPurchaseIds.size === 0;
+  if (btnLuckmailBulkDisable) btnLuckmailBulkDisable.disabled = loading || luckmailSelectedPurchaseIds.size === 0;
+  if (btnLuckmailBulkEnable) btnLuckmailBulkEnable.disabled = loading || luckmailSelectedPurchaseIds.size === 0;
+  if (summary && luckmailSummary) {
+    luckmailSummary.textContent = summary;
+  }
+}
+
+function renderLuckmailPurchases(purchases = []) {
+  if (!luckmailList || !luckmailSummary) return;
+
+  lastRenderedLuckmailPurchases = Array.isArray(purchases) ? purchases : [];
+  pruneLuckmailSelection(lastRenderedLuckmailPurchases);
+  luckmailList.innerHTML = '';
+
+  if (!lastRenderedLuckmailPurchases.length) {
+    luckmailSelectedPurchaseIds.clear();
+    luckmailList.innerHTML = '<div class="luckmail-empty">未找到 openai 项目的 LuckMail 邮箱。</div>';
+    luckmailSummary.textContent = '加载已购邮箱后可在这里管理 openai 项目的 LuckMail 邮箱。';
+    if (btnLuckmailDisableUsed) btnLuckmailDisableUsed.disabled = true;
+    updateLuckmailBulkUI([]);
+    return;
+  }
+
+  const usedCount = lastRenderedLuckmailPurchases.filter((purchase) => purchase.used).length;
+  const reusableCount = lastRenderedLuckmailPurchases.filter((purchase) => purchase.reusable).length;
+  const disableUsedCount = lastRenderedLuckmailPurchases.filter((purchase) => purchase.used && !purchase.preserved && !purchase.disabled).length;
+  luckmailSummary.textContent = `已加载 ${lastRenderedLuckmailPurchases.length} 个 openai 邮箱,其中 ${reusableCount} 个可复用,${usedCount} 个已本地标记为已用。`;
+  if (btnLuckmailDisableUsed) {
+    btnLuckmailDisableUsed.textContent = `禁用已用${disableUsedCount > 0 ? `(${disableUsedCount})` : ''}`;
+    btnLuckmailDisableUsed.disabled = disableUsedCount === 0;
+  }
+
+  const visiblePurchases = getFilteredLuckmailPurchases(lastRenderedLuckmailPurchases);
+  if (!visiblePurchases.length) {
+    luckmailList.innerHTML = '<div class="luckmail-empty">没有匹配当前筛选条件的 LuckMail 邮箱。</div>';
+    updateLuckmailBulkUI([]);
+    return;
+  }
+
+  for (const purchase of visiblePurchases) {
+    const purchaseId = String(purchase.id);
+    const item = document.createElement('div');
+    item.className = `luckmail-item${purchase.current ? ' is-current' : ''}`;
+    item.innerHTML = `
+      <input class="luckmail-item-check" type="checkbox" data-action="select" ${luckmailSelectedPurchaseIds.has(purchaseId) ? 'checked' : ''} />
+      <div class="luckmail-item-main">
+        <div class="luckmail-item-email-row">
+          <div class="luckmail-item-email">${escapeHtml(purchase.email_address || '(未知邮箱)')}</div>
+          <button
+            class="hotmail-copy-btn"
+            type="button"
+            data-action="copy-email"
+            title="复制邮箱"
+            aria-label="复制邮箱 ${escapeHtml(purchase.email_address || '')}"
+          >${COPY_ICON}</button>
+        </div>
+        <div class="luckmail-item-meta">
+          <span class="luckmail-tag">${escapeHtml(normalizeLuckmailProjectName(purchase.project_name) || 'openai')}</span>
+          ${purchase.reusable ? '<span class="luckmail-tag active">可复用</span>' : ''}
+          ${purchase.current ? '<span class="luckmail-tag current">当前</span>' : ''}
+          ${purchase.used ? '<span class="luckmail-tag used">已用</span>' : ''}
+          ${purchase.preserved ? '<span class="luckmail-tag">保留</span>' : ''}
+          ${purchase.disabled ? '<span class="luckmail-tag disabled">已禁用</span>' : ''}
+          ${purchase.tag_name && normalizeLuckmailSearchText(purchase.tag_name) !== normalizeLuckmailSearchText(getLuckmailPreserveTagName())
+            ? `<span class="luckmail-tag">${escapeHtml(purchase.tag_name)}</span>`
+            : ''}
+        </div>
+        <div class="luckmail-item-details">
+          <span>ID:${escapeHtml(String(purchase.id || ''))}</span>
+          <span>保修至:${escapeHtml(formatLuckmailDateTime(purchase.warranty_until))}</span>
+        </div>
+      </div>
+      <div class="luckmail-item-actions">
+        <button class="btn btn-outline btn-xs" type="button" data-action="use">使用此邮箱</button>
+        <button class="btn btn-outline btn-xs" type="button" data-action="toggle-used">${escapeHtml(purchase.used ? '标记未用' : '标记已用')}</button>
+        <button class="btn btn-outline btn-xs" type="button" data-action="toggle-preserved">${escapeHtml(purchase.preserved ? '取消保留' : '保留')}</button>
+        <button class="btn btn-outline btn-xs" type="button" data-action="toggle-disabled">${escapeHtml(purchase.disabled ? '启用' : '禁用')}</button>
+      </div>
+    `;
+
+    item.querySelector('[data-action="select"]').addEventListener('change', (event) => {
+      if (event.target.checked) {
+        luckmailSelectedPurchaseIds.add(purchaseId);
+      } else {
+        luckmailSelectedPurchaseIds.delete(purchaseId);
+      }
+      updateLuckmailBulkUI(visiblePurchases);
+    });
+    item.querySelector('[data-action="copy-email"]').addEventListener('click', async () => {
+      await copyTextToClipboard(purchase.email_address || '');
+      showToast('邮箱已复制', 'success', 1600);
+    });
+    item.querySelector('[data-action="use"]').addEventListener('click', async () => {
+      await selectSingleLuckmailPurchase(purchase);
+    });
+    item.querySelector('[data-action="toggle-used"]').addEventListener('click', async () => {
+      await setSingleLuckmailPurchaseUsedState(purchase, !purchase.used);
+    });
+    item.querySelector('[data-action="toggle-preserved"]').addEventListener('click', async () => {
+      await setSingleLuckmailPurchasePreservedState(purchase, !purchase.preserved);
+    });
+    item.querySelector('[data-action="toggle-disabled"]').addEventListener('click', async () => {
+      await setSingleLuckmailPurchaseDisabledState(purchase, !purchase.disabled);
+    });
+    luckmailList.appendChild(item);
+  }
+
+  updateLuckmailBulkUI(visiblePurchases);
+}
+
+async function refreshLuckmailPurchases(options = {}) {
+  const { silent = false } = options;
+  if (!luckmailSection || luckmailSection.style.display === 'none') {
+    return;
+  }
+
+  if (!silent) setLuckmailLoadingState(true, '正在加载 LuckMail openai 邮箱...');
+  try {
+    const response = await chrome.runtime.sendMessage({
+      type: 'LIST_LUCKMAIL_PURCHASES',
+      source: 'sidepanel',
+      payload: {},
+    });
+    if (response?.error) throw new Error(response.error);
+    renderLuckmailPurchases(response?.purchases || []);
+  } catch (err) {
+    luckmailSelectedPurchaseIds.clear();
+    if (luckmailList) {
+      luckmailList.innerHTML = '<div class="luckmail-empty">无法加载 LuckMail 邮箱列表。</div>';
+    }
+    if (luckmailSummary) {
+      luckmailSummary.textContent = err.message;
+    }
+    updateLuckmailBulkUI([]);
+    if (!silent) {
+      showToast(`LuckMail 邮箱列表加载失败:${err.message}`, 'error');
+    }
+  } finally {
+    setLuckmailLoadingState(false);
+  }
+}
+
+function queueLuckmailPurchaseRefresh() {
+  if (luckmailRefreshQueued) return;
+  luckmailRefreshQueued = true;
+  setTimeout(async () => {
+    luckmailRefreshQueued = false;
+    await refreshLuckmailPurchases({ silent: true });
+  }, 150);
+}
+
+async function selectSingleLuckmailPurchase(purchase) {
+  setLuckmailLoadingState(true, `正在切换到 ${purchase.email_address} ...`);
+  try {
+    const response = await chrome.runtime.sendMessage({
+      type: 'SELECT_LUCKMAIL_PURCHASE',
+      source: 'sidepanel',
+      payload: { purchaseId: purchase.id },
+    });
+    if (response?.error) throw new Error(response.error);
+    inputEmail.value = response?.purchase?.email_address || purchase.email_address || '';
+    showToast(`已切换当前 LuckMail 邮箱为 ${purchase.email_address}`, 'success', 2200);
+    await refreshLuckmailPurchases({ silent: true });
+  } catch (err) {
+    if (luckmailSummary) luckmailSummary.textContent = err.message;
+    showToast(`切换 LuckMail 邮箱失败:${err.message}`, 'error');
+  } finally {
+    setLuckmailLoadingState(false);
+  }
+}
+
+async function setSingleLuckmailPurchaseUsedState(purchase, used) {
+  setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的已用状态...`);
+  try {
+    const response = await chrome.runtime.sendMessage({
+      type: 'SET_LUCKMAIL_PURCHASE_USED_STATE',
+      source: 'sidepanel',
+      payload: { purchaseId: purchase.id, used },
+    });
+    if (response?.error) throw new Error(response.error);
+    showToast(`${purchase.email_address} 已${used ? '标记为已用' : '恢复为未用'}`, 'success', 2200);
+    await refreshLuckmailPurchases({ silent: true });
+  } catch (err) {
+    if (luckmailSummary) luckmailSummary.textContent = err.message;
+    showToast(`更新 LuckMail 已用状态失败:${err.message}`, 'error');
+  } finally {
+    setLuckmailLoadingState(false);
+  }
+}
+
+async function setSingleLuckmailPurchasePreservedState(purchase, preserved) {
+  setLuckmailLoadingState(true, `正在更新 ${purchase.email_address} 的保留状态...`);
+  try {
+    const response = await chrome.runtime.sendMessage({
+      type: 'SET_LUCKMAIL_PURCHASE_PRESERVED_STATE',
+      source: 'sidepanel',
+      payload: { purchaseId: purchase.id, preserved },
+    });
+    if (response?.error) throw new Error(response.error);
+    showToast(`${purchase.email_address} 已${preserved ? '设为保留' : '取消保留'}`, 'success', 2200);
+    await refreshLuckmailPurchases({ silent: true });
+  } catch (err) {
+    if (luckmailSummary) luckmailSummary.textContent = err.message;
+    showToast(`更新 LuckMail 保留状态失败:${err.message}`, 'error');
+  } finally {
+    setLuckmailLoadingState(false);
+  }
+}
+
+async function setSingleLuckmailPurchaseDisabledState(purchase, disabled) {
+  setLuckmailLoadingState(true, `正在${disabled ? '禁用' : '启用'} ${purchase.email_address} ...`);
+  try {
+    const response = await chrome.runtime.sendMessage({
+      type: 'SET_LUCKMAIL_PURCHASE_DISABLED_STATE',
+      source: 'sidepanel',
+      payload: { purchaseId: purchase.id, disabled },
+    });
+    if (response?.error) throw new Error(response.error);
+    showToast(`${purchase.email_address} 已${disabled ? '禁用' : '启用'}`, 'success', 2200);
+    await refreshLuckmailPurchases({ silent: true });
+  } catch (err) {
+    if (luckmailSummary) luckmailSummary.textContent = err.message;
+    showToast(`更新 LuckMail 禁用状态失败:${err.message}`, 'error');
+  } finally {
+    setLuckmailLoadingState(false);
+  }
+}
+
+async function runBulkLuckmailAction(action) {
+  const selectedIds = lastRenderedLuckmailPurchases
+    .filter((purchase) => luckmailSelectedPurchaseIds.has(String(purchase.id)))
+    .map((purchase) => purchase.id);
+  if (!selectedIds.length) {
+    updateLuckmailBulkUI();
+    return;
+  }
+
+  const actionLabelMap = {
+    used: '标记已用',
+    unused: '标记未用',
+    preserve: '保留',
+    unpreserve: '取消保留',
+    disable: '禁用',
+    enable: '启用',
+  };
+
+  setLuckmailLoadingState(true, `正在批量${actionLabelMap[action] || '处理'} LuckMail 邮箱...`);
+  try {
+    const response = await chrome.runtime.sendMessage({
+      type: 'BATCH_UPDATE_LUCKMAIL_PURCHASES',
+      source: 'sidepanel',
+      payload: { action, ids: selectedIds },
+    });
+    if (response?.error) throw new Error(response.error);
+    showToast(`已批量${actionLabelMap[action] || '处理'} ${selectedIds.length} 个 LuckMail 邮箱`, 'success', 2400);
+    await refreshLuckmailPurchases({ silent: true });
+  } catch (err) {
+    if (luckmailSummary) luckmailSummary.textContent = err.message;
+    showToast(`批量处理 LuckMail 邮箱失败:${err.message}`, 'error');
+  } finally {
+    setLuckmailLoadingState(false);
+    updateLuckmailBulkUI();
+  }
+}
+
+async function disableUsedLuckmailPurchases() {
+  const confirmed = await openConfirmModal({
+    title: '禁用已用 LuckMail 邮箱',
+    message: '确认禁用所有本地已用且未保留的 openai LuckMail 邮箱吗?',
+    confirmLabel: '确认禁用',
+    confirmVariant: 'btn-danger',
+  });
+  if (!confirmed) {
+    return;
+  }
+
+  setLuckmailLoadingState(true, '正在禁用已用 LuckMail 邮箱...');
+  try {
+    const response = await chrome.runtime.sendMessage({
+      type: 'DISABLE_USED_LUCKMAIL_PURCHASES',
+      source: 'sidepanel',
+      payload: {},
+    });
+    if (response?.error) throw new Error(response.error);
+    const disabledCount = Array.isArray(response?.disabledIds) ? response.disabledIds.length : 0;
+    showToast(`已禁用 ${disabledCount} 个 LuckMail 邮箱`, disabledCount > 0 ? 'success' : 'info', 2400);
+    await refreshLuckmailPurchases({ silent: true });
+  } catch (err) {
+    if (luckmailSummary) luckmailSummary.textContent = err.message;
+    showToast(`禁用已用 LuckMail 邮箱失败:${err.message}`, 'error');
+  } finally {
+    setLuckmailLoadingState(false);
+  }
+}
+
 function getMailProviderLoginConfig(provider = selectMailProvider.value) {
   return MAIL_PROVIDER_LOGIN_CONFIGS[String(provider || '').trim()] || null;
 }
@@ -1567,6 +2054,17 @@ function isCurrentEmailManagedByHotmail(state = latestState) {
   return inputEmailValue === hotmailEmail || stateEmailValue === hotmailEmail;
 }
 
+function isCurrentEmailManagedByLuckmail(state = latestState) {
+  const luckmailEmail = getCurrentLuckmailEmail(state);
+  if (!luckmailEmail) {
+    return false;
+  }
+
+  const inputEmailValue = String(inputEmail.value || '').trim();
+  const stateEmailValue = String(state?.email || '').trim();
+  return inputEmailValue === luckmailEmail || stateEmailValue === luckmailEmail;
+}
+
 function isCurrentEmailManagedByGeneratedAlias(provider = latestState?.mailProvider, state = latestState) {
   const normalizedProvider = String(provider || '').trim();
   if (!usesGeneratedAliasMailProvider(normalizedProvider)) {
@@ -1806,8 +2304,9 @@ function updateMailProviderUI() {
   const useGeneratedAlias = usesGeneratedAliasMailProvider(selectMailProvider.value);
   const useInbucket = selectMailProvider.value === 'inbucket';
   const useHotmail = selectMailProvider.value === 'hotmail-api';
+  const useLuckmail = isLuckmailProvider();
   const useCustomEmail = isCustomMailProvider();
-  const useEmailGenerator = !useHotmail && !useGeneratedAlias && !useCustomEmail;
+  const useEmailGenerator = !useHotmail && !useLuckmail && !useGeneratedAlias && !useCustomEmail;
   updateMailLoginButtonState();
   rowEmailPrefix.style.display = useGeneratedAlias ? '' : 'none';
   const hotmailServiceMode = getSelectedHotmailServiceMode();
@@ -1829,9 +2328,12 @@ function updateMailProviderUI() {
   if (hotmailSection) {
     hotmailSection.style.display = useHotmail ? '' : 'none';
   }
+  if (luckmailSection) {
+    luckmailSection.style.display = useLuckmail ? '' : 'none';
+  }
   labelEmailPrefix.textContent = '邮箱前缀';
   inputEmailPrefix.placeholder = '例如 abc';
-  selectEmailGenerator.disabled = useHotmail || useGeneratedAlias || useCustomEmail;
+  selectEmailGenerator.disabled = useHotmail || useLuckmail || useGeneratedAlias || useCustomEmail;
   if (rowHotmailServiceMode) {
     rowHotmailServiceMode.style.display = useHotmail ? '' : 'none';
   }
@@ -1841,27 +2343,36 @@ function updateMailProviderUI() {
   if (rowHotmailLocalBaseUrl) {
     rowHotmailLocalBaseUrl.style.display = useHotmail && hotmailServiceMode === HOTMAIL_SERVICE_MODE_LOCAL ? '' : 'none';
   }
-  btnFetchEmail.hidden = useHotmail || useCustomEmail;
-  inputEmail.readOnly = useHotmail || useGeneratedAlias;
+  btnFetchEmail.hidden = useHotmail || useLuckmail || useCustomEmail;
+  inputEmail.readOnly = useHotmail || useLuckmail || useGeneratedAlias;
   const uiCopy = useCustomEmail ? getCustomMailProviderUiCopy() : getEmailGeneratorUiCopy();
   inputEmail.placeholder = useHotmail
     ? '由 Hotmail 账号池自动分配'
-    : (use2925 ? '步骤 3 自动生成 2925 邮箱并回填' : uiCopy.placeholder);
-  btnFetchEmail.disabled = useGeneratedAlias || useCustomEmail || isAutoRunLockedPhase();
+    : (useLuckmail
+      ? '步骤 3 自动购买 LuckMail 邮箱并回填'
+      : (use2925 ? '步骤 3 自动生成 2925 邮箱并回填' : uiCopy.placeholder));
+  btnFetchEmail.disabled = useGeneratedAlias || useLuckmail || useCustomEmail || isAutoRunLockedPhase();
   if (!btnFetchEmail.disabled) {
     btnFetchEmail.textContent = uiCopy.buttonLabel;
   }
   if (autoHintText) {
     autoHintText.textContent = useHotmail
       ? '请先校验并选择一个 Hotmail 账号'
+      : (useLuckmail
+        ? '步骤 3 会自动购买 LuckMail 邮箱并用于收码'
       : (useGeneratedAlias
         ? '步骤 3 会自动生成邮箱,无需手动获取'
-        : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : '先自动获取邮箱,或手动粘贴邮箱后再继续'));
+        : (useCustomEmail ? '请先填写自定义注册邮箱,成功一轮后会自动清空' : '先自动获取邮箱,或手动粘贴邮箱后再继续')));
   }
   if (useHotmail) {
     inputEmail.value = getCurrentHotmailEmail();
+  } else if (useLuckmail) {
+    inputEmail.value = getCurrentLuckmailEmail();
   }
   renderHotmailAccounts();
+  if (useLuckmail) {
+    renderLuckmailPurchases(lastRenderedLuckmailPurchases);
+  }
 }
 
 async function saveCloudflareDomainSettings(domains, activeDomain, options = {}) {
@@ -2397,7 +2908,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
           syncLatestState({ customPassword: inputPassword.value });
         }
         let email = inputEmail.value.trim();
-        if (selectMailProvider.value === 'hotmail-api') {
+        if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) {
           const response = await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
           if (response?.error) {
             throw new Error(response.error);
@@ -2444,7 +2955,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
 });
 
 btnFetchEmail.addEventListener('click', async () => {
-  if (selectMailProvider.value === 'hotmail-api' || isCustomMailProvider()) {
+  if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider() || isCustomMailProvider()) {
     return;
   }
   await fetchGeneratedEmail().catch(() => { });
@@ -2908,7 +3419,13 @@ btnReset.addEventListener('click', async () => {
   }
 
   await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
-  syncLatestState({ stepStatuses: STEP_DEFAULT_STATUSES, currentHotmailAccountId: null, email: null });
+  syncLatestState({
+    stepStatuses: STEP_DEFAULT_STATUSES,
+    currentHotmailAccountId: null,
+    currentLuckmailPurchase: null,
+    currentLuckmailMailCursor: null,
+    email: null,
+  });
   syncAutoRunState({
     autoRunning: false,
     autoRunPhase: 'idle',
@@ -2937,6 +3454,10 @@ btnReset.addEventListener('click', async () => {
   updateButtonStates();
   updateProgressCounter();
   renderHotmailAccounts();
+  renderLuckmailPurchases(lastRenderedLuckmailPurchases);
+  if (isLuckmailProvider()) {
+    queueLuckmailPurchaseRefresh();
+  }
 });
 
 // Clear log
@@ -2946,7 +3467,7 @@ btnClearLog.addEventListener('click', () => {
 
 // Save settings on change
 inputEmail.addEventListener('change', async () => {
-  if (selectMailProvider.value === 'hotmail-api') {
+  if (selectMailProvider.value === 'hotmail-api' || isLuckmailProvider()) {
     return;
   }
   const email = inputEmail.value.trim();
@@ -2991,6 +3512,73 @@ inputVpsPassword.addEventListener('blur', () => {
   });
 });
 
+[inputLuckmailApiKey, inputLuckmailBaseUrl, inputLuckmailDomain].forEach((input) => {
+  input?.addEventListener('input', () => {
+    markSettingsDirty(true);
+    scheduleSettingsAutoSave();
+  });
+  input?.addEventListener('blur', () => {
+    saveSettings({ silent: true }).catch(() => { });
+  });
+});
+
+selectLuckmailEmailType?.addEventListener('change', () => {
+  markSettingsDirty(true);
+  saveSettings({ silent: true }).catch(() => { });
+});
+
+inputLuckmailSearch?.addEventListener('input', (event) => {
+  luckmailSearchTerm = event.target.value || '';
+  renderLuckmailPurchases(lastRenderedLuckmailPurchases);
+});
+
+selectLuckmailFilter?.addEventListener('change', (event) => {
+  luckmailFilterMode = String(event.target.value || 'all').trim() || 'all';
+  renderLuckmailPurchases(lastRenderedLuckmailPurchases);
+});
+
+checkboxLuckmailSelectAll?.addEventListener('change', () => {
+  const visiblePurchases = getFilteredLuckmailPurchases();
+  if (checkboxLuckmailSelectAll.checked) {
+    visiblePurchases.forEach((purchase) => luckmailSelectedPurchaseIds.add(String(purchase.id)));
+  } else {
+    visiblePurchases.forEach((purchase) => luckmailSelectedPurchaseIds.delete(String(purchase.id)));
+  }
+  renderLuckmailPurchases(lastRenderedLuckmailPurchases);
+});
+
+btnLuckmailRefresh?.addEventListener('click', async () => {
+  await refreshLuckmailPurchases();
+});
+
+btnLuckmailDisableUsed?.addEventListener('click', async () => {
+  await disableUsedLuckmailPurchases();
+});
+
+btnLuckmailBulkUsed?.addEventListener('click', async () => {
+  await runBulkLuckmailAction('used');
+});
+
+btnLuckmailBulkUnused?.addEventListener('click', async () => {
+  await runBulkLuckmailAction('unused');
+});
+
+btnLuckmailBulkPreserve?.addEventListener('click', async () => {
+  await runBulkLuckmailAction('preserve');
+});
+
+btnLuckmailBulkUnpreserve?.addEventListener('click', async () => {
+  await runBulkLuckmailAction('unpreserve');
+});
+
+btnLuckmailBulkDisable?.addEventListener('click', async () => {
+  await runBulkLuckmailAction('disable');
+});
+
+btnLuckmailBulkEnable?.addEventListener('click', async () => {
+  await runBulkLuckmailAction('enable');
+});
+
 inputPassword.addEventListener('input', () => {
   markSettingsDirty(true);
   updateButtonStates();
@@ -3007,12 +3595,18 @@ selectMailProvider.addEventListener('change', async () => {
   const leavingHotmail = previousProvider === 'hotmail-api'
     && nextProvider !== 'hotmail-api'
     && isCurrentEmailManagedByHotmail();
+  const leavingLuckmail = previousProvider === LUCKMAIL_PROVIDER
+    && nextProvider !== LUCKMAIL_PROVIDER
+    && isCurrentEmailManagedByLuckmail();
   const leavingGeneratedAlias = previousProvider !== nextProvider
     && usesGeneratedAliasMailProvider(previousProvider)
     && isCurrentEmailManagedByGeneratedAlias(previousProvider);
-  if (leavingHotmail || leavingGeneratedAlias) {
+  if (leavingHotmail || leavingLuckmail || leavingGeneratedAlias) {
     await clearRegistrationEmail({ silent: true }).catch(() => { });
   }
+  if (nextProvider === LUCKMAIL_PROVIDER) {
+    queueLuckmailPurchaseRefresh();
+  }
   markSettingsDirty(true);
   saveSettings({ silent: true }).catch(() => { });
 });
@@ -3308,6 +3902,25 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
           inputEmail.value = getCurrentHotmailEmail();
         }
       }
+      if (message.payload.luckmailApiKey !== undefined) {
+        inputLuckmailApiKey.value = message.payload.luckmailApiKey || '';
+      }
+      if (message.payload.luckmailBaseUrl !== undefined) {
+        inputLuckmailBaseUrl.value = normalizeLuckmailBaseUrl(message.payload.luckmailBaseUrl);
+      }
+      if (message.payload.luckmailEmailType !== undefined) {
+        selectLuckmailEmailType.value = normalizeLuckmailEmailType(message.payload.luckmailEmailType);
+      }
+      if (message.payload.luckmailDomain !== undefined) {
+        inputLuckmailDomain.value = message.payload.luckmailDomain || '';
+      }
+      if (message.payload.luckmailUsedPurchases !== undefined && isLuckmailProvider()) {
+        queueLuckmailPurchaseRefresh();
+      }
+      if (message.payload.currentLuckmailPurchase !== undefined && isLuckmailProvider()) {
+        inputEmail.value = getCurrentLuckmailEmail();
+        queueLuckmailPurchaseRefresh();
+      }
       if (message.payload.autoRunSkipFailures !== undefined) {
         inputAutoSkipFailures.checked = Boolean(message.payload.autoRunSkipFailures);
         updateFallbackThreadIntervalInputState();

+ 568 - 0
tests/background-luckmail.test.js

@@ -0,0 +1,568 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('background.js', 'utf8');
+
+function extractFunction(name) {
+  const markers = [`async function ${name}(`, `function ${name}(`];
+  const start = markers
+    .map((marker) => source.indexOf(marker))
+    .find((index) => index >= 0);
+  if (start < 0) {
+    throw new Error(`missing function ${name}`);
+  }
+
+  let parenDepth = 0;
+  let signatureEnded = false;
+  let braceStart = -1;
+  for (let i = start; i < source.length; i += 1) {
+    const ch = source[i];
+    if (ch === '(') {
+      parenDepth += 1;
+    } else if (ch === ')') {
+      parenDepth -= 1;
+      if (parenDepth === 0) {
+        signatureEnded = true;
+      }
+    } else if (ch === '{' && signatureEnded) {
+      braceStart = i;
+      break;
+    }
+  }
+
+  if (braceStart < 0) {
+    throw new Error(`missing body for function ${name}`);
+  }
+
+  let depth = 0;
+  let end = braceStart;
+  for (; end < source.length; end += 1) {
+    const ch = source[end];
+    if (ch === '{') depth += 1;
+    if (ch === '}') {
+      depth -= 1;
+      if (depth === 0) {
+        end += 1;
+        break;
+      }
+    }
+  }
+
+  return source.slice(start, end);
+}
+
+test('ensureLuckmailPurchaseForFlow buys openai mailbox and defaults email type to ms_graph', async () => {
+  const bundle = [
+    extractFunction('getLuckmailSessionConfig'),
+    extractFunction('getCurrentLuckmailPurchase'),
+    extractFunction('ensureLuckmailPurchaseForFlow'),
+  ].join('\n');
+
+  const factory = new Function('initialState', `
+let currentState = { ...initialState };
+const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
+const purchaseCalls = [];
+const activateCalls = [];
+
+function normalizeLuckmailBaseUrl(value) {
+  return String(value || '').trim() || 'https://mails.luckyous.com';
+}
+function normalizeLuckmailEmailType(value) {
+  return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
+    ? String(value || '').trim()
+    : 'ms_graph';
+}
+function normalizeLuckmailPurchase(value) {
+  return value;
+}
+function normalizeLuckmailPurchases(value) {
+  return value.purchases || [];
+}
+async function getState() {
+  return currentState;
+}
+function createLuckmailClient() {
+  return {
+    user: {
+      async purchaseEmails(projectCode, quantity, options) {
+        purchaseCalls.push({ projectCode, quantity, options });
+        return {
+          purchases: [{ id: 15, email_address: 'demo@outlook.com', token: 'tok-1' }],
+        };
+      },
+    },
+  };
+}
+async function findReusableLuckmailPurchaseForFlow() {
+  return null;
+}
+async function activateLuckmailPurchaseForFlow(state, client, purchase, options) {
+  activateCalls.push({ state, purchase, options });
+  currentState.currentLuckmailPurchase = purchase;
+  currentState.email = purchase.email_address;
+  return purchase;
+}
+
+${bundle}
+
+return {
+  ensureLuckmailPurchaseForFlow,
+  snapshot() {
+    return { currentState, purchaseCalls, activateCalls };
+  },
+};
+`);
+
+  const api = factory({
+    luckmailApiKey: 'sk-test',
+    luckmailBaseUrl: '',
+    luckmailEmailType: '',
+    luckmailDomain: '',
+    currentLuckmailPurchase: null,
+    email: null,
+  });
+
+  const purchase = await api.ensureLuckmailPurchaseForFlow();
+  const snapshot = api.snapshot();
+
+  assert.equal(purchase.email_address, 'demo@outlook.com');
+  assert.deepStrictEqual(snapshot.purchaseCalls, [{
+    projectCode: 'openai',
+    quantity: 1,
+    options: {
+      emailType: 'ms_graph',
+      domain: undefined,
+    },
+  }]);
+  assert.equal(snapshot.activateCalls[0].options.initializeCursor, false);
+  assert.equal(snapshot.currentState.email, 'demo@outlook.com');
+});
+
+test('ensureLuckmailPurchaseForFlow reuses reusable openai mailbox before buying a new one', async () => {
+  const bundle = [
+    extractFunction('getLuckmailSessionConfig'),
+    extractFunction('getCurrentLuckmailPurchase'),
+    extractFunction('ensureLuckmailPurchaseForFlow'),
+  ].join('\n');
+
+  const factory = new Function('initialState', `
+let currentState = { ...initialState };
+const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
+const purchaseCalls = [];
+const activateCalls = [];
+
+function normalizeLuckmailBaseUrl(value) {
+  return String(value || '').trim() || 'https://mails.luckyous.com';
+}
+function normalizeLuckmailEmailType(value) {
+  return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
+    ? String(value || '').trim()
+    : 'ms_graph';
+}
+function normalizeLuckmailPurchase(value) {
+  return value;
+}
+function normalizeLuckmailPurchases(value) {
+  return value.purchases || [];
+}
+async function getState() {
+  return currentState;
+}
+function createLuckmailClient() {
+  return {
+    user: {
+      async purchaseEmails(projectCode, quantity, options) {
+        purchaseCalls.push({ projectCode, quantity, options });
+        return { purchases: [] };
+      },
+    },
+  };
+}
+async function findReusableLuckmailPurchaseForFlow() {
+  return {
+    id: 99,
+    email_address: 'reuse@outlook.com',
+    token: 'tok-reuse',
+  };
+}
+async function activateLuckmailPurchaseForFlow(state, client, purchase, options) {
+  activateCalls.push({ state, purchase, options });
+  currentState.currentLuckmailPurchase = purchase;
+  currentState.email = purchase.email_address;
+  return purchase;
+}
+
+${bundle}
+
+return {
+  ensureLuckmailPurchaseForFlow,
+  snapshot() {
+    return { currentState, purchaseCalls, activateCalls };
+  },
+};
+`);
+
+  const api = factory({
+    luckmailApiKey: 'sk-test',
+    luckmailBaseUrl: 'https://mails.luckyous.com',
+    luckmailEmailType: 'ms_imap',
+    luckmailDomain: 'outlook.com',
+    currentLuckmailPurchase: null,
+    email: null,
+  });
+
+  const purchase = await api.ensureLuckmailPurchaseForFlow();
+  const snapshot = api.snapshot();
+
+  assert.equal(purchase.id, 99);
+  assert.deepStrictEqual(snapshot.purchaseCalls, []);
+  assert.equal(snapshot.activateCalls[0].options.initializeCursor, true);
+  assert.match(snapshot.activateCalls[0].options.logMessage, /已复用 openai 邮箱/);
+});
+
+test('activateLuckmailPurchaseForFlow builds baseline cursor from existing mails when reusing mailbox', async () => {
+  const bundle = extractFunction('activateLuckmailPurchaseForFlow');
+
+  const factory = new Function(`
+let currentPurchase = null;
+let currentCursor = null;
+let currentEmail = null;
+const buildCalls = [];
+
+function normalizeLuckmailPurchase(value) {
+  return value;
+}
+async function setLuckmailPurchaseState(value) {
+  currentPurchase = value;
+}
+async function setLuckmailMailCursorState(value) {
+  currentCursor = value;
+}
+async function setEmailState(value) {
+  currentEmail = value;
+}
+async function addLog() {}
+function buildLuckmailBaselineCursor(mails) {
+  buildCalls.push(mails);
+  return { messageId: 'mail-new', receivedAt: '2026-04-14 13:32:05' };
+}
+
+${bundle}
+
+return {
+  activateLuckmailPurchaseForFlow,
+  snapshot() {
+    return { currentPurchase, currentCursor, currentEmail, buildCalls };
+  },
+};
+`);
+
+  const api = factory();
+  const client = {
+    user: {
+      async getTokenMails() {
+        return {
+          mails: [
+            { message_id: 'mail-old', received_at: '2026-04-14 13:31:15' },
+            { message_id: 'mail-new', received_at: '2026-04-14 13:32:05' },
+          ],
+        };
+      },
+    },
+  };
+
+  await api.activateLuckmailPurchaseForFlow({}, client, {
+    id: 5,
+    email_address: 'reuse@outlook.com',
+    token: 'tok-reuse',
+  }, {
+    initializeCursor: true,
+    logMessage: 'reuse',
+  });
+
+  const snapshot = api.snapshot();
+  assert.equal(snapshot.currentPurchase.id, 5);
+  assert.deepStrictEqual(snapshot.currentCursor, {
+    messageId: 'mail-new',
+    receivedAt: '2026-04-14 13:32:05',
+  });
+  assert.equal(snapshot.currentEmail, 'reuse@outlook.com');
+  assert.equal(snapshot.buildCalls.length, 1);
+});
+
+test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
+  const bundle = extractFunction('listLuckmailPurchasesByProject');
+
+  const factory = new Function(`
+const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
+function normalizeLuckmailProjectName(value) {
+  return String(value || '').trim().toLowerCase();
+}
+function isLuckmailPurchaseForProject(purchase, projectCode) {
+  return normalizeLuckmailProjectName(purchase.project_name || purchase.project) === normalizeLuckmailProjectName(projectCode);
+}
+async function getAllLuckmailPurchases() {
+  return [
+    { id: 1, project_name: 'OpenAi' },
+    { id: 2, project_name: 'other' },
+    { id: 3, project: 'openai' },
+  ];
+}
+
+${bundle}
+
+return { listLuckmailPurchasesByProject };
+`);
+
+  const api = factory();
+  const result = await api.listLuckmailPurchasesByProject({}, { projectCode: 'openai' });
+  assert.deepStrictEqual(result.map((item) => item.id), [1, 3]);
+});
+
+test('disableUsedLuckmailPurchases only disables locally used and non-preserved openai mailboxes', async () => {
+  const bundle = extractFunction('disableUsedLuckmailPurchases');
+
+  const factory = new Function(`
+let clearedOptions = null;
+const disabledCalls = [];
+const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
+
+function normalizeLuckmailPurchaseId(value) {
+  const numeric = Number(value);
+  return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
+}
+async function ensureManualInteractionAllowed() {
+  return {
+    luckmailUsedPurchases: { 1: true, 2: true, 3: true },
+    luckmailPreserveTagId: 9,
+    luckmailPreserveTagName: '保留',
+    mailProvider: 'luckmail-api',
+  };
+}
+function getLuckmailUsedPurchases(state) {
+  return state.luckmailUsedPurchases;
+}
+function getLuckmailPreserveTagInfo(state) {
+  return {
+    id: state.luckmailPreserveTagId,
+    name: state.luckmailPreserveTagName,
+  };
+}
+function isLuckmailPurchasePreserved(purchase, options) {
+  return purchase.tag_id === options.preserveTagId || purchase.tag_name === options.preserveTagName;
+}
+function createLuckmailClient() {
+  return {
+    user: {
+      async batchSetPurchaseDisabled(ids, disabled) {
+        disabledCalls.push({ ids, disabled });
+      },
+    },
+  };
+}
+async function listLuckmailPurchasesByProject() {
+  return [
+    { id: 1, email_address: 'used-1@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' },
+    { id: 2, email_address: 'preserved@outlook.com', user_disabled: 0, tag_id: 9, tag_name: '保留' },
+    { id: 3, email_address: 'already-disabled@outlook.com', user_disabled: 1, tag_id: 0, tag_name: '' },
+    { id: 4, email_address: 'unused@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' },
+  ];
+}
+async function getState() {
+  return {
+    currentLuckmailPurchase: { id: 1 },
+    mailProvider: 'luckmail-api',
+  };
+}
+function getCurrentLuckmailPurchase(state) {
+  return state.currentLuckmailPurchase;
+}
+function isLuckmailProvider(state) {
+  return state.mailProvider === 'luckmail-api';
+}
+async function clearLuckmailRuntimeState(options) {
+  clearedOptions = options;
+}
+async function addLog() {}
+
+${bundle}
+
+return {
+  disableUsedLuckmailPurchases,
+  snapshot() {
+    return { disabledCalls, clearedOptions };
+  },
+};
+`);
+
+  const api = factory();
+  const result = await api.disableUsedLuckmailPurchases();
+  const snapshot = api.snapshot();
+
+  assert.deepStrictEqual(result.disabledIds, [1]);
+  assert.deepStrictEqual(snapshot.disabledCalls, [{ ids: [1], disabled: 1 }]);
+  assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true });
+});
+
+test('resetState preserves LuckMail session config, used map, and preserve tag cache while clearing runtime purchase state', async () => {
+  const bundle = extractFunction('resetState');
+
+  const factory = new Function([
+    'let cleared = false;',
+    'let storedPayload = null;',
+    "const LOG_PREFIX = '[test]';",
+    "const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';",
+    'const DEFAULT_STATE = {',
+    "  luckmailApiKey: '',",
+    "  luckmailBaseUrl: 'https://mails.luckyous.com',",
+    "  luckmailEmailType: 'ms_graph',",
+    "  luckmailDomain: '',",
+    '  luckmailUsedPurchases: {},',
+    '  luckmailPreserveTagId: 0,',
+    "  luckmailPreserveTagName: '保留',",
+    "  currentLuckmailPurchase: { token: 'stale' },",
+    "  currentLuckmailMailCursor: { messageId: 'stale' },",
+    '  email: null,',
+    '};',
+    'function normalizeLuckmailBaseUrl(value) {',
+    "  const normalized = String(value || '').trim() || 'https://mails.luckyous.com';",
+    "  return normalized.replace(/\\/$/, '');",
+    '}',
+    'function normalizeLuckmailEmailType(value) {',
+    "  return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())",
+    "    ? String(value || '').trim()",
+    "    : 'ms_graph';",
+    '}',
+    'function normalizeLuckmailUsedPurchases(value) {',
+    '  return value || {};',
+    '}',
+    'async function getPersistedSettings() {',
+    "  return { mailProvider: '163' };",
+    '}',
+    'const chrome = {',
+    '  storage: {',
+    '    session: {',
+    '      async get() {',
+    '        return {',
+    "          seenCodes: ['seen-1'],",
+    "          seenInbucketMailIds: ['mail-1'],",
+    "          accounts: [{ email: 'saved@example.com' }],",
+    "          tabRegistry: { foo: { tabId: 1 } },",
+    "          sourceLastUrls: { foo: 'https://example.com' },",
+    "          luckmailApiKey: 'sk-session',",
+    "          luckmailBaseUrl: 'https://demo.example.com/',",
+    "          luckmailEmailType: 'ms_imap',",
+    "          luckmailDomain: 'outlook.com',",
+    "          luckmailUsedPurchases: { 88: true },",
+    '          luckmailPreserveTagId: 9,',
+    "          luckmailPreserveTagName: '保留',",
+    '        };',
+    '      },',
+    '      async clear() {',
+    '        cleared = true;',
+    '      },',
+    '      async set(payload) {',
+    '        storedPayload = payload;',
+    '      },',
+    '    },',
+    '  },',
+    '};',
+    bundle,
+    'return {',
+    '  resetState,',
+    '  snapshot() {',
+    '    return { cleared, storedPayload };',
+    '  },',
+    '};',
+  ].join('\n'));
+
+  const api = factory();
+  await api.resetState();
+  const snapshot = api.snapshot();
+
+  assert.equal(snapshot.cleared, true);
+  assert.equal(snapshot.storedPayload.luckmailApiKey, 'sk-session');
+  assert.equal(snapshot.storedPayload.luckmailBaseUrl, 'https://demo.example.com');
+  assert.equal(snapshot.storedPayload.luckmailEmailType, 'ms_imap');
+  assert.equal(snapshot.storedPayload.luckmailDomain, 'outlook.com');
+  assert.deepStrictEqual(snapshot.storedPayload.luckmailUsedPurchases, { 88: true });
+  assert.equal(snapshot.storedPayload.luckmailPreserveTagId, 9);
+  assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
+  assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
+  assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
+});
+
+test('handleStepData step 9 marks current LuckMail purchase as used and clears runtime state', async () => {
+  const bundle = extractFunction('handleStepData');
+
+  const factory = new Function(`
+let clearedOptions = null;
+let usedMarker = null;
+const logs = [];
+
+async function closeLocalhostCallbackTabs() {}
+async function getState() {
+  return {
+    mailProvider: 'luckmail-api',
+    currentHotmailAccountId: null,
+    currentLuckmailPurchase: {
+      id: 123,
+      email_address: 'demo@outlook.com',
+    },
+    email: 'demo@outlook.com',
+  };
+}
+function getCurrentLuckmailPurchase(state) {
+  return state.currentLuckmailPurchase;
+}
+function isHotmailProvider() {
+  return false;
+}
+async function patchHotmailAccount() {}
+function isLuckmailProvider(state) {
+  return state.mailProvider === 'luckmail-api';
+}
+async function setLuckmailPurchaseUsedState(purchaseId, used) {
+  usedMarker = { purchaseId, used };
+}
+async function clearLuckmailRuntimeState(options) {
+  clearedOptions = options;
+}
+async function addLog(message, level) {
+  logs.push({ message, level });
+}
+function buildLocalhostCleanupPrefix() {
+  return '';
+}
+async function closeTabsByUrlPrefix() {}
+function shouldUseCustomRegistrationEmail() {
+  return false;
+}
+async function setEmailStateSilently() {}
+async function setState() {}
+function broadcastDataUpdate() {}
+function isLocalhostOAuthCallbackUrl() {
+  return true;
+}
+
+${bundle}
+
+return {
+  handleStepData,
+  snapshot() {
+    return { clearedOptions, usedMarker, logs };
+  },
+};
+`);
+
+  const api = factory();
+  await api.handleStepData(9, {
+    localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
+  });
+
+  const snapshot = api.snapshot();
+  assert.deepStrictEqual(snapshot.usedMarker, { purchaseId: 123, used: true });
+  assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true });
+  assert.equal(snapshot.logs.at(-1).message, '当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。');
+});

+ 261 - 0
tests/luckmail-utils.test.js

@@ -0,0 +1,261 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+
+const {
+  DEFAULT_LUCKMAIL_BASE_URL,
+  DEFAULT_LUCKMAIL_EMAIL_TYPE,
+  DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
+  buildLuckmailBaselineCursor,
+  buildLuckmailMailCursor,
+  extractLuckmailVerificationCode,
+  filterReusableLuckmailPurchases,
+  isLuckmailMailNewerThanCursor,
+  isLuckmailPurchaseForProject,
+  normalizeLuckmailBaseUrl,
+  normalizeLuckmailEmailType,
+  normalizeLuckmailProjectName,
+  normalizeLuckmailPurchaseListPage,
+  normalizeLuckmailTags,
+  normalizeLuckmailUsedPurchases,
+  normalizeTimestamp,
+  normalizeLuckmailTokenCode,
+  normalizeLuckmailTokenMail,
+  pickReusableLuckmailPurchase,
+  pickLuckmailVerificationMail,
+} = require('../luckmail-utils.js');
+
+test('normalizeLuckmailEmailType keeps supported values and falls back to ms_graph', () => {
+  assert.equal(normalizeLuckmailEmailType('self_built'), 'self_built');
+  assert.equal(normalizeLuckmailEmailType('ms_imap'), 'ms_imap');
+  assert.equal(normalizeLuckmailEmailType('ms_graph'), 'ms_graph');
+  assert.equal(normalizeLuckmailEmailType('google_variant'), 'google_variant');
+  assert.equal(normalizeLuckmailEmailType(''), DEFAULT_LUCKMAIL_EMAIL_TYPE);
+  assert.equal(normalizeLuckmailEmailType('unknown'), DEFAULT_LUCKMAIL_EMAIL_TYPE);
+});
+
+test('normalizeLuckmailBaseUrl trims invalid input to default base url', () => {
+  assert.equal(normalizeLuckmailBaseUrl(''), DEFAULT_LUCKMAIL_BASE_URL);
+  assert.equal(normalizeLuckmailBaseUrl('https://mails.luckyous.com/'), DEFAULT_LUCKMAIL_BASE_URL);
+  assert.equal(normalizeLuckmailBaseUrl('https://demo.example.com/api/'), 'https://demo.example.com/api');
+  assert.equal(normalizeLuckmailBaseUrl('notaurl'), DEFAULT_LUCKMAIL_BASE_URL);
+});
+
+test('normalizeLuckmailTokenCode and normalizeLuckmailTokenMail extract verification code', () => {
+  const tokenCode = normalizeLuckmailTokenCode({
+    email_address: 'demo@outlook.com',
+    project: 'openai',
+    has_new_mail: true,
+    verification_code: '123456',
+    mail: {
+      message_id: 'mail-1',
+      from: 'noreply@openai.com',
+      subject: 'Your ChatGPT code is 123456',
+      received_at: '2026-04-14T10:00:00Z',
+    },
+  });
+
+  assert.equal(tokenCode.verification_code, '123456');
+  assert.equal(tokenCode.mail.message_id, 'mail-1');
+  assert.equal(tokenCode.mail.verification_code, '123456');
+
+  const normalizedMail = normalizeLuckmailTokenMail({
+    message_id: 'mail-2',
+    from: 'noreply@openai.com',
+    subject: 'OpenAI security message',
+    body: 'Your verification code is 654321.',
+    received_at: '2026-04-14T10:01:00Z',
+  });
+
+  assert.equal(normalizedMail.verification_code, '654321');
+  assert.equal(extractLuckmailVerificationCode('你的验证码为 778899'), '778899');
+});
+
+test('normalizeLuckmailProjectName and isLuckmailPurchaseForProject match openai case-insensitively', () => {
+  assert.equal(normalizeLuckmailProjectName(' OpenAi '), 'openai');
+  assert.equal(isLuckmailPurchaseForProject({
+    id: 1,
+    email_address: 'demo@outlook.com',
+    token: 'tok-1',
+    project_name: 'OpenAi',
+  }, 'openai'), true);
+  assert.equal(isLuckmailPurchaseForProject({
+    id: 2,
+    email_address: 'other@example.com',
+    token: 'tok-2',
+    project: 'OtherProject',
+  }, 'openai'), false);
+});
+
+test('normalizeLuckmailPurchaseListPage and normalizeLuckmailTags normalize list payloads', () => {
+  const page = normalizeLuckmailPurchaseListPage({
+    list: [{
+      id: 3,
+      email_address: 'demo@outlook.com',
+      token: 'tok-3',
+      project_name: 'OpenAi',
+    }],
+    total: 4,
+    page: 2,
+    page_size: 1,
+  });
+  assert.equal(page.total, 4);
+  assert.equal(page.page, 2);
+  assert.equal(page.page_size, 1);
+  assert.equal(page.list[0].project_code, 'openai');
+
+  const tags = normalizeLuckmailTags([{
+    id: 9,
+    name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
+    limit_type: 0,
+  }]);
+  assert.deepEqual(tags[0], {
+    id: 9,
+    name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
+    remark: '',
+    limit_type: 0,
+    purchase_count: 0,
+    created_at: null,
+  });
+});
+
+test('normalizeLuckmailUsedPurchases keeps positive numeric keys only', () => {
+  assert.deepEqual(normalizeLuckmailUsedPurchases({
+    1: true,
+    foo: true,
+    '-2': true,
+    3: false,
+  }), {
+    1: true,
+    3: false,
+  });
+});
+
+test('pickReusableLuckmailPurchase only returns reusable openai purchase', () => {
+  const purchases = [{
+    id: 10,
+    email_address: 'used@outlook.com',
+    token: 'tok-used',
+    project_name: 'openai',
+  }, {
+    id: 11,
+    email_address: 'preserved@outlook.com',
+    token: 'tok-preserved',
+    project_name: 'OpenAi',
+    tag_name: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
+  }, {
+    id: 12,
+    email_address: 'disabled@outlook.com',
+    token: 'tok-disabled',
+    project_name: 'openai',
+    user_disabled: 1,
+  }, {
+    id: 13,
+    email_address: 'expired@outlook.com',
+    token: 'tok-expired',
+    project_name: 'openai',
+    warranty_until: '2026-04-14T09:00:00Z',
+  }, {
+    id: 14,
+    email_address: 'other@example.com',
+    token: 'tok-other',
+    project_name: 'other',
+  }, {
+    id: 15,
+    email_address: 'ready@outlook.com',
+    token: 'tok-ready',
+    project_name: 'OpenAi',
+    warranty_until: '2026-04-15T09:00:00Z',
+  }];
+
+  const reusable = filterReusableLuckmailPurchases(purchases, {
+    projectCode: 'openai',
+    usedPurchases: { 10: true },
+    preserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
+    now: Date.parse('2026-04-14T10:00:00Z'),
+  });
+
+  assert.deepEqual(reusable.map((purchase) => purchase.id), [15]);
+  assert.equal(pickReusableLuckmailPurchase(purchases, {
+    projectCode: 'openai',
+    usedPurchases: { 10: true },
+    preserveTagName: DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME,
+    now: Date.parse('2026-04-14T10:00:00Z'),
+  }).id, 15);
+});
+
+test('pickLuckmailVerificationMail respects sender filters, time filters, and excluded codes', () => {
+  const match = pickLuckmailVerificationMail([
+    {
+      message_id: 'old-mail',
+      from: 'noreply@openai.com',
+      subject: 'Your code is 111111',
+      received_at: '2026-04-14T09:59:00Z',
+    },
+    {
+      message_id: 'new-mail',
+      from: 'noreply@openai.com',
+      subject: 'Your code is 222222',
+      received_at: '2026-04-14T10:05:00Z',
+    },
+  ], {
+    senderFilters: ['openai'],
+    subjectFilters: ['code'],
+    excludeCodes: ['111111'],
+    afterTimestamp: Date.parse('2026-04-14T10:00:00Z'),
+  });
+
+  assert.equal(match.code, '222222');
+  assert.equal(match.mail.message_id, 'new-mail');
+});
+
+test('isLuckmailMailNewerThanCursor compares message id and timestamp safely', () => {
+  const cursor = buildLuckmailMailCursor({
+    message_id: 'mail-1',
+    received_at: '2026-04-14T10:00:00Z',
+  });
+
+  assert.equal(isLuckmailMailNewerThanCursor({
+    message_id: 'mail-1',
+    received_at: '2026-04-14T10:00:00Z',
+  }, cursor), false);
+
+  assert.equal(isLuckmailMailNewerThanCursor({
+    message_id: 'mail-2',
+    received_at: '2026-04-14T10:01:00Z',
+  }, cursor), true);
+});
+
+test('normalizeLuckmailMailCursor tolerates null cursor input', () => {
+  const { normalizeLuckmailMailCursor } = require('../luckmail-utils.js');
+  assert.deepEqual(normalizeLuckmailMailCursor(null), {
+    messageId: '',
+    receivedAt: '',
+  });
+});
+
+test('normalizeTimestamp treats LuckMail naive datetime strings as UTC', () => {
+  assert.equal(
+    normalizeTimestamp('2026-04-14 13:32:05'),
+    Date.UTC(2026, 3, 14, 13, 32, 5, 0)
+  );
+});
+
+test('buildLuckmailBaselineCursor tracks newest existing mail as baseline', () => {
+  const cursor = buildLuckmailBaselineCursor([
+    {
+      message_id: 'mail-old',
+      received_at: '2026-04-14 13:31:15',
+      subject: '你的 ChatGPT 代码为 111111',
+    },
+    {
+      message_id: 'mail-new',
+      received_at: '2026-04-14 13:32:05',
+      subject: '你的 ChatGPT 代码为 222222',
+    },
+  ]);
+
+  assert.deepEqual(cursor, {
+    messageId: 'mail-new',
+    receivedAt: '2026-04-14 13:32:05',
+  });
+});

Some files were not shown because too many files changed in this diff