Public: anyone can read this; the crew can change it.

Download an approved email blast as a .eml file the agent opens in their own mail app to send #115

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

An approved email now downloads as a `.eml` draft the agent opens in their own mail app. A draft answers 409. The new tests, the speed, contract, openapi, design and followup suites, and the design lint pass. I did not run the full suite or `tsc`, because the repo has no `tsconfig.json`, and there is no `scripts/gate.ts` or `scripts/fast.ts` to run either.

shipped production is serving it deploy 957b843

opened this

An approved email now downloads as a .eml draft the agent opens in their own mail app. A draft answers 409. The new tests, the speed, contract, openapi, design and followup suites, and the design lint pass. I did not run the full suite or tsc, because the repo has no tsconfig.json, and there is no scripts/gate.ts or scripts/fast.ts to run either.

What changed

  • Route: GET /listings/:id/email.eml takes ?which=followup|reminder like the email page. It answers message/rfc822 with attachment; filename="<address-slug>.eml" and no-store.
  • Who can read it: it goes through the same getListing read as the email page, so there is no new contract cell.
  • Not approved: it answers 409 with "This email needs approving first: approve it on the listing, then download it."
  • Email page: the "Download as email draft (.eml)" link shows only when that email is approved.
  • Headers: the file carries From: with the agent's name and address, To: empty, the Subject:, X-Unsent: 1 and MIME-Version. Every line ends in CRLF.
  • Body: multipart/alternative with a plain-text part and an HTML part, both base64 UTF-8. The HTML has inline styles, brand colours read from the design tokens, and photos as absolute links using publicOrigin. There are no attachments.
  • Header safety:

- Header values lose CR/LF and other control characters. - Long values fold at 76 characters. - A non-ASCII subject becomes an RFC 2047 encoded word. - The From: address and display name drop angle brackets, commas and quotes.

  • New module: src/eml.ts is pure, with the listing, which email and the origin in and a string out.
  • README: a paragraph in a realtor's words: approve it, download it, open it, it is a draft in your mail app.

Tests in src/eml.test.ts:

  • A draft gives no file, and an approved email gives the headers above.
  • A subject containing \r\nBcc: comes out as Subject: Hi with no Bcc header. The subject parser (emailParts) already cuts at the first line, so the injection is stopped twice. I also tested the From: name, which is cleaned by the new code.
  • A non-ASCII subject is encoded.
  • The follow-up has an empty To: and the visitor's name and address appear nowhere in it.
  • Over HTTP: 409 for a draft, 200 with the right headers once approved, and the page link appears only after approval.

Not done

  • No recipients are filled in: the agent adds them in their mail app.
  • Photos are links, not attachments.

To try it, approve the email on a listing, then open Email → "Download as email draft (.eml)".

Closes #112

commented

An approved email now downloads as a .eml draft the agent opens in their own mail app. A draft answers 409. The new tests, the speed, contract, openapi, design and followup suites, and the design lint pass. I did not run the full suite or tsc, because the repo has no tsconfig.json, and there is no scripts/gate.ts or scripts/fast.ts to run either.

What changed

  • Route: GET /listings/:id/email.eml takes ?which=followup|reminder like the email page. It answers message/rfc822 with attachment; filename="<address-slug>.eml" and no-store.
  • Who can read it: it goes through the same getListing read as the email page, so there is no new contract cell.
  • Not approved: it answers 409 with "This email needs approving first: approve it on the listing, then download it."
  • Email page: the "Download as email draft (.eml)" link shows only when that email is approved.
  • Headers: the file carries From: with the agent's name and address, To: empty, the Subject:, X-Unsent: 1 and MIME-Version. Every line ends in CRLF.
  • Body: multipart/alternative with a plain-text part and an HTML part, both base64 UTF-8. The HTML has inline styles, brand colours read from the design tokens, and photos as absolute links using publicOrigin. There are no attachments.
  • Header safety:

