Skip to content

Commit

Permalink
Feat: Attachments
Browse files Browse the repository at this point in the history
  • Loading branch information
Gum-Joe committed Aug 3, 2024
1 parent 3e1a1b2 commit 33e61cb
Show file tree
Hide file tree
Showing 31 changed files with 792 additions and 198 deletions.
19 changes: 11 additions & 8 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"singleQuote": false,
"trailingComma": "all",
"useTabs": false,
"tabWidth": 4,
"printWidth": 120,
"importOrderSeparation": true,
"importOrder": ["<THIRD_PARTY_MODULES>", "^[./]"]
}
"singleQuote": false,
"trailingComma": "all",
"useTabs": false,
"tabWidth": 4,
"printWidth": 100,
"importOrderSeparation": true,
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^[./]"
]
}
15 changes: 13 additions & 2 deletions email/libmailmerge/src/mailer/defaultMailer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Mail from "nodemailer/lib/mailer";

import { EmailString } from "../util/types";
import Mailer from "./mailer";

Expand All @@ -7,7 +9,9 @@ import Mailer from "./mailer";
export const getDefaultMailer = () =>
new Mailer(
process.env["DOCSOC_SMTP_SERVER"] ?? "smtp-mail.outlook.com",
587,
process.env["DOCSOC_SMTP_PORT"] && isFinite(parseInt(process.env["DOCSOC_SMTP_PORT"]))
? parseInt(process.env["DOCSOC_SMTP_PORT"])
: 587,
process.env["DOCSOC_SMTP_USERNAME"] ?? "[email protected]",
process.env["DOCSOC_SMTP_PASSWORD"] ?? "password",
);
Expand All @@ -17,7 +21,13 @@ export const getDefaultMailer = () =>
*
* Pass it an instance of a Mailer from {@link getDefaultMailer} to use the default mailer.
*/
export const defaultMailer = (to: EmailString[], subject: string, html: string, mailer: Mailer): Promise<void> =>
export const defaultMailer = (
to: EmailString[],
subject: string,
html: string,
mailer: Mailer,
attachments: Mail.Options["attachments"] = [],
): Promise<void> =>
mailer.sendMail(
Mailer.makeFromLineFromEmail(
process.env["DOCSOC_SENDER_NAME"] ?? "DoCSoc",
Expand All @@ -28,4 +38,5 @@ export const defaultMailer = (to: EmailString[], subject: string, html: string,
to,
subject,
html,
attachments,
);
16 changes: 13 additions & 3 deletions email/libmailmerge/src/mailer/mailer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createLogger } from "@docsoc/util";
import { validate } from "email-validator";
import { convert } from "html-to-text";
import nodemailer from "nodemailer";
import Mail from "nodemailer/lib/mailer";

import { createLogger } from "@docsoc/util";
import { EmailString, FromEmail } from "../util/types";

const logger = createLogger("mailer");
Expand All @@ -16,7 +17,12 @@ const logger = createLogger("mailer");
* @param password SMTP server password (usually your microsoft 365 password)
*/
export default class Mailer {
constructor(private host: string, private port: number, private username: string, private password: string) {}
constructor(
private host: string,
private port: number,
private username: string,
private password: string,
) {}

private transporter = nodemailer.createTransport({
host: this.host,
Expand All @@ -33,6 +39,7 @@ export default class Mailer {
to: string[],
subject: string,
html: string,
attachments: Mail.Options["attachments"] = [],
text: string = convert(html),
): Promise<void> {
const info = await this.transporter.sendMail({
Expand All @@ -41,10 +48,13 @@ export default class Mailer {
subject, // Subject line
text: text, // plain text body
html: html, // html body
attachments,
});

logger.debug(
`Sent email to ${to.join(", ")}, from ${from}, subject: ${subject}, message id: ${info.messageId}`,
`Sent email to ${to.join(", ")}, from ${from}, subject: ${subject}, message id: ${
info.messageId
}`,
);
}

Expand Down
109 changes: 83 additions & 26 deletions email/libmailmerge/src/previews/sidecarData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ const logger = createLogger("docsoc.sidecar");
* @param fileNamer A function that generates a filename for a record
* @returns
*/
export const getRecordPreviewPrefix = (record: CSVRecord, fileNamer: (record: CSVRecord) => string) =>
`${fileNamer(record)}`;
export const getRecordPreviewPrefix = (
record: CSVRecord,
fileNamer: (record: CSVRecord) => string,
) => `${fileNamer(record)}`;

/**
* Generate predicable prefixes for preview names (including record specific part
Expand All @@ -42,7 +44,8 @@ export const getRecordPreviewPrefixForIndividual = (
fileNamer: (record: CSVRecord) => string,
templateEngine: string,
preview: TemplatePreview,
) => [getRecordPreviewPrefix(record, fileNamer), templateEngine, preview.name].join(PARTS_SEPARATOR);
) =>
[getRecordPreviewPrefix(record, fileNamer), templateEngine, preview.name].join(PARTS_SEPARATOR);

/**
* Generate the filename for the metadata file for a record
Expand All @@ -54,44 +57,98 @@ export const getRecordPreviewPrefixForIndividual = (
* getRecordPreviewPrefixForMetadata(record, fileNamer)
* // => "file_1-metadata.json"
*/
export const getRecordPreviewPrefixForMetadata = (record: CSVRecord, fileNamer: (record: CSVRecord) => string) =>
`${getRecordPreviewPrefix(record, fileNamer)}${METADATA_FILE_SUFFIX}`;
export const getRecordPreviewPrefixForMetadata = (
record: CSVRecord,
fileNamer: (record: CSVRecord) => string,
) => `${getRecordPreviewPrefix(record, fileNamer)}${METADATA_FILE_SUFFIX}`;

type ValidRecordReturn = { valid: false; reason: string } | { valid: true };
/**
* Check a record is valid for use in mailmerge - specifically, that it has a valid email address and a subject.
* @param record __Mapped__ CSV Record to validate
*/
export const validateRecord = (record: CSVRecord): ValidRecordReturn => {
if (!Mailer.validateEmail(record["email"] as string)) {
return {
valid: false,
reason: "Invalid email address",
};
}

if (!record["subject"]) {
return {
valid: false,
reason: "No subject provided",
};
}

return {
valid: true,
};
};

/**
* Write the metadata for a record & its associated previews to a JSON file.
* @param record The record to write metadata for, mapped to the fields required by the template
* @param templateEngine The engine used to render the previews
* @param templateOptions The options given to the engine
* @param previews The previews rendered for the record
* @param sidecarData Sidecar data to write. Use {@link getSidecarMetadata} to get.
* @param fileNamer A function that generates a filename for a record
* @param previewsRoot The root directory to write the metadata to
* @returns
*/
export async function writeMetadata(
record: CSVRecord,
templateEngine: TEMPLATE_ENGINES,
templateOptions: TemplateEngineOptions,
previews: TemplatePreviews,
sidecarData: SidecarData,
fileNamer: (record: CSVRecord) => string,
previewsRoot: string,
): Promise<void> {
if (!Mailer.validateEmail(record["email"] as string)) {
logger.warn(`Skipping metadata write for ${fileNamer(record)} - invalid email address`);
return Promise.resolve();
}

if (!record["subject"]) {
logger.warn(`Skipping metadata write for ${fileNamer(record)} - no subject provided`);
const recordState = validateRecord(record);
if (!recordState.valid) {
logger.warn(
`Skipping metadata for ${fileNamer(record)} due to invalid record: ${
recordState.reason
}`,
);
return Promise.resolve();
}
const metadataFile = getRecordPreviewPrefixForMetadata(record, fileNamer);
logger.debug(`Writing metadata for ${fileNamer(record)} to ${metadataFile}`);
await writeSidecarFile(previewsRoot, metadataFile, sidecarData);
return Promise.resolve();
}

const sidecar: SidecarData = {
/**
* Generate sidecar metadata for a record & the previews generated from it.
* It is recommended you then store this info alongside the preview, e.g. in a JSON file
*
* The idea of sidecar metadata is to allow us to rerender previews again and again.
* @param fileNamer Function that given a record, provides the prefix to preppend the name of any generated preview files (including sidecar metadata)
* @param record The record to write metadata for, mapped to the fields required by the template. This is assumed to be a valid record (see {@link validateRecord})
* @param templateEngine Template engine used
* @param templateOptions Options for that template engine (included in the sidecar)
* @param attachments Array of paths to attachments to include in the final email, relative to the mailmerge workspace root (top level folder)
* @param previews Previews rendered for the record - we will remove the content field from it and include the rest of the preview object to the sidecar metadata
* @returns Sidecar metadata
*/
export function getSidecarMetadata(
fileNamer: (record: CSVRecord) => string,
record: CSVRecord,
templateEngine: TEMPLATE_ENGINES,
templateOptions: TemplateEngineOptions,
attachments: string[],
previews: TemplatePreviews,
): SidecarData {
return {
name: fileNamer(record),
record: record,
engine: templateEngine,
engineOptions: templateOptions,
files: previews.map((preview) => ({
filename: getRecordPreviewPrefixForIndividual(record, fileNamer, templateEngine, preview),
filename: getRecordPreviewPrefixForIndividual(
record,
fileNamer,
templateEngine,
preview,
),
engineData: {
...preview,
content: undefined,
Expand All @@ -101,18 +158,18 @@ export async function writeMetadata(
to: record["email"] as EmailString,
subject: record["subject"] as string,
},
attachments,
};

const metadataFile = getRecordPreviewPrefixForMetadata(record, fileNamer);
logger.debug(`Writing metadata for ${fileNamer(record)} to ${metadataFile}`);
await writeSidecarFile(previewsRoot, metadataFile, sidecar);
return Promise.resolve();
}

/**
* Write the sidecar metadata file for a record
*/
export async function writeSidecarFile(previewsRoot: string, metadataFile: string, sidecar: SidecarData) {
export async function writeSidecarFile(
previewsRoot: string,
metadataFile: string,
sidecar: SidecarData,
) {
await fs.writeFile(join(previewsRoot, metadataFile), JSON.stringify(sidecar, null, 4));
}

Expand Down
2 changes: 2 additions & 0 deletions email/libmailmerge/src/previews/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ export interface SidecarData {
to: EmailString;
subject: string;
};
/** Array of paths to attachments to include in the final email, relative to the mailmerge workspace root (top level folder) */
attachments: string[];
}
1 change: 1 addition & 0 deletions email/libmailmerge/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"types": [
"node"
],
Expand Down
Loading

0 comments on commit 33e61cb

Please sign in to comment.