feat: name an unknown sender from the reader to enable replies
This commit is contained in:
parent
025963371f
commit
fd2d9298f8
2 changed files with 72 additions and 15 deletions
|
|
@ -174,25 +174,79 @@ async function openMessage(id) {
|
||||||
: `from: ${opened.from}\nkey: ${b32encode(opened.sender)}\nid: ${row.id}\ndate: ${fmtTime(opened.time)}`;
|
: `from: ${opened.from}\nkey: ${b32encode(opened.sender)}\nid: ${row.id}\ndate: ${fmtTime(opened.time)}`;
|
||||||
view.querySelector(".fields").textContent = fields.map(([k, v]) => `${k}: ${v}`).join("\n");
|
view.querySelector(".fields").textContent = fields.map(([k, v]) => `${k}: ${v}`).join("\n");
|
||||||
view.querySelector("pre").textContent = opened.body;
|
view.querySelector("pre").textContent = opened.body;
|
||||||
if (folder === "inbox" && !opened.knownSender) {
|
|
||||||
const note = document.createElement("p");
|
|
||||||
note.className = "placeholder";
|
|
||||||
note.textContent = "The wire carries no sender name, only this signed key. " +
|
|
||||||
"Import the sender's smol:// address in settings to name future mail.";
|
|
||||||
view.append(note);
|
|
||||||
}
|
|
||||||
if (folder === "inbox") {
|
if (folder === "inbox") {
|
||||||
const reply = document.createElement("button");
|
if (opened.knownSender) {
|
||||||
reply.textContent = "reply";
|
view.append(replyButton(opened, row));
|
||||||
reply.onclick = () => openCompose({
|
} else {
|
||||||
to: store.addressForKey(opened.sender) ?? "",
|
// The wire gives only a signed key; a reply needs the sender's address,
|
||||||
subject: opened.subject ? (opened.subject.startsWith("Re:") ? opened.subject : `Re: ${opened.subject}`) : "",
|
// which the user supplies once and we bind to that exact key.
|
||||||
replyTo: row.id,
|
const note = document.createElement("p");
|
||||||
});
|
note.className = "placeholder";
|
||||||
view.append(reply);
|
note.textContent = "The wire carries no sender name, only this signed key. " +
|
||||||
|
"To reply, give the sender's address — it is checked against this key:";
|
||||||
|
const form = document.createElement("form");
|
||||||
|
form.className = "name-sender";
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.placeholder = "alice@example.org or smol://…";
|
||||||
|
input.spellcheck = false;
|
||||||
|
const name = document.createElement("button");
|
||||||
|
name.textContent = "name sender & reply";
|
||||||
|
form.append(input, name);
|
||||||
|
form.onsubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
name.disabled = true;
|
||||||
|
try {
|
||||||
|
await nameSender(input.value, opened.sender);
|
||||||
|
openMessage(row.id); // the sender is named now; the reply button appears
|
||||||
|
openCompose({
|
||||||
|
to: store.addressForKey(opened.sender),
|
||||||
|
subject: opened.subject ? (opened.subject.startsWith("Re:") ? opened.subject : `Re: ${opened.subject}`) : "",
|
||||||
|
replyTo: row.id,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
status(err.message, true);
|
||||||
|
} finally {
|
||||||
|
name.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.append(note, form);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replyButton(opened, row) {
|
||||||
|
const reply = document.createElement("button");
|
||||||
|
reply.textContent = "reply";
|
||||||
|
reply.onclick = () => openCompose({
|
||||||
|
to: store.addressForKey(opened.sender) ?? "",
|
||||||
|
subject: opened.subject ? (opened.subject.startsWith("Re:") ? opened.subject : `Re: ${opened.subject}`) : "",
|
||||||
|
replyTo: row.id,
|
||||||
|
});
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind a user-supplied address to the key that signed a message. A smol://
|
||||||
|
// address carries its own key (verified); a short address is resolved and the
|
||||||
|
// result kept on first use. Anything that binds a different key is refused.
|
||||||
|
async function nameSender(text, senderKey) {
|
||||||
|
const addr = parseAddress(text.trim());
|
||||||
|
let key = addr.identity, verified = true;
|
||||||
|
if (!key) {
|
||||||
|
const opened = await connect(addr, { requirePin: false });
|
||||||
|
try {
|
||||||
|
({ identity: key } = await resolveOp(opened.session, addr.user));
|
||||||
|
} finally {
|
||||||
|
opened.session.stream.close();
|
||||||
|
}
|
||||||
|
verified = false; // trust on first use, as with any RESOLVE
|
||||||
|
}
|
||||||
|
if (!timingSafeEqual(key, senderKey))
|
||||||
|
throw new SmolError("that address carries a different key than this message's sender");
|
||||||
|
store.saveContact(addr.short, senderKey, verified);
|
||||||
|
status(`${addr.short} saved for this sender${verified ? " (verified key)" : ""}`);
|
||||||
|
renderFolder();
|
||||||
|
}
|
||||||
|
|
||||||
// --- compose and send -----------------------------------------------------------
|
// --- compose and send -----------------------------------------------------------
|
||||||
|
|
||||||
const compose = $("compose");
|
const compose = $("compose");
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,9 @@ input, textarea {
|
||||||
.verified { color: var(--accent); font-size: 12px; }
|
.verified { color: var(--accent); font-size: 12px; }
|
||||||
.warn { color: var(--warn); }
|
.warn { color: var(--warn); }
|
||||||
|
|
||||||
|
.name-sender { display: flex; gap: 8px; margin: 12px 0 0; }
|
||||||
|
.name-sender input { flex: 1; }
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue