Action Script - Find Draft by UUID

Paste a UUID, a partial, or a drafts:// link — it finds the draft and opens it. This has been successfully tested in macOS as a drafts action.

// Find Draft by UUID · ACTION SOURCE · NEW action — create it, then paste
// ============================================================================
// Drafts action: “Find Draft by UUID”
// Paste a UUID, a partial, or a drafts:// link — it finds the draft and opens it.
// ----------------------------------------------------------------------------
// Motivation: a UUID handed over in conversation is unusable inside Drafts. There
// is no built-in “go to this UUID”. This closes that gap.
//
// Accepts, interchangeably:
// DB99B636-7E98-4249-9D7A-E45C39501E1B full UUID (any case)
// drafts://open?uuid=DB99B636-7E98-… a link — cropped to the UUID
// x-callback-url://drafts/open?uuid=FC5E.. ditto, params after it ignored
// DB99B6 a partial — matched as a substring
// “the spec is DB99B6” prose around it is ignored
//
// Partial search covers EVERY folder including trash and archive, because the
// point is to find a draft you have lost track of. Full-UUID lookup uses
// Draft.find(), which is folder-agnostic already.
//
// Bind it to a keyboard shortcut. Seeds itself from the clipboard, so the usual
// flow is: copy the UUID → run → Enter.
// JavaScriptCore-safe: no require / fs / npm / HTTP.
// ============================================================================

const MAX_BUTTONS = 20; // Prompt gets unusable past ~20; longer lists go to text

// ---- key extraction -------------------------------------------------------
// Returns { key, partial } or null. partial:false means an exact UUID lookup.
function extractKey(raw){
if(raw===null||raw===undefined) return null;
const s=String(raw).trim();
if(!s) return null;
// A bare UUID, a drafts:// link, an x-callback URL, or a UUID pasted mid-sentence
// all reduce to the same thing: the first UUID-shaped run. Try that first.
const full=s.match(/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/);
if(full) return {key:full[0].toUpperCase(), partial:false};
// No full UUID: treat the input as a fragment. An explicit uuid= wins; otherwise
// take the longest hex-ish run, which survives surrounding prose and punctuation.
// (Stripping non-hex characters globally would NOT work — “drafts://open” is
// itself mostly hex letters and would corrupt the fragment.)
let frag=null;
const m=s.match(/uuid=([0-9A-Fa-f-]+)/i);
if(m) frag=m[1];
else {
const runs=s.match(/[0-9A-Fa-f-]{4,}/g)||;
for(const r of runs) if(!frag||r.length>frag.length) frag=r;
}
if(!frag) return null;
frag=frag.replace(/^-+|-+$/g,“”);
if(!frag) return null;
return {key:frag.toUpperCase(), partial:true};
}

// ---- search ---------------------------------------------------------------
function findAll(key, partial){
if(!partial){
const d=Draft.find(key);
return d?[d]:;
}
const out=, seen={};
// “all” omits trash in Drafts’ query filter, so trash is queried separately —
// a draft you cannot find is exactly the one that might be sitting in it.
for(const folder of [“all”,“trash”]){
let batch=;
try { batch=Draft.query(“”, folder, , , “modified”, true, false); } catch(e){ batch=; }
for(const d of batch){
const u=String(d.uuid).toUpperCase();
if(seen[u]) continue;
if(u.indexOf(key)>-1){ seen[u]=true; out.push(d); }
}
}
// Prefix matches first — a fragment is nearly always the start of a UUID.
out.sort((a,b)=>{
const pa=String(a.uuid).toUpperCase().indexOf(key)===0?0:1;
const pb=String(b.uuid).toUpperCase().indexOf(key)===0?0:1;
return pa-pb;
});
return out;
}