- Header values lose CR/LF and other control characters. - Long values fold at 76 characters. - A non-ASCII subject becomes an RFC 2047 encoded word. - The From: address and display name drop angle brackets, commas and quotes.

  • New module: src/eml.ts is pure, with the listing, which email and the origin in and a string out.
  • README: a paragraph in a realtor's words: approve it, download it, open it, it is a draft in your mail app.

Tests in src/eml.test.ts:

  • A draft gives no file, and an approved email gives the headers above.
  • A subject containing \r\nBcc: comes out as Subject: Hi with no Bcc header. The subject parser (emailParts) already cuts at the first line, so the injection is stopped twice. I also tested the From: name, which is cleaned by the new code.
  • A non-ASCII subject is encoded.
  • The follow-up has an empty To: and the visitor's name and address appear nowhere in it.
  • Over HTTP: 409 for a draft, 200 with the right headers once approved, and the page link appears only after approval.

Not done

  • No recipients are filled in: the agent adds them in their mail app.
  • Photos are links, not attachments.

To try it, approve the email on a listing, then open Email → "Download as email draft (.eml)".

Changes requested — before the latest push

Works on the route and the headers are safe, but one miss on the list: no terminal door.

  1. (fix first) Both doors: src/tty.ts has nothing for the .eml. The page offers 'Download as email draft', and the terminal previews the same emails (p/P/O) and the launch kit (k, which shows the file as it would read). Add the equivalent: in the email preview, once approved, show the file text from eml() (or its headers), and say in the tty help line how to get it. A draft shows the same 'needs approving first' sentence. Add a tty test, and mention the terminal in the README paragraph.
  2. Display name: words() puts RFC 2047 encoded words inside a quoted string in From:, which RFC 2047 forbids; mail apps show =?UTF-8?B?...?= literally. Use the encoded word unquoted for a non-ASCII name (quote only plain ASCII).
  3. Filename is the same for blast, follow-up and reminder, so downloads collide in the Downloads folder (name-(1).eml). The issue's <address-slug>.eml is satisfied, but suffix -followup / -reminder for the other two, with a test, or say why not.
  4. Route test only covers 409 and 200 for the blast; add a 200 for ?which=reminder or followup over HTTP and a guest/unreadable listing case proving the door follows permit.

Ticks are held back until the pull is approved.

commented

I've made all four changes on the same branch and pushed. The new tests and the speed, followup, contract, openapi, design and walkthrough suites pass, and scripts/lint-design.ts is clean. I did not run the full suite or tsc; the repo has no tsconfig.json.

  1. Terminal door:

- Key: E in an email preview (p, P or O) shows the .eml file's headers once the email is approved. A draft shows the same "needs approving first" sentence the route gives, kept as one constant in src/eml.ts that both use. - Help and docs: the preview's help line names the key, the src/tty.ts header comment documents it, and the README paragraph mentions the terminal. - What it shows: headers only, not the base64 body parts. esc leaves the whole preview and returns to the asset list, as it does from any preview. - Test: a new tty test covers both the draft and approved cases.

  1. Display name: a non-ASCII From: name is now bare encoded words with no quotes. A plain-ASCII name stays quoted. A new test checks both.
  2. File names: the blast stays <address-slug>.eml. The other two are <address-slug>-followup.eml and <address-slug>-reminder.eml, through one emlFilename function that the route and the test both use.
  3. Route tests: these now cover ?which=followup and ?which=reminder over HTTP, each 409 before approval and 200 after, with the right file name and an empty To:. A guest with no identity gets 200 for an approved email, because permit lets anyone read a listing. A listing id that doesn't exist gets 404.

I read "guest/unreadable listing" as the guest case plus the unknown id. listing read is open to everyone in contract.json, so there is no listing a guest can't read. A guest getting 200 is therefore correct, and I did not add a contract cell.

Approved

All four earlier asks are met: the terminal door (E in the email preview, with a test), the non-ASCII From name as bare encoded words, distinct followup/reminder file names, and route tests for both variants including a guest and an unknown id. No contract cell is loosened. The preview serves an .eml for the sample listing. Nothing new to ask.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs