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

Send email when new passlist entry is added #2087

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a776727
dependencies upgrade
rfontanarosa Nov 12, 2024
2057ebb
added oncreatepasslistentry firebase function
rfontanarosa Nov 12, 2024
24609b4
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Nov 14, 2024
592d68a
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Nov 19, 2024
c7eabb4
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Nov 21, 2024
d8f91f3
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Nov 27, 2024
8ec872f
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Nov 29, 2024
6ad5180
added mailserver configuration and sendMail function (nodemailer)
rfontanarosa Nov 29, 2024
ff2daeb
moved logic into mail-service
rfontanarosa Dec 3, 2024
a3ab59f
changed mail server structure
rfontanarosa Dec 6, 2024
cfcd2e6
fixed datastore sync issue
rfontanarosa Dec 9, 2024
aa495bf
removed servers sub-collection in favour of servers array, fixed send…
rfontanarosa Dec 9, 2024
dde03bf
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
gino-m Dec 10, 2024
b6465f6
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Dec 11, 2024
a86ac53
added mail templating system
rfontanarosa Dec 11, 2024
3852b7b
removed servers[] in favour of server
rfontanarosa Dec 11, 2024
5b5fce2
fixed wrong documentdata type
rfontanarosa Dec 11, 2024
294504d
fixed some error messages and existing checks
rfontanarosa Dec 11, 2024
caec9e5
fixed typo
rfontanarosa Dec 11, 2024
cbaeb03
missing function rename
rfontanarosa Dec 11, 2024
0f594cd
added some comments
rfontanarosa Dec 11, 2024
af407a7
adde other comments
rfontanarosa Dec 11, 2024
328bae0
log error instead of raising an exception when server config doesn't …
rfontanarosa Dec 13, 2024
3ee13e8
added html email sanitizer
rfontanarosa Dec 13, 2024
5dab8a4
fixed missing assignments
rfontanarosa Dec 13, 2024
d376255
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
gino-m Dec 13, 2024
5522d57
fixed typo, missing mail configuration error
rfontanarosa Dec 16, 2024
f05b4ff
Merge branch 'rfontanarosa/2077/feature-user-access-granted-notificat…
rfontanarosa Dec 16, 2024
6990f3e
added some tsdoc, improved mail send error management
rfontanarosa Dec 16, 2024
b86960f
changed some console.error into console.debug
rfontanarosa Dec 16, 2024
abc45a3
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Dec 23, 2024
3874319
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Jan 7, 2025
5532549
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Feb 4, 2025
4ac9ce8
added some tests
rfontanarosa Feb 4, 2025
973fc34
Merge branch 'master' into rfontanarosa/2077/feature-user-access-gran…
rfontanarosa Feb 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@
"cors": "2.8.5",
"csv-parser": "2.3.3",
"firebase-admin": "12.1.0",
"firebase-functions": "^5.0.1",
"firebase-functions": "^5.1.1",
"google-auth-library": "6.1.3",
"googleapis": "64.0.0",
"http-status-codes": "1.4.0",
"immutable": "^4.3.6",
"jsonstream-ts": "1.3.6",
"module-alias": "^2.2.2",
"nodemailer": "^6.9.16",
"requests": "0.3.0",
"ts-node": "^10.9.1"
},
Expand All @@ -53,6 +54,7 @@
"@types/geojson": "^7946.0.14",
"@types/jasmine": "^4.3.5",
"@types/jsonstream": "0.8.30",
"@types/nodemailer": "^6.4.16",
"@types/terraformer__wkt": "2.0.0",
"firebase-functions-test": "^3.3.0",
"firebase-tools": "13.6.0",
Expand Down
15 changes: 15 additions & 0 deletions functions/src/common/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
*/

import {Datastore} from './datastore';
import {MailService} from './mail-service';
import {initializeApp, getApp} from 'firebase-admin/app';
import {getFirestore} from 'firebase-admin/firestore';

let datastore: Datastore | undefined;
let mailService: MailService | undefined;

export function initializeFirebaseApp() {
try {
Expand All @@ -36,6 +38,19 @@ export function getDatastore(): Datastore {
return datastore;
}

export async function getMailService(): Promise<MailService | undefined> {
if (!mailService) {
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
try {
const config = await MailService.gerMailServerConfig(getDatastore());

mailService = new MailService(config);
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
console.error(e);
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
}
}
return mailService;
}

export function resetDatastore() {
datastore = undefined;
}
24 changes: 24 additions & 0 deletions functions/src/common/datastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/
type pseudoGeoJsonGeometry = {
type: string;
coordinates: any;

Check warning on line 32 in functions/src/common/datastore.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type
};

/**
Expand All @@ -37,11 +37,21 @@
*/
export const config = () => 'config';

/**
* Returns the path of passlist entry doc with the specified id.
*/
export const passlistEntry = (entryId: string) => `passlist/${entryId}`;

/**
* Returns the path of integrations doc.
*/
export const integrations = () => config() + '/integrations';

/**
* Returns the path of mail doc.
*/
export const mail = () => config() + '/mail';

/**
* Returns path to survey colection. This is a function for consistency with other path functions.
*/
Expand Down Expand Up @@ -81,6 +91,12 @@
export const submission = (surveyId: string, submissionId: string) =>
submissions(surveyId) + '/' + submissionId;

/**
* Returns the path of template doc with the specified id.
*/
export const mailTemplate = (templateId: string) =>
`${mail()}/templates/${templateId}`;

export class Datastore {
private db_: firestore.Firestore;

Expand Down Expand Up @@ -124,6 +140,10 @@
return this.db_.collection(integrations() + '/propertyGenerators').get();
}

fetchMail() {
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
return this.db_.doc(mail()).get();
}

fetchSurvey(surveyId: string) {
return this.db_.doc(survey(surveyId)).get();
}
Expand Down Expand Up @@ -188,6 +208,10 @@
}
}

fetchMailTemplate(templateId: string) {
return this.fetchDoc_(mailTemplate(templateId));
}

fetchSheetsConfig(surveyId: string) {
return this.fetchDoc_(`${survey(surveyId)}/sheets/config`);
}
Expand Down Expand Up @@ -220,7 +244,7 @@
await loiRef.update({[l.properties]: loiDoc[l.properties]});
}

static toFirestoreMap(geometry: any) {

Check warning on line 247 in functions/src/common/datastore.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type
return Object.fromEntries(
Object.entries(geometry).map(([key, value]) => [
key,
Expand All @@ -229,7 +253,7 @@
);
}

static toFirestoreValue(value: any): any {

Check warning on line 256 in functions/src/common/datastore.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type

Check warning on line 256 in functions/src/common/datastore.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type
if (value === null) {
return null;
}
Expand Down Expand Up @@ -257,7 +281,7 @@
*
* @returns GeoJSON geometry object (with geometry as list of lists)
*/
static fromFirestoreMap(geoJsonGeometry: any): any {

Check warning on line 284 in functions/src/common/datastore.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type

Check warning on line 284 in functions/src/common/datastore.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type
const geometryObject = geoJsonGeometry as pseudoGeoJsonGeometry;
if (!geometryObject) {
throw new Error(
Expand All @@ -272,7 +296,7 @@
return geometryObject;
}

static fromFirestoreValue(coordinates: any) {

Check warning on line 299 in functions/src/common/datastore.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type
if (coordinates instanceof GeoPoint) {
// Note: GeoJSON coordinates are in lng-lat order.
return [coordinates.longitude, coordinates.latitude];
Expand All @@ -281,7 +305,7 @@
if (typeof coordinates !== 'object') {
return coordinates;
}
const result = new Array<any>(coordinates.length);

Check warning on line 308 in functions/src/common/datastore.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type

Object.entries(coordinates).map(([i, nestedValue]) => {
const index = Number.parseInt(i);
Expand Down
85 changes: 85 additions & 0 deletions functions/src/common/mail-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright 2024 The Ground Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as nodemailer from 'nodemailer';
import {Datastore} from './datastore';

type MailConfig = {
server?: MailServerConfig;
};

type MailServerConfig = {
id: string;
host: string;
port: number;
username: string;
password: string;
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
sender?: string;
};

export interface MailServiceEmail {
to: string;
subject: string;
html: string;
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
}

export class MailService {
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
private transporter_: nodemailer.Transporter;
private sender_: string;

constructor(mailServerConfig: MailServerConfig) {
const {host, port, username, password, sender} = mailServerConfig;

this.sender_ = sender || username;

this.transporter_ = nodemailer.createTransport({
host,
port,
auth: {user: username, pass: password},
sender: this.sender_,
});
}

async sendMail(email: MailServiceEmail) {
await new Promise<void>((resolve, reject) => {
this.transporter_.sendMail(
{from: this.sender_, ...email},
(error: Error | any, _info: any) => {

Check warning on line 60 in functions/src/common/mail-service.ts

View workflow job for this annotation

GitHub Actions / Check

Unexpected any. Specify a different type

Check warning on line 60 in functions/src/common/mail-service.ts

View workflow job for this annotation

GitHub Actions / Check

'_info' is defined but never used
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
if (error) {
// 501 and 550 are errors from the mail server: email address not found
if (error.responseCode === 501 || error.responseCode === 550) {
reject(new Error(error.response));
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
} else {
reject(error);
}
} else {
resolve();
}
}
);
});
}

static async gerMailServerConfig(db: Datastore): Promise<MailServerConfig> {
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
const mail = await db.fetchMail();
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
if (!mail.exists) throw new Error('Unable to find Mail Configuration');
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
const mailConfig = mail.data() as MailConfig;
const mailServerConfig = mailConfig.server;
if (!mailServerConfig)
throw new Error('Unable to find Mail Server Configuration');
return mailServerConfig;
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
}
}
19 changes: 19 additions & 0 deletions functions/src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright 2024 The Ground Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export function stringFormat(s: string, ...args: any[]): string {
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
return s.replace(/\{(\d+)\}/g, (_, index) => args[index] || `{${index}}`);
}
10 changes: 9 additions & 1 deletion functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ import {importGeoJsonCallback} from './import-geojson';
import {exportCsvHandler} from './export-csv';
import {onCall} from 'firebase-functions/v2/https';
import {onCreateLoiHandler} from './on-create-loi';
import {onCreatePasslistEntryHandler} from './on-create-passlist-entry';
import {onWriteJobHandler} from './on-write-job';
import {onWriteLoiHandler} from './on-write-loi';
import {onWriteSubmissionHandler} from './on-write-submission';
import {onWriteSurveyHandler} from './on-write-survey';
import {job, loi, submission, survey} from './common/datastore';
import {job, loi, passlistEntry, submission, survey} from './common/datastore';
import {initializeFirebaseApp} from './common/context';

// Ensure Firebase is initialized.
initializeFirebaseApp();

/** Template for passlist entry write triggers capturing passlist entry id. */
const passlistEntryPathTemplate = passlistEntry('{entryId}');

/** Template for job write triggers capturing survey and job id. */
const jobPathTemplate = job('{surveyId}', '{jobId}');

Expand All @@ -49,6 +53,10 @@ export const profile = {
refresh: onCall(request => handleProfileRefresh(request)),
};

export const onCreatePasslistEntry = functions.firestore
.document(passlistEntryPathTemplate)
.onCreate(onCreatePasslistEntryHandler);

export const importGeoJson = onHttpsRequestAsync(importGeoJsonCallback);

export const exportCsv = onHttpsRequest(exportCsvHandler);
Expand Down
46 changes: 46 additions & 0 deletions functions/src/on-create-passlist-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright 2024 The Ground Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {EventContext} from 'firebase-functions';
import {QueryDocumentSnapshot} from 'firebase-functions/v1/firestore';
import {getDatastore, getMailService} from './common/context';
import {MailServiceEmail} from './common/mail-service';
import {stringFormat} from './common/utils';

export async function onCreatePasslistEntryHandler(
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
_: QueryDocumentSnapshot,
context: EventContext
) {
const entryId = context!.params.entryId;

const db = getDatastore();

const template = await db.fetchMailTemplate('passlisted');

if (template) {
rfontanarosa marked this conversation as resolved.
Show resolved Hide resolved
const {subject, html} = template;

const mail = {
to: entryId,
subject,
html: stringFormat(html || '', [entryId]),
} as MailServiceEmail;

const mailService = await getMailService();

await mailService?.sendMail(mail);
}
}
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"dependencies": {
"@google-cloud/firestore": "^7.7.0",
"@ground/proto": "file:../proto",
"firebase-functions": "^5.0.1",
"firebase-functions": "^5.1.1",
"immutable": "^4.3.6",
"long": "^5.2.3",
"protobufjs": "^7.3.0"
Expand Down
Loading
Loading