// ---- display --------------------------------------------------------------
function where(d){
if(d.isTrashed) return “trash”;
if(d.isArchived) return “archive”;
if(d.isFlagged) return “flagged”;
return “inbox”;
}
function titleOf(d){
let t=String(d.title||“”).trim();
if(!t) t=“(untitled)”;
return t.length>52 ? t.slice(0,49)+“…” : t;
}
function label(d,i){
return (i+1)+“. “+titleOf(d)+” [”+String(d.uuid).slice(0,8)+" · “+where(d)+”]“;
}
function detail(d){
return titleOf(d)+”\n"+
" uuid “+d.uuid+”\n"+
" link drafts://open?uuid=“+d.uuid+”\n"+
" folder “+where(d)+
(d.tags&&d.tags.length?”\n tags “+d.tags.join(”, “):”")+
"\n changed "+String(d.modifiedAt).slice(0,24);
}

// ============================================================================
// MAIN
// ============================================================================
(function main(){
// Seed from the clipboard only if it actually holds something UUID-shaped —
// otherwise the field opens prefilled with junk you have to clear first.
let seed=“”;
try { const c=app.getClipboard(); if(c && extractKey(c)) seed=String(c).trim(); } catch(e){}

const p=Prompt.create();
p.title=“Find Draft by UUID”;
p.message=“Full UUID, a partial, or a drafts:// link. Surrounding text is ignored.”;
p.addTextField(“q”,“UUID / partial / link”, seed);
p.addButton(“Find”);
p.addButton(“Cancel”);
if(!p.show() || p.buttonPressed===“Cancel”){ context.cancel(“cancelled”); return; }

const parsed=extractKey(p.fieldValues[“q”]);
if(!parsed){
alert(“Nothing UUID-shaped in:\n\n”+String(p.fieldValues[“q”]||“(empty)”));
context.cancel(“no key”); return;
}

const hits=findAll(parsed.key, parsed.partial);

if(hits.length===0){
alert((parsed.partial?“No draft whose UUID contains:”:“No draft with UUID:”)+
“\n\n”+parsed.key+
“\n\nSearched every folder, trash included.”+
(parsed.partial?“”:“\n\nIf you meant a partial, pass fewer characters — a full-length\nUUID is looked up exactly and will not fuzzy-match.”));
context.cancel(“no match”); return;
}

if(hits.length===1){
const d=hits[0];
editor.load(d);
app.displaySuccessMessage(titleOf(d));
alert(detail(d));
return;
}

if(hits.length>MAX_BUTTONS){
alert(hits.length+" drafts match “”+parsed.key+“” — too many to pick from.\n"+
“Add a few more characters.\n\nFirst “+MAX_BUTTONS+”:\n\n”+
hits.slice(0,MAX_BUTTONS).map((d,i)=>label(d,i)).join(“\n”));
context.cancel(“too many”); return;
}

const pick=Prompt.create();
pick.title=hits.length+" matches";
pick.message=“”“+parsed.key+”" matches “+hits.length+” drafts.";
// Index-prefixed so labels stay unique even when two drafts share a title —
// the button string is the only thing Drafts hands back.
const labels=hits.map((d,i)=>label(d,i));
for(const l of labels) pick.addButton(l);
pick.addButton(“Cancel”);
if(!pick.show() || pick.buttonPressed===“Cancel”){ context.cancel(“cancelled”); return; }

const idx=labels.indexOf(pick.buttonPressed);
if(idx<0){ context.cancel(“no selection”); return; }
editor.load(hits[idx]);
app.displaySuccessMessage(titleOf(hits[idx]));
alert(detail(hits[idx]));
})();// Find Draft by UUID · ACTION SOURCE · NEW action — create it, then paste
// ============================================================================
// Drafts action: “Find Draft by UUID”
// Paste a UUID, a partial, or a drafts:// link — it finds the draft and opens it.
// ----------------------------------------------------------------------------
// Motivation: a UUID handed over in conversation is unusable inside Drafts. There
// is no built-in “go to this UUID”. This closes that gap.
//
// Accepts, interchangeably:
// DB99B636-7E98-4249-9D7A-E45C39501E1B full UUID (any case)
// drafts://open?uuid=DB99B636-7E98-… a link — cropped to the UUID
// x-callback-url://drafts/open?uuid=FC5E.. ditto, params after it ignored
// DB99B6 a partial — matched as a substring
// “the spec is DB99B6” prose around it is ignored
//
// Partial search covers EVERY folder including trash and archive, because the
// point is to find a draft you have lost track of. Full-UUID lookup uses
// Draft.find(), which is folder-agnostic already.
//
// Bind it to a keyboard shortcut. Seeds itself from the clipboard, so the usual
// flow is: copy the UUID → run → Enter.
// JavaScriptCore-safe: no require / fs / npm / HTTP.
// ============================================================================

const MAX_BUTTONS = 20; // Prompt gets unusable past ~20; longer lists go to text

// ---- key extraction -------------------------------------------------------
// Returns { key, partial } or null. partial:false means an exact UUID lookup.
function extractKey(raw){
if(raw===null||raw===undefined) return null;
const s=String(raw).trim();
if(!s) return null;
// A bare UUID, a drafts:// link, an x-callback URL, or a UUID pasted mid-sentence
// all reduce to the same thing: the first UUID-shaped run. Try that first.
const full=s.match(/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/);
if(full) return {key:full[0].toUpperCase(), partial:false};
// No full UUID: treat the input as a fragment. An explicit uuid= wins; otherwise
// take the longest hex-ish run, which survives surrounding prose and punctuation.
// (Stripping non-hex characters globally would NOT work — “drafts://open” is
// itself mostly hex letters and would corrupt the fragment.)
let frag=null;
const m=s.match(/uuid=([0-9A-Fa-f-]+)/i);
if(m) frag=m[1];
else {
const runs=s.match(/[0-9A-Fa-f-]{4,}/g)||;
for(const r of runs) if(!frag||r.length>frag.length) frag=r;
}
if(!frag) return null;
frag=frag.replace(/^-+|-+$/g,“”);
if(!frag) return null;
return {key:frag.toUpperCase(), partial:true};
}

// ---- search ---------------------------------------------------------------
function findAll(key, partial){
if(!partial){
const d=Draft.find(key);
return d?[d]:;
}
const out=, seen={};
// “all” omits trash in Drafts’ query filter, so trash is queried separately —
// a draft you cannot find is exactly the one that might be sitting in it.
for(const folder of [“all”,“trash”]){
let batch=;
try { batch=Draft.query(“”, folder, , , “modified”, true, false); } catch(e){ batch=; }
for(const d of batch){
const u=String(d.uuid).toUpperCase();
if(seen[u]) continue;
if(u.indexOf(key)>-1){ seen[u]=true; out.push(d); }
}
}
// Prefix matches first — a fragment is nearly always the start of a UUID.
out.sort((a,b)=>{
const pa=String(a.uuid).toUpperCase().indexOf(key)===0?0:1;
const pb=String(b.uuid).toUpperCase().indexOf(key)===0?0:1;
return pa-pb;
});
return out;
}

// ---- display --------------------------------------------------------------
function where(d){
if(d.isTrashed) return “trash”;
if(d.isArchived) return “archive”;
if(d.isFlagged) return “flagged”;
return “inbox”;
}
function titleOf(d){
let t=String(d.title||“”).trim();
if(!t) t=“(untitled)”;
return t.length>52 ? t.slice(0,49)+“…” : t;
}
function label(d,i){
return (i+1)+“. “+titleOf(d)+” [”+String(d.uuid).slice(0,8)+" · “+where(d)+”]“;
}
function detail(d){
return titleOf(d)+”\n"+
" uuid “+d.uuid+”\n"+
" link drafts://open?uuid=“+d.uuid+”\n"+
" folder “+where(d)+
(d.tags&&d.tags.length?”\n tags “+d.tags.join(”, “):”")+
"\n changed "+String(d.modifiedAt).slice(0,24);
}

// ============================================================================
// MAIN
// ============================================================================
(function main(){
// Seed from the clipboard only if it actually holds something UUID-shaped —
// otherwise the field opens prefilled with junk you have to clear first.
let seed=“”;
try { const c=app.getClipboard(); if(c && extractKey(c)) seed=String(c).trim(); } catch(e){}

const p=Prompt.create();
p.title=“Find Draft by UUID”;
p.message=“Full UUID, a partial, or a drafts:// link. Surrounding text is ignored.”;
p.addTextField(“q”,“UUID / partial / link”, seed);
p.addButton(“Find”);
p.addButton(“Cancel”);
if(!p.show() || p.buttonPressed===“Cancel”){ context.cancel(“cancelled”); return; }

const parsed=extractKey(p.fieldValues[“q”]);
if(!parsed){
alert(“Nothing UUID-shaped in:\n\n”+String(p.fieldValues[“q”]||“(empty)”));
context.cancel(“no key”); return;
}

const hits=findAll(parsed.key, parsed.partial);

if(hits.length===0){
alert((parsed.partial?“No draft whose UUID contains:”:“No draft with UUID:”)+
“\n\n”+parsed.key+
“\n\nSearched every folder, trash included.”+
(parsed.partial?“”:“\n\nIf you meant a partial, pass fewer characters — a full-length\nUUID is looked up exactly and will not fuzzy-match.”));
context.cancel(“no match”); return;
}

if(hits.length===1){
const d=hits[0];
editor.load(d);
app.displaySuccessMessage(titleOf(d));
alert(detail(d));
return;
}

if(hits.length>MAX_BUTTONS){
alert(hits.length+" drafts match “”+parsed.key+“” — too many to pick from.\n"+
“Add a few more characters.\n\nFirst “+MAX_BUTTONS+”:\n\n”+
hits.slice(0,MAX_BUTTONS).map((d,i)=>label(d,i)).join(“\n”));
context.cancel(“too many”); return;
}

const pick=Prompt.create();
pick.title=hits.length+" matches";
pick.message=“”“+parsed.key+”" matches “+hits.length+” drafts.";
// Index-prefixed so labels stay unique even when two drafts share a title —
// the button string is the only thing Drafts hands back.
const labels=hits.map((d,i)=>label(d,i));
for(const l of labels) pick.addButton(l);
pick.addButton(“Cancel”);
if(!pick.show() || pick.buttonPressed===“Cancel”){ context.cancel(“cancelled”); return; }

const idx=labels.indexOf(pick.buttonPressed);
if(idx<0){ context.cancel(“no selection”); return; }
editor.load(hits[idx]);
app.displaySuccessMessage(titleOf(hits[idx]));
alert(detail(hits[idx]));
})();