13 KiB
13 KiB
Epic 5: Digest Assembly & Email Dispatch
Goal: Assemble the collected story data and summaries from local files, format them into a readable HTML email digest, and send the email using Nodemailer with configured credentials. Implement a stage testing utility for emailing with a dry-run option.
Story List
Story 5.1: Implement Email Content Assembler
- User Story / Goal: As a developer, I want a module that reads the persisted story metadata (
_data.json) and summaries (_summary.json) from a specified directory, consolidating the necessary information needed to render the email digest. - Detailed Requirements:
- Create a new module:
src/email/contentAssembler.ts. - Define a TypeScript type/interface
DigestDatarepresenting the data needed per story for the email template:{ storyId: string, title: string, hnUrl: string, articleUrl: string | null, articleSummary: string | null, discussionSummary: string | null }. - Implement an async function
assembleDigestData(dateDirPath: string): Promise<DigestData[]>. - The function should:
- Use Node.js
fsto read the contents of thedateDirPath. - Identify all files matching the pattern
{storyId}_data.json. - For each
storyIdfound:- Read and parse the
{storyId}_data.jsonfile. Extracttitle,hnUrl, andurl(use asarticleUrl). Handle potential file read/parse errors gracefully (log and skip story). - Attempt to read and parse the corresponding
{storyId}_summary.jsonfile. Handle file-not-found or parse errors gracefully (treatarticleSummaryanddiscussionSummaryasnull). - Construct a
DigestDataobject for the story, including the extracted metadata and summaries (or nulls).
- Read and parse the
- Collect all successfully constructed
DigestDataobjects into an array. - Return the array. It should ideally contain 10 items if all previous stages succeeded.
- Use Node.js
- Log progress (e.g., "Assembling digest data from directory...", "Processing story {storyId}...") and any errors encountered during file processing using the logger.
- Create a new module:
- Acceptance Criteria (ACs):
- AC1: The
contentAssembler.tsmodule exists and exportsassembleDigestDataand theDigestDatatype. - AC2:
assembleDigestDatacorrectly reads_data.jsonfiles from the provided directory path. - AC3: It attempts to read corresponding
_summary.jsonfiles, correctly handling cases where the summary file might be missing or unparseable (resulting in null summaries for that story). - AC4: The function returns a promise resolving to an array of
DigestDataobjects, populated with data extracted from the files. - AC5: Errors during file reading or JSON parsing are logged, and the function returns data for successfully processed stories.
- AC1: The
Story 5.2: Create HTML Email Template & Renderer
- User Story / Goal: As a developer, I want a basic HTML email template and a function to render it with the assembled digest data, producing the final HTML content for the email body.
- Detailed Requirements:
- Define the HTML structure. This can be done using template literals within a function or potentially using a simple template file (e.g.,
src/email/templates/digestTemplate.html) andfs.readFileSync. Template literals are simpler for MVP. - Create a function
renderDigestHtml(data: DigestData[], digestDate: string): string(e.g., insrc/email/contentAssembler.tsor a newtemplater.ts). - The function should generate an HTML string with:
- A suitable title in the body (e.g.,
<h1>Hacker News Top 10 Summaries for ${digestDate}</h1>). - A loop through the
dataarray. - For each
storyindata:- Display
<h2><a href="${story.articleUrl || story.hnUrl}">${story.title}</a></h2>. - Display
<p><a href="${story.hnUrl}">View HN Discussion</a></p>. - Conditionally display
<h3>Article Summary</h3><p>${story.articleSummary}</p>only ifstory.articleSummaryis not null/empty. - Conditionally display
<h3>Discussion Summary</h3><p>${story.discussionSummary}</p>only ifstory.discussionSummaryis not null/empty. - Include a separator (e.g.,
<hr style="margin-top: 20px; margin-bottom: 20px;">).
- Display
- A suitable title in the body (e.g.,
- Use basic inline CSS for minimal styling (margins, etc.) to ensure readability. Avoid complex layouts.
- Return the complete HTML document as a string.
- Define the HTML structure. This can be done using template literals within a function or potentially using a simple template file (e.g.,
- Acceptance Criteria (ACs):
- AC1: A function
renderDigestHtmlexists that accepts the digest data array and a date string. - AC2: The function returns a single, complete HTML string.
- AC3: The generated HTML includes a title with the date and correctly iterates through the story data.
- AC4: For each story, the HTML displays the linked title, HN link, and conditionally displays the article and discussion summaries with headings.
- AC5: Basic separators and margins are used for readability. The HTML is simple and likely to render reasonably in most email clients.
- AC1: A function
Story 5.3: Implement Nodemailer Email Sender
- User Story / Goal: As a developer, I want a module to send the generated HTML email using Nodemailer, configured with credentials stored securely in the environment file.
- Detailed Requirements:
- Add Nodemailer dependencies:
npm install nodemailer @types/nodemailer --save-prod. - Add required configuration variables to
.env.example(and local.env):EMAIL_HOST,EMAIL_PORT(e.g., 587),EMAIL_SECURE(e.g.,falsefor STARTTLS on 587,truefor 465),EMAIL_USER,EMAIL_PASS,EMAIL_FROM(e.g.,"Your Name <you@example.com>"),EMAIL_RECIPIENTS(comma-separated list). - Create a new module:
src/email/emailSender.ts. - Implement an async function
sendDigestEmail(subject: string, htmlContent: string): Promise<boolean>. - Inside the function:
- Load the
EMAIL_*variables from the config module. - Create a Nodemailer transporter using
nodemailer.createTransportwith the loaded config (host, port, secure flag, auth: { user, pass }). - Verify transporter configuration using
transporter.verify()(optional but recommended). Log verification success/failure. - Parse the
EMAIL_RECIPIENTSstring into an array or comma-separated string suitable for thetofield. - Define the
mailOptions:{ from: EMAIL_FROM, to: parsedRecipients, subject: subject, html: htmlContent }. - Call
await transporter.sendMail(mailOptions). - If
sendMailsucceeds, log the success message including themessageIdfrom the result. Returntrue. - If
sendMailfails (throws error), log the error using the logger. Returnfalse.
- Load the
- Add Nodemailer dependencies:
- Acceptance Criteria (ACs):
- AC1:
nodemailerand@types/nodemailerdependencies are added. - AC2:
EMAIL_*variables are defined in.env.exampleand loaded from config. - AC3:
emailSender.tsmodule exists and exportssendDigestEmail. - AC4:
sendDigestEmailcorrectly creates a Nodemailer transporter using configuration from.env. Transporter verification is attempted (optional AC). - AC5: The
tofield is correctly populated based onEMAIL_RECIPIENTS. - AC6:
transporter.sendMailis called with correctfrom,to,subject, andhtmloptions. - AC7: Email sending success (including message ID) or failure is logged clearly.
- AC8: The function returns
trueon successful sending,falseotherwise.
- AC1:
Story 5.4: Integrate Email Assembly and Sending into Main Workflow
- User Story / Goal: As a developer, I want the main application workflow (
src/index.ts) to orchestrate the final steps: assembling digest data, rendering the HTML, and triggering the email send after all previous stages are complete. - Detailed Requirements:
- Modify the main execution flow in
src/index.ts. - Import
assembleDigestData,renderDigestHtml,sendDigestEmail. - Execute these steps after the main loop (where stories are fetched, scraped, summarized, and persisted) completes:
- Log "Starting final digest assembly and email dispatch...".
- Determine the path to the current date-stamped output directory.
- Call
const digestData = await assembleDigestData(dateDirPath). - Check if
digestDataarray is not empty.- If yes:
- Get the current date string (e.g., 'YYYY-MM-DD').
const htmlContent = renderDigestHtml(digestData, currentDate).const subject = \BMad Hacker Daily Digest - ${currentDate}``.const emailSent = await sendDigestEmail(subject, htmlContent).- Log the final outcome based on
emailSent("Digest email sent successfully." or "Failed to send digest email.").
- If no (
digestDatais empty or assembly failed):- Log an error: "Failed to assemble digest data or no data found. Skipping email."
- If yes:
- Log "BMad Hacker Daily Digest process finished."
- Modify the main execution flow in
- Acceptance Criteria (ACs):
- AC1: Running
npm run devexecutes all stages (Epics 1-4) and then proceeds to email assembly and sending. - AC2:
assembleDigestDatais called correctly with the output directory path after other processing is done. - AC3: If data is assembled,
renderDigestHtmlandsendDigestEmailare called with the correct data, subject, and HTML. - AC4: The final success or failure of the email sending step is logged.
- AC5: If
assembleDigestDatareturns no data, email sending is skipped, and an appropriate message is logged. - AC6: The application logs a final completion message.
- AC1: Running
Story 5.5: Implement Stage Testing Utility for Emailing
- User Story / Goal: As a developer, I want a separate script/command to test the email assembly, rendering, and sending logic using persisted local data, including a crucial
--dry-runoption to prevent accidental email sending during tests. - Detailed Requirements:
- Add
yargsdependency for argument parsing:npm install yargs @types/yargs --save-dev. - Create a new standalone script file:
src/stages/send_digest.ts. - Import necessary modules:
fs,path,logger,config,assembleDigestData,renderDigestHtml,sendDigestEmail,yargs. - Use
yargsto parse command-line arguments, specifically looking for a--dry-runboolean flag (defaulting tofalse). Allow an optional argument for specifying the date-stamped directory, otherwise default to current date. - The script should:
- Initialize logger, load config.
- Determine the target date-stamped directory path (from arg or default). Log the target directory.
- Call
await assembleDigestData(dateDirPath). - If data is assembled and not empty:
- Determine the date string for the subject/title.
- Call
renderDigestHtml(digestData, dateString)to get HTML. - Construct the subject string.
- Check the
dryRunflag:- If
true: Log "DRY RUN enabled. Skipping actual email send.". Log the subject. Save thehtmlContentto a file in the target directory (e.g.,_digest_preview.html). Log that the preview file was saved. - If
false: Log "Live run: Attempting to send email...". Callawait sendDigestEmail(subject, htmlContent). Log success/failure based on the return value.
- If
- If data assembly fails or is empty, log the error.
- Add script to
package.json:"stage:email": "ts-node src/stages/send_digest.ts --". The--allows passing arguments like--dry-run.
- Add
- Acceptance Criteria (ACs):
- AC1: The file
src/stages/send_digest.tsexists.yargsdependency is added. - AC2: The script
stage:emailis defined inpackage.jsonallowing arguments. - AC3: Running
npm run stage:email -- --dry-runreads local data, renders HTML, logs the intent, saves_digest_preview.htmllocally, and does not callsendDigestEmail. - AC4: Running
npm run stage:email(without--dry-run) reads local data, renders HTML, and does callsendDigestEmail, logging the outcome. - AC5: The script correctly identifies and acts upon the
--dry-runflag. - AC6: Logs clearly distinguish between dry runs and live runs and report success/failure.
- AC7: The script operates using only local files and the email configuration/service; it does not invoke prior pipeline stages (Algolia, scraping, Ollama).
- AC1: The file
Change Log
| Change | Date | Version | Description | Author |
|---|---|---|---|---|
| Initial Draft | 2025-05-04 | 0.1 | First draft of Epic 5 | 2-pm |