M
mail-service
メールを選択してください
/gi, ''); s = s.replace(//gi, ''); s = s.replace(//gi, ''); s = s.replace(//gi, ''); s = s.replace(/]*>/gi, ''); s = s.replace(//gi, ''); s = s.replace(/sonw+s*=s*["'][^"']*["']/gi, ''); s = s.replace(/sonw+s*=s*[^s>]+/gi, ''); s = s.replace(/]*src=["'](https?://[^"']+)["'][^>]*>/gi, (m,u)=>'[画像を開く] '+escapeHtml(u)+''); s = s.replace(/]*>/gi, ''); return s; } function demoData(){ const now = Date.now(), day = 86400000, f = [{name:'GitHub',address:'noreply@github.com'},{name:'Proton Mail',address:'no-reply@proton.me'},{name:'Cloudflare',address:'community@cloudflare.com'},{name:'Stripe',address:'receipts@stripe.com'},{name:'田中太郎',address:'tanaka@example.com'},{name:'佐藤花子',address:'sato@example.com'}]; const s = ['【重要】サインイン検出','Weekly report','Welcome','Receipt #INV-2026-00231','Verification code 482-901','【会議】打ち合わせ','Newsletter']; const b = ['新しいデバイスからサインインしました。 心当たりがない場合はパスワードを変更してください。','Hello. All services are operational. - Workers - D1 - Email Service','mail-serviceへようこそ。Proton Mail Bridge経由で取得しD1に保存します。R2不使用。','Payment of $9.00 received. Invoice: INV-2026-00231','Your verification code is 482-901. Expires in 10 minutes.','来週の打ち合わせについて相談したいことがあります。 候補日: 火曜10:00 / 水曜14:00 / 金曜16:00','Hi. Cloudflare Email Service update. Read at https://blog.cloudflare.com/email-service']; const list = []; for(let i=0;i<12;i++){ const from = f[i%f.length]; list.push({id:'demo-'+i, messageId:'', from: from, to:[{name:'You',address:'yumedebug@proton.me'}], cc:[], subject:s[i%s.length], date:new Date(now-i*0.4*day).toISOString(), receivedAt:new Date(now-i*0.4*day).toISOString(), textBody:b[i%b.length], htmlBody:i%3===0 ? '

'+b[i%b.length].replace(/ /g,'
')+'

Best regards.

' : null, attachments:i%4===0?[{id:'a-'+i,filename:'invoice.pdf',mimeType:'application/pdf',size:124000}]:[], isRead:i>1, hasAttachments:i%4===0, size:2048+i*256}); } return list; } function filtered(){ let list = state.messages; if(state.filter==='unread') list = list.filter(m=>!m.isRead); if(state.filter==='attachments') list = list.filter(m=>m.hasAttachments); if(state.query){ const q = state.query.toLowerCase(); list = list.filter(m=>{ const hay = [m.subject, m.from?.name, m.from?.address, m.textBody, m.htmlBody, m.date, ...(m.to||[]).map(t=>t.address+' '+(t.name||''))].join(' ').toLowerCase(); return hay.includes(q); }); } return list; } function renderList(){ const list = filtered(); $('#countChip').textContent = list.length; const root = $('#messageList'); if(list.length===0){ root.innerHTML='
📭
受信箱は空です
'; return; } root.innerHTML = list.map(m=>{ const from = m.from || {}; const cls = m.isRead ? '' : 'unread'; const sel = m.id === state.selectedId ? 'selected' : ''; const preview = (m.textBody || '').replace(/s+/g, ' ').slice(0, 120); const attachDot = m.hasAttachments ? '📎' : ''; return '
' + '
' + '
' + '
'+escapeHtml(from.name || from.address || '')+'
' + '
'+escapeHtml(m.subject || '(件名なし)')+'' + attachDot + '
' + '
'+escapeHtml(preview)+'
' + '
' + '
'+escapeHtml(timeStr(m.date || m.receivedAt))+'
' + '
'; }).join(''); $$('.row').forEach(r=>{ r.addEventListener('click', ()=>selectMessage(r.dataset.id)); }); } function renderDetail(){ const root = $('#detailBody'); const m = state.messages.find(x=>x.id === state.selectedId); if(!m){ root.innerHTML='
メールを選択してください
'; return; } const from = m.from || {}; const to = m.to || []; const cc = m.cc || []; const date = new Date(m.date || m.receivedAt).toLocaleString('ja-JP'); const addrHtml = a => '
'+escapeHtml(a.name || a.address)+'
' + (a.name ? '
'+escapeHtml(a.address)+'
' : ''); let body = m.htmlBody ? sanitizeHtml(m.htmlBody) : ''; if(!body) body = '
'+escapeHtml(m.textBody || '').replace(/ /g,'
')+'
'; const atts = m.attachments || []; const attsHtml = atts.length ? ('

添付ファイル ('+atts.length+')

' + atts.map(a=>'
'+escapeHtml(extIcon(a.filename || ''))+'
'+escapeHtml(a.filename)+'
'+escapeHtml(bytes(a.size))+'
').join('') + '
') : ''; const toHtml = to.map(addrHtml).join(''); const ccHtml = cc.length ? '
CC: '+cc.map(addrHtml).join(', ')+'
' : ''; root.innerHTML = '

'+escapeHtml(m.subject || '(件名なし)')+'

' + '
'+escapeHtml(initials(from.name, from.address))+'
'+toHtml+'
' + '
To: '+to.map(addrHtml).join('')+'
' + ccHtml + '
'+body+'
' + attsHtml; } function selectMessage(id){ state.selectedId = id; document.body.classList.add('has-selection'); renderList(); renderDetail(); const m = state.messages.find(x=>x.id===id); if(m && !m.isRead){ m.isRead = true; apiPatch('/api/messages/'+encodeURIComponent(id)+'/read',{}).catch(()=>{}); renderList(); } } $('#backBtn').addEventListener('click',()=>{state.selectedId=null;document.body.classList.remove('has-selection');renderList();renderDetail();}); $$('.tab').forEach(t=>{t.addEventListener('click',()=>{$$('.tab').forEach(x=>x.classList.remove('active'));t.classList.add('active');state.filter=t.dataset.filter;renderList();});}); $('#searchInput').addEventListener('input',(e)=>{clearTimeout(window.searchTimer);window.searchTimer=setTimeout(()=>{state.query=e.target.value;renderList();},200);}); $('#refreshBtn').addEventListener('click',async()=>{toast('メール取得中...');try{const r=await apiPost('/api/fetch-emails',{});const d=await r.json();toast(d.message||'取得完了');await loadMessages();}catch(e){toast('取得失敗: '+e.message);}}); $('#themeBtn').addEventListener('click',()=>{const b=document.body;const cur=b.getAttribute('data-theme')||'dark';const next=cur==='dark'?'light':'dark';b.setAttribute('data-theme',next);fetch('/api/settings',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({theme:next})}).catch(()=>{});toast('テーマ: '+next);}); $('#fetchEmailsBtn').addEventListener('click',async()=>{const b=$('#fetchEmailsBtn');b.disabled=true;b.textContent='⏳ 取得中...';try{const r=await fetch('/api/fetch-emails',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({limit:10})});const d=await r.json();toast(d.message||'取得: '+d.fetched+'件');await loadMessages();await loadStatus();}catch(e){toast('失敗: '+e.message);}finally{b.disabled=false;b.textContent='📥 メール取得';}}); $('#fetchStatusBtn').addEventListener('click',async()=>{await loadStatus();toast('状態更新済み');}); $('#testApiBtn').addEventListener('click',async()=>{try{const r=await fetch('/api/health');const d=await r.json();toast('API: '+d.status);}catch(e){toast('API失敗: '+e.message);}}); $('#dbTestBtn').addEventListener('click',async()=>{try{const r=await fetch('/api/status');const d=await r.json();if(d.d1&&d.d1.ok){toast('DB接続: OK ('+d.d1.message_count+'件)');}else{toast('DB: '+(d.d1?.error||'接続不可'));}}catch(e){toast('DB確認失敗: '+e.message);}}); async function loadStatus(){try{const r=await fetch('/api/status');if(!r.ok)return;const d=await r.json();if(d.accounts){document.getElementById('currentEmail').textContent=d.accounts.current;document.getElementById('targetEmail').textContent=d.accounts.target;}if(d.d1){document.getElementById('dbStatus').textContent=d.d1.ok?('✓ OK ('+d.d1.message_count+'件)'):'✗ '+(d.d1.error||'未接続');}if(d.deployed_at){const u=new URL(d.deployed_at);document.getElementById('deployedUrl').textContent=u.hostname;}}catch(e){console.warn('loadStatus failed',e);}} $('#unreadBtn').addEventListener('click',async()=>{if(!state.selectedId)return;const m=state.messages.find(x=>x.id===state.selectedId);if(!m)return;try{await apiPatch('/api/messages/'+encodeURIComponent(m.id)+'/unread',{});m.isRead=false;renderList();toast('未読に戻しました');}catch(e){toast('失敗: '+e.message);}}); $('#deleteBtn').addEventListener('click',async()=>{if(!state.selectedId||!confirm('削除しますか?'))return;try{await apiDelete('/api/messages/'+encodeURIComponent(state.selectedId));state.messages=state.messages.filter(x=>x.id!==state.selectedId);state.selectedId=null;document.body.classList.remove('has-selection');renderList();renderDetail();toast('削除しました');}catch(e){toast('削除失敗: '+e.message);}}); async function apiGet(p){return fetch(p);} async function apiPost(p,b){return fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});} async function apiPatch(p,b){return fetch(p,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});} async function apiDelete(p){return fetch(p,{method:'DELETE'});} function loadMessages(){fetch('/api/messages?limit=50').then(r=>r.ok?r.json():Promise.resolve({data:[]})).then(d=>{state.messages=(d&&Array.isArray(d.data))?d.data:demoData();renderList();}).catch(()=>{state.messages=demoData();renderList();});} loadMessages(); loadStatus(); setInterval(loadStatus, 30000);