Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jobs: templating for email action #1326

Merged
merged 14 commits into from
Sep 9, 2024
85 changes: 85 additions & 0 deletions src/common/email-templates/job-template-simplified.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<html>
<head>
<style type="text/css">
body {
font-family: Helvetica, sans-serif;
background: #f0f0f0;
margin: 1em;
}

.container {
margin: auto;
max-width: 50em;
background: white;
padding: 2em;
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2),
0px 2px 2px 0px rgba(0, 0, 0, 0.14),
0px 1px 5px 0px rgba(0, 0, 0, 0.12);
}
.job-id-container {
padding: 1em 0;
}
.footer {
font-size: 0.8em;
color: #666;
font-style: italic;
padding-top: 2em;
}
.link {
text-decoration: none;
color: #de9300;
}
.link:hover {
color: #fea901;
}

table {
border-collapse: collapse;
border-spacing: 10px;

tr {
border-top: 1px solid #eee;
}
td {
padding: 0.5em;
}

.key{
font-weight: bold;
color: #666;
vertical-align: top;
margin-top: 0;
}
}
</style>
</head>
<body>
<div class="container">
<div>
Your {{type}} job has been submitted and will be processed as soon as possible.<br>
You will be notified by email as soon as the job is completed.
</div>
<div class="job-id-container">
<b>Job id:</b> {{id}}
</div>
{{#if jobParams.datasetIds}}
<p><b>Job will be perfomed on the following dataset(s):</b></p>
<table>
{{#each jobParams.datasetIds}}
<tr style="background-color: lightblue;">
<td class="key">
{{this}}
</td>
</tr>
{{/each}}
</table>
{{/if}}

<div class="footer">
This email was automatically generated by
<a class="link" href="https://scicatproject.github.io/">SciCat</a>.
Please do not reply.
</div>
</div>
</body>
</html>
86 changes: 65 additions & 21 deletions src/jobs/actions/emailaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,27 @@
* This is intended as an example of the JobAction interface
*
*/
import { readFileSync } from "fs";
import { compile, TemplateDelegate } from "handlebars";
import { createTransport, Transporter } from "nodemailer";

Check warning on line 8 in src/jobs/actions/emailaction.ts

View workflow job for this annotation

GitHub Actions / eslint

'createTransport' is defined but never used
import { Logger, NotFoundException } from "@nestjs/common";
import { JobAction } from "../config/jobconfig";
import { JobClass } from "../schemas/job.schema";
import { createTransport, Transporter } from "nodemailer";
import { compile, TemplateDelegate } from "handlebars";
import configuration from "src/config/configuration";

Check warning on line 12 in src/jobs/actions/emailaction.ts

View workflow job for this annotation

GitHub Actions / eslint

'configuration' is defined but never used

// Handlebar options for JobClass templates
const jobTemplateOptions = {
allowedProtoProperties: {
id: true,
type: true,
statusCode: true,
statusMessage: true,
createdBy: true,
jobParams: true,
contactEmail: true,
},
allowProtoPropertiesByDefault: false, // limit accessible fields for security
};

type MailOptions = {
to: string;
Expand All @@ -16,17 +32,23 @@
text?: string;
};

type Auth = {
user: string;
password: string;
};

/**
* Send an email following a job
*/
export class EmailJobAction<T> implements JobAction<T> {
public static readonly actionType = "email";

private mailService: Transporter;
private toTemplate: TemplateDelegate<JobClass>;
private from: string;
private auth: Auth | object = {};
private subjectTemplate: TemplateDelegate<JobClass>;
private bodyTemplate?: TemplateDelegate<JobClass>;

public static readonly actionType = "email";
private bodyTemplate: TemplateDelegate<JobClass>;

getActionType(): string {
return EmailJobAction.actionType;
Expand All @@ -38,31 +60,52 @@
"EmailJobAction",
);

if (!data["mailer"]) {
throw new NotFoundException("Param 'mailer' is undefined");
if (data["auth"]) {
// check optional auth field
function CheckAuthDefinition(obj: object): obj is Auth {
return (
Object.keys(obj).length == 2 && "user" in obj && "password" in obj
);
}

if (!CheckAuthDefinition(data["auth"])) {
throw new NotFoundException(
"Param 'auth' should contain fields 'user' and 'password' only.",
);
}
this.auth = data["auth"] as Auth;
}
if (!data["to"]) {
throw new NotFoundException("Param 'to' is undefined");
}
if (!data["from"]) {
throw new NotFoundException("Param 'from' is undefined");
}
if (typeof data["from"] !== "string") {
throw new TypeError("from should be a string");
}
if (!data["subject"]) {
throw new NotFoundException("Param 'subject' is undefined");
}
if (!data["body"]) {
throw new NotFoundException("Param 'body' is undefined");
if (!data["bodyTemplateFile"]) {
throw new NotFoundException("Param 'bodyTemplateFile' is undefined");
}
Logger.log("EmailJobAction parameters are valid.", "EmailJobAction");

this.mailService = createTransport(data["mailer"]);
// const mailerConfig = configuration().smtp;
// this.mailService = createTransport({
// host: mailerConfig.host,
// port: mailerConfig.port,
// secure: mailerConfig.secure,
// auth: this.auth
// } as any);

this.from = data["from"] as string;
this.toTemplate = compile(data["to"]);
this.from = data["from"];
this.subjectTemplate = compile(data["subject"]);
this.bodyTemplate = compile(data["body"]);

const templateFile = readFileSync(
data["bodyTemplateFile"] as string,
"utf8",
);
this.bodyTemplate = compile(templateFile);
}

async performJob(job: JobClass) {
Expand All @@ -73,13 +116,14 @@

// Fill templates
const mail: MailOptions = {
to: this.toTemplate(job),
to: this.toTemplate(job, jobTemplateOptions),
from: this.from,
subject: this.subjectTemplate(job),
subject: this.subjectTemplate(job, jobTemplateOptions),
};
if (this.bodyTemplate) {
mail.text = this.bodyTemplate(job);
}
await this.mailService.sendMail(mail);
mail.text = this.bodyTemplate(job, jobTemplateOptions);
Logger.log(mail);

// Send the email
// await this.mailService.sendMail(mail);
}
}
11 changes: 11 additions & 0 deletions src/jobs/config/jobConfig.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@
"exchange": "jobs.write",
"queue": "client.jobs.write",
"key": "jobqueue"
},
{
"actionType": "email",
"auth": {
"user": "user",
"password": "password"
},
"to": "{{contactEmail}}",
"from": "from",
"subject": "[SciCat] Your {{type}} job was submitted successfully",
"bodyTemplateFile": "src/common/email-templates/job-template-simplified.html"
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions src/jobs/jobs.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
HttpException,
Req,
ForbiddenException,
UseInterceptors,

Check warning on line 15 in src/jobs/jobs.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

'UseInterceptors' is defined but never used
} from "@nestjs/common";
import { Request } from "express";
import { FilterQuery } from "mongoose";
import { JobsService } from "./jobs.service";
import { CreateJobDto, CreateJobDtoWithConfig } from "./dto/create-job.dto";

Check warning on line 20 in src/jobs/jobs.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

'CreateJobDtoWithConfig' is defined but never used
import { StatusUpdateJobDto } from "./dto/status-update-job.dto";
import { PoliciesGuard } from "src/casl/guards/policies.guard";
import { CheckPolicies } from "src/casl/decorators/check-policies.decorator";
Expand Down Expand Up @@ -46,7 +46,7 @@
filterDescriptionSimplified,
filterExampleSimplified,
} from "src/common/utils";
import { JobCreateInterceptor } from "./interceptors/job-create.interceptor";

Check warning on line 49 in src/jobs/jobs.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

'JobCreateInterceptor' is defined but never used
import { JobAction } from "./config/jobconfig";

@ApiBearerAuth()
Expand Down Expand Up @@ -401,6 +401,10 @@
let datasetIds: string[] = [];
if (JobsConfigSchema.DatasetIds in jobCreateDto.jobParams) {
datasetIds = await this.checkDatasetIds(jobCreateDto.jobParams);
jobInstance.jobParams = {
...jobInstance.jobParams,
[JobsConfigSchema.DatasetIds]: datasetIds,
};
}
if (user) {
// the request comes from a user who is logged in.
Expand Down
Loading