Skip to content

Commit 40710db

Browse files
authored
Merge pull request #340 from FirebasePrivate/@ehesp/#310
Fixes to firestore-send-email #310
2 parents f0a4fc0 + 18e4d22 commit 40710db

File tree

7 files changed

+278
-62
lines changed

7 files changed

+278
-62
lines changed

firestore-send-email/POSTINSTALL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ You can test out this extension right away:
2222
**Note:** You can also use the [Firebase Admin SDK][admin_sdk] to add a document:
2323
```
2424
admin.firestore().collection('${param:MAIL_COLLECTION}').add({
25-
to: ['someone@example.com'],
25+
to: 'someone@example.com',
2626
message: {
2727
subject: 'Hello from Firebase!',
2828
text: 'This is the plaintext section of the email body.',
@@ -41,11 +41,11 @@ The top-level fields of the document supply the email sender and recipient infor
4141
4242
* **from:** The sender's email address. If not specified in the document, uses the configured "Default FROM address" parameter.
4343
* **replyTo:** The reply-to email address. If not specified in the document, uses the configured "Default REPLY-TO address" parameter.
44-
* **to:** An array containing the recipient email addresses.
44+
* **to:** A single recipient email address or an array containing multiple recipient email addresses.
4545
* **toUids:** An array containing the recipient UIDs.
46-
* **cc:** An array containing the CC recipient email addresses.
46+
* **cc:** A single recipient email address or an array containing multiple recipient email addresses.
4747
* **ccUids:** An array containing the CC recipient UIDs.
48-
* **bcc:** An array containing the BCC recipient email addresses.
48+
* **bcc:** A single recipient email address or an array containing multiple recipient email addresses.
4949
* **bccUids:** An array containing the BCC recipient UIDs.
5050
5151
**NOTE:** The `toUids`, `ccUids`, and `bccUids` options deliver emails based on user UIDs keyed to email addresses within a Cloud Firestore document. To use these recipient options, you need to specify a Cloud Firestore collection for the extension's "Users collection" parameter. The extension can then read the `email` field for each UID specified in the `toUids`, `ccUids`, and/or `bccUids` fields.

firestore-send-email/PREINSTALL.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,19 @@ Use this extension to render and send emails that contain the information from d
22

33
Adding a document triggers this extension to send an email built from the document's fields. The document's top-level fields specify the email sender and recipients, including `to`, `cc`, and `bcc` options (each supporting UIDs). The document's `message` field specifies the other email elements, like subject line and email body (either plaintext or HTML)
44

5-
Here's a basic example documment write that would trigger this extension:
5+
Here's a basic example document write that would trigger this extension:
66

77
```js
88
admin.firestore().collection('mail').add({
9-
to: ['someone@example.com'],
9+
to: 'someone@example.com',
1010
message: {
1111
subject: 'Hello from Firebase!',
1212
html: 'This is an <code>HTML</code> email body.',
1313
},
1414
})
1515
```
1616

17-
Because each email is built from a Cloud Firestore document, you can reference information stored in _other_ Cloud Firestore documents and fields, like image URLs.
18-
19-
You can also optionally configure this extension to render emails using [Handlebar](https://handlebarsjs.com/) templates. Each template must be a document stored in a Cloud Firestore collection that you specify when configuring this extension.
17+
You can also optionally configure this extension to render emails using [Handlebar](https://handlebarsjs.com/) templates. Each template is a document stored in a Cloud Firestore collection.
2018

2119
When you configure this extension, you'll need to supply your **SMTP credentials for mail delivery**.
2220

firestore-send-email/extension.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ author:
3232
contributors:
3333
- authorName: Michael Bleigh
3434
url: https://github.com/mbleigh
35+
- authorName: Elliot Hesp
36+
email: elliot@invertase.io
37+
url: https://github.com/ehesp
3538

3639
roles:
3740
- role: datastore.user
@@ -90,25 +93,34 @@ params:
9093
type: string
9194
label: Email documents collection
9295
default: mail
96+
validationRegex: "^[^/]+(/[^/]+/[^/]+)*$"
97+
validationErrorMessage: Must be a valid Cloud Firestore collection
9398
required: true
9499
description: >-
95100
What is the path to the collection that contains the documents used to build and send the emails?
96101
97102
- param: DEFAULT_FROM
98103
type: string
99104
label: Default FROM address
105+
validationRegex: ^\S+@\S+\.\S+$
106+
validationErrorMessage: Must be a valid email address
107+
required: true
100108
description: >-
101109
The email address to use as the sender's address (if it's not specified in the added email document).
102110
103111
- param: DEFAULT_REPLY_TO
104112
type: string
105113
label: Default REPLY-TO address
114+
validationRegex: ^\S+@\S+\.\S+$
115+
validationErrorMessage: Must be a valid email address
106116
description: >-
107117
The email address to use as the reply-to address (if it's not specified in the added email document).
108118
109119
- param: USERS_COLLECTION
110120
type: string
111121
label: Users collection
122+
validationRegex: "^[^/]+(/[^/]+/[^/]+)*$"
123+
validationErrorMessage: Must be a valid Cloud Firestore collection
112124
description: >-
113125
A collection of documents keyed by user UID.
114126
If the `toUids`, `ccUids`, and/or `bccUids` recipient options are used in the added email document,
@@ -117,6 +129,8 @@ params:
117129
- param: TEMPLATES_COLLECTION
118130
type: string
119131
label: Templates collection
132+
validationRegex: "^[^/]+(/[^/]+/[^/]+)*$"
133+
validationErrorMessage: Must be a valid Cloud Firestore collection
120134
description: >-
121135
A collection of email templates keyed by name.
122136
This extension can render an email using a [Handlebar](https://handlebarsjs.com/) template,

firestore-send-email/functions/lib/index.js

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,18 @@ const templates_1 = require("./templates");
3232
admin.initializeApp();
3333
const db = admin.firestore();
3434
const transport = nodemailer.createTransport(config_1.default.smtpConnectionUri);
35-
const templates = new templates_1.default(admin.firestore().collection(config_1.default.templatesCollection));
35+
let templates;
36+
if (config_1.default.templatesCollection) {
37+
templates = new templates_1.default(admin.firestore().collection(config_1.default.templatesCollection));
38+
}
39+
function validateFieldArray(field, array) {
40+
if (!Array.isArray(array)) {
41+
throw new Error(`Invalid field "${field}". Expected an array of strings.`);
42+
}
43+
if (array.find(item => typeof item !== 'string')) {
44+
throw new Error(`Invalid field "${field}". Expected an array of strings.`);
45+
}
46+
}
3647
function processCreate(snap) {
3748
return __awaiter(this, void 0, void 0, function* () {
3849
return snap.ref.update({
@@ -47,35 +58,107 @@ function processCreate(snap) {
4758
}
4859
function preparePayload(payload) {
4960
return __awaiter(this, void 0, void 0, function* () {
50-
if (config_1.default.templatesCollection && payload.template) {
51-
payload.message = Object.assign(payload.message || {}, yield templates.render(payload.template.name, payload.template.data));
61+
const { template } = payload;
62+
if (templates && template) {
63+
if (!template.name) {
64+
throw new Error(`Template object is missing a 'name' parameter.`);
65+
}
66+
payload.message = Object.assign(payload.message || {}, yield templates.render(template.name, template.data));
67+
}
68+
let to = [];
69+
let cc = [];
70+
let bcc = [];
71+
if (typeof payload.to === 'string') {
72+
to = [payload.to];
73+
}
74+
else if (payload.to) {
75+
validateFieldArray("to", payload.to);
76+
to = to.concat(payload.to);
5277
}
53-
if (!config_1.default.usersCollection ||
54-
(!payload.toUids && !payload.ccUids && !payload.bccUids)) {
78+
if (typeof payload.cc === 'string') {
79+
cc = [payload.cc];
80+
}
81+
else if (payload.cc) {
82+
validateFieldArray("cc", payload.cc);
83+
cc = cc.concat(payload.cc);
84+
}
85+
if (typeof payload.bcc === 'string') {
86+
bcc = [payload.bcc];
87+
}
88+
else if (payload.bcc) {
89+
validateFieldArray("bcc", payload.bcc);
90+
bcc = bcc.concat(payload.bcc);
91+
}
92+
if (!payload.toUids && !payload.ccUids && !payload.bccUids) {
93+
payload.to = to;
94+
payload.cc = cc;
95+
payload.bcc = bcc;
5596
return payload;
5697
}
98+
if (!config_1.default.usersCollection) {
99+
throw new Error("Must specify a users collection to send using uids.");
100+
}
101+
let uids = [];
102+
if (payload.toUids) {
103+
validateFieldArray("toUids", payload.toUids);
104+
uids = uids.concat(payload.toUids);
105+
}
106+
if (payload.ccUids) {
107+
validateFieldArray("ccUids", payload.ccUids);
108+
uids = uids.concat(payload.ccUids);
109+
}
110+
if (payload.bccUids) {
111+
validateFieldArray("bccUids", payload.bccUids);
112+
uids = uids.concat(payload.bccUids);
113+
}
57114
const toFetch = {};
58-
[]
59-
.concat(payload.toUids, payload.ccUids, payload.bccUids)
60-
.forEach((uid) => (toFetch[uid] = null));
61-
const docs = yield db.getAll(...Object.keys(toFetch).map((uid) => db.collection(config_1.default.usersCollection).doc(uid)), { fieldMask: ["email"] });
62-
docs.forEach((doc) => {
63-
if (doc.exists) {
64-
toFetch[doc.id] = doc.get("email");
115+
uids.forEach(uid => toFetch[uid] = null);
116+
const documents = yield db.getAll(...Object.keys(toFetch).map((uid) => db.collection(config_1.default.usersCollection).doc(uid)), {
117+
fieldMask: ["email"],
118+
});
119+
const missingUids = [];
120+
documents.forEach((documentSnapshot) => {
121+
if (documentSnapshot.exists) {
122+
const email = documentSnapshot.get("email");
123+
if (email) {
124+
toFetch[documentSnapshot.id] = email;
125+
}
126+
else {
127+
missingUids.push(documentSnapshot.id);
128+
}
129+
}
130+
else {
131+
missingUids.push(documentSnapshot.id);
65132
}
66133
});
134+
logs.missingUids(missingUids);
67135
if (payload.toUids) {
68-
payload.to = payload.toUids.map((uid) => toFetch[uid]);
69-
delete payload.toUids;
136+
payload.toUids.forEach((uid) => {
137+
const email = toFetch[uid];
138+
if (email) {
139+
to.push(email);
140+
}
141+
});
70142
}
143+
payload.to = to;
71144
if (payload.ccUids) {
72-
payload.cc = payload.ccUids.map((uid) => toFetch[uid]);
73-
delete payload.ccUids;
145+
payload.ccUids.forEach((uid) => {
146+
const email = toFetch[uid];
147+
if (email) {
148+
cc.push(email);
149+
}
150+
});
74151
}
152+
payload.cc = cc;
75153
if (payload.bccUids) {
76-
payload.bcc = payload.bccUids.map((uid) => toFetch[uid]);
77-
delete payload.bccUids;
154+
payload.bccUids.forEach((uid) => {
155+
const email = toFetch[uid];
156+
if (email) {
157+
bcc.push(email);
158+
}
159+
});
78160
}
161+
payload.bcc = bcc;
79162
return payload;
80163
});
81164
}
@@ -90,6 +173,9 @@ function deliver(payload, ref) {
90173
};
91174
try {
92175
payload = yield preparePayload(payload);
176+
if (!payload.to.length && !payload.cc.length && !payload.bcc.length) {
177+
throw new Error("Failed to deliver email. Expected at least 1 recipient.");
178+
}
93179
const result = yield transport.sendMail(Object.assign(payload.message, {
94180
from: payload.from || config_1.default.defaultFrom,
95181
replyTo: payload.replyTo || config_1.default.defaultReplyTo,

firestore-send-email/functions/lib/logs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ function missingDeliveryField(ref) {
5151
console.error(`message=${ref.path} is missing 'delivery' field`);
5252
}
5353
exports.missingDeliveryField = missingDeliveryField;
54+
function missingUids(uids) {
55+
console.log(`The following uids were provided, however a document does not exist or has no 'email' field: ${uids.join(',')}`);
56+
}
57+
exports.missingUids = missingUids;

0 commit comments

Comments
 (0)