Files
.sune/proton-send.sune

1 line
9.7 KiB
Plaintext

[{"id":"1esgs6k","name":"1 Click Proton Email","pinned":false,"avatar":"","url":"gh://multipleof4/.sune/proton-send.sune","updatedAt":1775780667048,"settings":{"model":"","temperature":"","top_p":"","top_k":"","frequency_penalty":"","repetition_penalty":"","min_p":"","top_a":"","verbosity":"","reasoning_effort":"default","system_prompt":"","html":"<!--\nSune: 1-Click Proton Email Sender\nVersion: 1.2\n\nFormat:\n ` ` `email\n DISPLAY_NAME: \"Your Name\"\n TO: \"recipient@example.com\"\n SUBJECT: \"Re: whatever\"\n MESSAGE_ID: \"<from the email you're replying to>\"\n REFERENCES: \"<ref1> <ref2>\"\n BODY: \"Hello,\\n\\nMultiline body here.\\n\\nCheers\"\n ` ` `\n\nRequired: TO, SUBJECT, BODY\nOptional: DISPLAY_NAME, MESSAGE_ID, REFERENCES\nAuth: USER.customKey1 (Account Settings > API > Custom Key 1)\n-->\n<div id=\"sune_email_sender\" x-data=\"{v:'1.2'}\" class=\"hidden\"></div>\n<script>\n(()=>{\n const NS='__suneEml';\n\n // ── Tear down ANY prior instance before doing anything ──\n if(window[NS]){\n try{window[NS].destroy()}catch{}\n delete window[NS];\n }\n\n const suneEl=document.getElementById('sune_email_sender');\n if(!suneEl)return;\n\n const SUNE_NAME='[Sune: Email Sender]',\n SUNE_V='1.2',\n ENDPOINT='https://proton.planetrenox.com/send';\n\n // ── Global dedup: tracks fingerprints of emails already sent this session ──\n if(!window.__emlSentSet)window.__emlSentSet=new Set();\n const sentSet=window.__emlSentSet;\n\n // ── Per-<pre> dedup: tracks which <pre> nodes already have a button ──\n const btnPres=new WeakSet();\n\n const defaultCls=['bg-slate-100','text-slate-700','hover:bg-slate-200'],\n successCls=['bg-green-100','text-green-800'],\n errorCls=['bg-red-100','text-red-800'];\n\n const KNOWN_KEYS=/^(DISPLAY_NAME|TO|SUBJECT|BODY|MESSAGE_ID|REFERENCES)\\s*:\\s*(.*)/i;\n\n const fingerprint=f=>[f.TO,f.SUBJECT,f.BODY,f.MESSAGE_ID||''].join('\\x00');\n\n const parseEmailBlock=raw=>{\n const fields={},lines=raw.split('\\n');\n let curKey=null,curVal='';\n for(const line of lines){\n const m=line.match(KNOWN_KEYS);\n if(m){\n if(curKey)fields[curKey]=curVal;\n curKey=m[1].toUpperCase();\n curVal=m[2];\n }else if(curKey){\n curVal+='\\n'+line;\n }\n }\n if(curKey)fields[curKey]=curVal;\n for(const k in fields){\n let v=fields[k].trim();\n if(v.startsWith('\"')&&v.endsWith('\"'))v=v.slice(1,-1);\n fields[k]=v.replace(/\\\\n/g,'\\n');\n }\n return fields;\n };\n\n let destroyed=false;\n\n const sendEmail=async(btn,fields)=>{\n if(destroyed)return;\n const fp=fingerprint(fields);\n\n // ── Already sent guard ──\n if(sentSet.has(fp)){\n btn.disabled=true;\n btn.classList.remove(...defaultCls);\n btn.classList.add(...successCls);\n btn.innerHTML='<i data-lucide=\"check\" class=\"h-3.5 w-3.5\"></i> Already Sent';\n window.lucide?.createIcons();\n return;\n }\n\n const auth=window.USER?.customKey1;\n if(!auth){\n alert('Email auth password not set.\\nAccount Settings → API → Custom Key 1');\n return;\n }\n\n // ── Disable ALL buttons sharing this fingerprint ──\n document.querySelectorAll(`[data-eml-fp=\"${CSS.escape(fp)}\"]`).forEach(b=>{\n b.disabled=true;\n b.innerHTML='<span class=\"relative flex h-3 w-3\"><span class=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75\"></span><span class=\"relative inline-flex rounded-full h-3 w-3 bg-sky-500\"></span></span> Sending...';\n });\n\n try{\n const body={\n auth,\n to:fields.TO,\n subject:fields.SUBJECT||'(no subject)',\n text:fields.BODY||''\n };\n if(fields.DISPLAY_NAME)body.display_name=fields.DISPLAY_NAME;\n if(fields.MESSAGE_ID){\n body.in_reply_to=fields.MESSAGE_ID;\n const refs=(fields.REFERENCES||'').trim();\n body.references=refs?refs+' '+fields.MESSAGE_ID:fields.MESSAGE_ID;\n }else if(fields.REFERENCES){\n body.references=fields.REFERENCES;\n }\n\n const res=await fetch(ENDPOINT,{\n method:'POST',\n headers:{'Content-Type':'application/json'},\n body:JSON.stringify(body)\n });\n const data=await res.json();\n if(!res.ok)throw new Error(data.error||`HTTP ${res.status}`);\n\n // ── Mark as sent globally ──\n sentSet.add(fp);\n\n document.querySelectorAll(`[data-eml-fp=\"${CSS.escape(fp)}\"]`).forEach(b=>{\n b.disabled=true;\n b.classList.remove(...defaultCls);\n b.classList.add(...successCls);\n b.innerHTML='<i data-lucide=\"check\" class=\"h-3.5 w-3.5\"></i> Sent';\n });\n }catch(err){\n alert(`${SUNE_NAME} Send Failed:\\n\\n${err.message}`);\n document.querySelectorAll(`[data-eml-fp=\"${CSS.escape(fp)}\"]`).forEach(b=>{\n b.classList.remove(...defaultCls);\n b.classList.add(...errorCls);\n b.innerHTML='<i data-lucide=\"x\" class=\"h-3.5 w-3.5\"></i> Failed';\n });\n setTimeout(()=>{\n if(destroyed)return;\n document.querySelectorAll(`[data-eml-fp=\"${CSS.escape(fp)}\"]`).forEach(b=>{\n if(sentSet.has(fp))return;\n b.disabled=false;\n b.classList.remove(...errorCls);\n b.classList.add(...defaultCls);\n b.innerHTML='<i data-lucide=\"send\" class=\"h-3.5 w-3.5\"></i> Send Email';\n window.lucide?.createIcons();\n });\n },4e3);\n }finally{\n window.lucide?.createIcons();\n }\n };\n\n const processBubble=bubble=>{\n if(!bubble||destroyed)return false;\n let added=false;\n\n bubble.querySelectorAll('code[class*=\"language-email\"]').forEach(code=>{\n const pre=code.closest('pre');\n if(!pre||btnPres.has(pre))return;\n\n const fields=parseEmailBlock(code.textContent);\n if(!fields.TO||!fields.BODY)return;\n\n const fp=fingerprint(fields);\n const alreadySent=sentSet.has(fp);\n const to=fields.TO,subj=fields.SUBJECT||'(no subject)';\n const isReply=!!fields.MESSAGE_ID;\n const label=isReply?'Reply':'Send Email';\n const icon=alreadySent?'check':(isReply?'reply':'send');\n\n const wrapper=document.createElement('div');\n wrapper.className='flex items-center gap-2 mt-2 flex-wrap';\n wrapper.setAttribute('data-eml-wrapper','');\n\n const preview=document.createElement('span');\n preview.className='text-[11px] text-gray-400 truncate max-w-[260px]';\n preview.textContent=`→ ${to} · ${subj}`;\n\n const btn=document.createElement('button');\n btn.setAttribute('data-eml-fp',fp);\n btn.className=[\n 'email-send-btn inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors',\n ...(alreadySent?successCls:defaultCls)\n ].join(' ');\n\n if(alreadySent){\n btn.disabled=true;\n btn.innerHTML='<i data-lucide=\"check\" class=\"h-3.5 w-3.5\"></i> Already Sent';\n }else{\n btn.innerHTML=`<i data-lucide=\"${icon}\" class=\"h-3.5 w-3.5\"></i> ${label}`;\n btn.onclick=e=>{\n e.preventDefault();\n e.stopPropagation();\n if(btn.disabled||sentSet.has(fp))return;\n sendEmail(btn,fields);\n };\n }\n\n wrapper.append(btn,preview);\n btnPres.add(pre);\n pre.insertAdjacentElement('afterend',wrapper);\n added=true;\n });\n\n if(added)window.lucide?.createIcons();\n return added;\n };\n\n const scanExisting=()=>{\n if(destroyed)return;\n document.querySelectorAll('#messages .msg-bubble').forEach(processBubble);\n };\n\n const observer=new MutationObserver(mutations=>{\n if(destroyed)return;\n for(const m of mutations)\n for(const node of m.addedNodes)\n if(node.nodeType===1){\n if(node.matches?.('.msg-bubble'))processBubble(node);\n else node.querySelectorAll?.('.msg-bubble')?.forEach(processBubble);\n }\n });\n\n const newResponseListener=e=>{\n if(destroyed)return;\n const id=e?.detail?.message?.id;\n if(!id)return;\n let attempts=0;\n const tryProcess=()=>{\n if(destroyed)return;\n attempts++;\n const bubble=window.getBubbleById?.(id);\n if(bubble&&processBubble(bubble))return;\n if(attempts<8)setTimeout(tryProcess,300);\n };\n setTimeout(tryProcess,150);\n };\n\n const composerEl=window.el?.composer,\n chatContainer=window.el?.messages;\n\n if(chatContainer&&composerEl){\n observer.observe(chatContainer,{childList:true,subtree:true});\n composerEl.addEventListener('sune:newSuneResponse',newResponseListener);\n scanExisting();\n }\n\n // ── Destroy function: deterministic full teardown ──\n const destroy=()=>{\n if(destroyed)return;\n destroyed=true;\n observer.disconnect();\n if(composerEl)\n composerEl.removeEventListener('sune:newSuneResponse',newResponseListener);\n delete window[NS];\n };\n\n // ── Expose instance for next load to tear down ──\n window[NS]={destroy};\n\n // ── sune:unmount from renderSuneHTML ──\n suneEl.addEventListener('sune:unmount',destroy,{once:true});\n\n // ── Also listen on the suneHtml container directly as a fallback ──\n const suneHtmlEl=document.getElementById('suneHtml');\n if(suneHtmlEl&&suneHtmlEl!==suneEl)\n suneHtmlEl.addEventListener('sune:unmount',destroy,{once:true});\n\n console.log(`${SUNE_NAME} v${SUNE_V} ready.`);\n})();\n</script>\n","extension_html":"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>","hide_composer":true,"include_thoughts":false,"img_output":false,"aspect_ratio":"1:1","image_size":"1K","ignore_master_prompt":false},"storage":{}}]