@budibase/worker 3.4.20 → 3.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "3.4.20",
4
+ "version": "3.4.22",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -85,11 +85,14 @@
85
85
  "@types/jsonwebtoken": "9.0.3",
86
86
  "@types/koa__router": "12.0.4",
87
87
  "@types/lodash": "4.14.200",
88
+ "@types/maildev": "^0.0.7",
88
89
  "@types/node-fetch": "2.6.4",
90
+ "@types/nodemailer": "^6.4.17",
89
91
  "@types/server-destroy": "1.0.1",
90
92
  "@types/supertest": "2.0.14",
91
93
  "@types/uuid": "8.3.4",
92
94
  "jest": "29.7.0",
95
+ "maildev": "^2.2.1",
93
96
  "nock": "^13.5.4",
94
97
  "nodemon": "2.0.15",
95
98
  "rimraf": "3.0.2",
@@ -114,5 +117,5 @@
114
117
  }
115
118
  }
116
119
  },
117
- "gitHead": "7f0df167ae6af3b8b9e45e0c7fe70dd20216d707"
120
+ "gitHead": "d13bdcbc41f1c15066ef24c19290452cd5c4dd46"
118
121
  }
@@ -11,7 +11,6 @@ export async function sendEmail(
11
11
  ctx: UserCtx<SendEmailRequest, SendEmailResponse>
12
12
  ) {
13
13
  let {
14
- workspaceId,
15
14
  email,
16
15
  userId,
17
16
  purpose,
@@ -24,13 +23,15 @@ export async function sendEmail(
24
23
  invite,
25
24
  attachments,
26
25
  } = ctx.request.body
27
- let user: any
26
+ let user: User | undefined = undefined
28
27
  if (userId) {
29
28
  const db = tenancy.getGlobalDB()
30
- user = await db.get<User>(userId)
29
+ user = await db.tryGet<User>(userId)
30
+ if (!user) {
31
+ ctx.throw(404, "User not found.")
32
+ }
31
33
  }
32
34
  const response = await sendEmailFn(email, purpose, {
33
- workspaceId,
34
35
  user,
35
36
  contents,
36
37
  from,
@@ -1,33 +1,269 @@
1
- jest.mock("nodemailer")
2
- import { EmailTemplatePurpose } from "@budibase/types"
3
- import { TestConfiguration, mocks } from "../../../../tests"
4
-
5
- const sendMailMock = mocks.email.mock()
1
+ import { EmailTemplatePurpose, SendEmailRequest } from "@budibase/types"
2
+ import { TestConfiguration } from "../../../../tests"
3
+ import {
4
+ captureEmail,
5
+ deleteAllEmail,
6
+ getAttachments,
7
+ Mailserver,
8
+ startMailserver,
9
+ stopMailserver,
10
+ } from "../../../../tests/mocks/email"
11
+ import { objectStore } from "@budibase/backend-core"
6
12
 
7
13
  describe("/api/global/email", () => {
8
14
  const config = new TestConfiguration()
15
+ let mailserver: Mailserver
9
16
 
10
17
  beforeAll(async () => {
11
18
  await config.beforeAll()
19
+ mailserver = await startMailserver(config)
12
20
  })
13
21
 
14
22
  afterAll(async () => {
23
+ await stopMailserver(mailserver)
15
24
  await config.afterAll()
16
25
  })
17
26
 
18
- it("should be able to send an email (with mocking)", async () => {
19
- // initially configure settings
20
- await config.saveSmtpConfig()
21
- await config.saveSettingsConfig()
27
+ beforeEach(async () => {
28
+ await deleteAllEmail(mailserver)
29
+ })
30
+
31
+ interface TestCase {
32
+ req: Partial<SendEmailRequest>
33
+ expectedStatus?: number
34
+ expectedContents?: string
35
+ }
36
+
37
+ const testCases: TestCase[] = [
38
+ {
39
+ req: {
40
+ purpose: EmailTemplatePurpose.WELCOME,
41
+ },
42
+ expectedContents: `Thanks for getting started with Budibase's Budibase platform.`,
43
+ },
44
+ {
45
+ req: {
46
+ purpose: EmailTemplatePurpose.INVITATION,
47
+ },
48
+ expectedContents: `Use the button below to set up your account and get started:`,
49
+ },
50
+ {
51
+ req: {
52
+ purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
53
+ },
54
+ expectedContents: `You recently requested to reset your password for your Budibase account in your Budibase platform`,
55
+ },
56
+ {
57
+ req: {
58
+ purpose: EmailTemplatePurpose.CUSTOM,
59
+ contents: "Hello, world!",
60
+ },
61
+ expectedContents: "Hello, world!",
62
+ },
63
+ ]
64
+
65
+ it.each(testCases)(
66
+ "can send $req.purpose emails",
67
+ async ({ req, expectedContents, expectedStatus }) => {
68
+ const email = await captureEmail(mailserver, async () => {
69
+ const res = await config.api.emails.sendEmail(
70
+ {
71
+ email: "to@example.com",
72
+ subject: "Test",
73
+ userId: config.user!._id,
74
+ purpose: EmailTemplatePurpose.WELCOME,
75
+ ...req,
76
+ },
77
+ {
78
+ status: expectedStatus || 200,
79
+ }
80
+ )
81
+ expect(res.message).toBeDefined()
82
+ })
83
+
84
+ expect(email.html).toContain(expectedContents)
85
+ expect(email.html).not.toContain("Invalid binding")
86
+ }
87
+ )
88
+
89
+ it("should be able to send an email with an attachment", async () => {
90
+ let bucket = "testbucket"
91
+ let filename = "test.txt"
92
+ await objectStore.upload({
93
+ bucket,
94
+ filename,
95
+ body: Buffer.from("test data"),
96
+ })
97
+ let presignedUrl = await objectStore.getPresignedUrl(
98
+ bucket,
99
+ filename,
100
+ 60000
101
+ )
102
+
103
+ let attachmentObject = {
104
+ url: presignedUrl,
105
+ filename,
106
+ }
107
+
108
+ const email = await captureEmail(mailserver, async () => {
109
+ const res = await config.api.emails.sendEmail({
110
+ email: "to@example.com",
111
+ subject: "Test",
112
+ userId: config.user!._id,
113
+ purpose: EmailTemplatePurpose.WELCOME,
114
+ attachments: [attachmentObject],
115
+ })
116
+ expect(res.message).toBeDefined()
117
+ })
118
+
119
+ expect(email.html).toContain(
120
+ "Thanks for getting started with Budibase's Budibase platform."
121
+ )
122
+ expect(email.html).not.toContain("Invalid binding")
123
+
124
+ const attachments = await getAttachments(mailserver, email)
125
+ expect(attachments).toEqual(["test data"])
126
+ })
127
+
128
+ it("should be able to send email without a userId", async () => {
129
+ const res = await config.api.emails.sendEmail({
130
+ email: "to@example.com",
131
+ subject: "Test",
132
+ purpose: EmailTemplatePurpose.WELCOME,
133
+ })
134
+ expect(res.message).toBeDefined()
135
+ })
22
136
 
137
+ it("should fail to send a password reset email without a userId", async () => {
23
138
  const res = await config.api.emails.sendEmail(
24
- EmailTemplatePurpose.INVITATION
139
+ {
140
+ email: "to@example.com",
141
+ subject: "Test",
142
+ purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
143
+ },
144
+ {
145
+ status: 400,
146
+ }
25
147
  )
148
+ expect(res.message).toBeDefined()
149
+ })
150
+
151
+ it("can cc people", async () => {
152
+ const email = await captureEmail(mailserver, async () => {
153
+ await config.api.emails.sendEmail({
154
+ email: "to@example.com",
155
+ cc: "cc@example.com",
156
+ subject: "Test",
157
+ purpose: EmailTemplatePurpose.CUSTOM,
158
+ contents: "Hello, world!",
159
+ })
160
+ })
26
161
 
27
- expect(res.body.message).toBeDefined()
28
- expect(sendMailMock).toHaveBeenCalled()
29
- const emailCall = sendMailMock.mock.calls[0][0]
30
- expect(emailCall.subject).toBe("Hello!")
31
- expect(emailCall.html).not.toContain("Invalid binding")
162
+ expect(email.cc).toEqual([{ address: "cc@example.com", name: "" }])
163
+ })
164
+
165
+ it("can bcc people", async () => {
166
+ const email = await captureEmail(mailserver, async () => {
167
+ await config.api.emails.sendEmail({
168
+ email: "to@example.com",
169
+ bcc: "bcc@example.com",
170
+ subject: "Test",
171
+ purpose: EmailTemplatePurpose.CUSTOM,
172
+ contents: "Hello, world!",
173
+ })
174
+ })
175
+
176
+ expect(email.calculatedBcc).toEqual([
177
+ { address: "bcc@example.com", name: "" },
178
+ ])
179
+ })
180
+
181
+ it("can change the from address", async () => {
182
+ const email = await captureEmail(mailserver, async () => {
183
+ const res = await config.api.emails.sendEmail({
184
+ email: "to@example.com",
185
+ from: "from@example.com",
186
+ subject: "Test",
187
+ purpose: EmailTemplatePurpose.CUSTOM,
188
+ contents: "Hello, world!",
189
+ })
190
+ expect(res.message).toBeDefined()
191
+ })
192
+
193
+ expect(email.to).toEqual([{ address: "to@example.com", name: "" }])
194
+ expect(email.from).toEqual([{ address: "from@example.com", name: "" }])
195
+ })
196
+
197
+ it("can send a calendar invite", async () => {
198
+ const startTime = new Date()
199
+ const endTime = new Date()
200
+
201
+ const email = await captureEmail(mailserver, async () => {
202
+ await config.api.emails.sendEmail({
203
+ email: "to@example.com",
204
+ subject: "Test",
205
+ purpose: EmailTemplatePurpose.CUSTOM,
206
+ contents: "Hello, world!",
207
+ invite: {
208
+ startTime,
209
+ endTime,
210
+ summary: "Summary",
211
+ location: "Location",
212
+ url: "http://example.com",
213
+ },
214
+ })
215
+ })
216
+
217
+ expect(email.alternatives).toEqual([
218
+ {
219
+ charset: "utf-8",
220
+ contentType: "text/calendar",
221
+ method: "REQUEST",
222
+ transferEncoding: "7bit",
223
+ content: expect.any(String),
224
+ },
225
+ ])
226
+
227
+ // Reference iCal invite:
228
+ // BEGIN:VCALENDAR
229
+ // VERSION:2.0
230
+ // PRODID:-//sebbo.net//ical-generator//EN
231
+ // NAME:Invite
232
+ // X-WR-CALNAME:Invite
233
+ // BEGIN:VEVENT
234
+ // UID:2b5947b7-ec5a-4341-8d70-8d8130183f2a
235
+ // SEQUENCE:0
236
+ // DTSTAMP:20200101T000000Z
237
+ // DTSTART:20200101T000000Z
238
+ // DTEND:20200101T000000Z
239
+ // SUMMARY:Summary
240
+ // LOCATION:Location
241
+ // URL;VALUE=URI:http://example.com
242
+ // END:VEVENT
243
+ // END:VCALENDAR
244
+ expect(email.alternatives[0].content).toContain("BEGIN:VCALENDAR")
245
+ expect(email.alternatives[0].content).toContain("BEGIN:VEVENT")
246
+ expect(email.alternatives[0].content).toContain("UID:")
247
+ expect(email.alternatives[0].content).toContain("SEQUENCE:0")
248
+ expect(email.alternatives[0].content).toContain("SUMMARY:Summary")
249
+ expect(email.alternatives[0].content).toContain("LOCATION:Location")
250
+ expect(email.alternatives[0].content).toContain(
251
+ "URL;VALUE=URI:http://example.com"
252
+ )
253
+ expect(email.alternatives[0].content).toContain("END:VEVENT")
254
+ expect(email.alternatives[0].content).toContain("END:VCALENDAR")
255
+
256
+ const formatDate = (date: Date) =>
257
+ date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"
258
+
259
+ expect(email.alternatives[0].content).toContain(
260
+ `DTSTAMP:${formatDate(startTime)}`
261
+ )
262
+ expect(email.alternatives[0].content).toContain(
263
+ `DTSTART:${formatDate(startTime)}`
264
+ )
265
+ expect(email.alternatives[0].content).toContain(
266
+ `DTEND:${formatDate(endTime)}`
267
+ )
32
268
  })
33
269
  })
@@ -32,6 +32,8 @@ import {
32
32
  AuthToken,
33
33
  SCIMConfig,
34
34
  ConfigType,
35
+ SMTPConfig,
36
+ SMTPInnerConfig,
35
37
  } from "@budibase/types"
36
38
  import API from "./api"
37
39
  import jwt, { Secret } from "jsonwebtoken"
@@ -348,9 +350,15 @@ class TestConfiguration {
348
350
 
349
351
  // CONFIGS - SMTP
350
352
 
351
- async saveSmtpConfig() {
353
+ async saveSmtpConfig(config?: SMTPInnerConfig) {
352
354
  await this.deleteConfig(Config.SMTP)
353
- await this._req(structures.configs.smtp(), null, controllers.config.save)
355
+
356
+ let smtpConfig: SMTPConfig = structures.configs.smtp()
357
+ if (config) {
358
+ smtpConfig = { type: ConfigType.SMTP, config }
359
+ }
360
+
361
+ await this._req(smtpConfig, null, controllers.config.save)
354
362
  }
355
363
 
356
364
  async saveEtherealSmtpConfig() {
@@ -1,19 +1,18 @@
1
- import { EmailAttachment } from "@budibase/types"
1
+ import { SendEmailRequest, SendEmailResponse } from "@budibase/types"
2
2
  import { TestAPI } from "./base"
3
3
 
4
4
  export class EmailAPI extends TestAPI {
5
- sendEmail = (purpose: string, attachments?: EmailAttachment[]) => {
6
- return this.request
5
+ sendEmail = async (
6
+ req: SendEmailRequest,
7
+ expectations?: { status?: number }
8
+ ): Promise<SendEmailResponse> => {
9
+ const res = await this.request
7
10
  .post(`/api/global/email/send`)
8
- .send({
9
- email: "test@example.com",
10
- attachments,
11
- purpose,
12
- tenantId: this.config.getTenantId(),
13
- userId: this.config.user!._id!,
14
- })
11
+ .send(req)
15
12
  .set(this.config.defaultHeaders())
16
13
  .expect("Content-Type", /json/)
17
- .expect(200)
14
+ .expect(expectations?.status || 200)
15
+
16
+ return res.body as SendEmailResponse
18
17
  }
19
18
  }
@@ -1,3 +1,10 @@
1
+ import MailDev from "maildev"
2
+ import { promisify } from "util"
3
+ import TestConfiguration from "../TestConfiguration"
4
+
5
+ /**
6
+ * @deprecated please use the `MailDev` email server instead of this mock.
7
+ */
1
8
  export function mock() {
2
9
  // mock the email system
3
10
  const sendMailMock = jest.fn()
@@ -8,3 +15,170 @@ export function mock() {
8
15
  })
9
16
  return sendMailMock
10
17
  }
18
+
19
+ export type Mailserver = InstanceType<typeof MailDev>
20
+ export type MailserverConfig = ConstructorParameters<typeof MailDev>[0]
21
+
22
+ export interface Attachment {
23
+ checksum: string
24
+ contentId: string
25
+ contentType: string
26
+ fileName: string
27
+ generatedFileName: string
28
+ length: number
29
+ transferEncoding: string
30
+ transformed: boolean
31
+ }
32
+
33
+ export interface Address {
34
+ address: string
35
+ args?: boolean
36
+ name?: string
37
+ }
38
+
39
+ export interface Alternative {
40
+ contentType: string
41
+ content: string
42
+ charset: string
43
+ method: string
44
+ transferEncoding: string
45
+ }
46
+
47
+ export interface Envelope {
48
+ from: Address
49
+ to: Address[]
50
+ host: string
51
+ remoteAddress: string
52
+ }
53
+
54
+ export interface Email {
55
+ attachments: Attachment[]
56
+ alternatives: Alternative[]
57
+ calculatedBcc: Address[]
58
+ cc: Address[]
59
+ date: string
60
+ envelope: Envelope
61
+ from: Address[]
62
+ headers: Record<string, string>
63
+ html: string
64
+ id: string
65
+ messageId: string
66
+ priority: string
67
+ read: boolean
68
+ size: number
69
+ sizeHuman: string
70
+ source: string
71
+ time: Date
72
+ to: Address[]
73
+ }
74
+
75
+ export function getUnusedPort(): Promise<number> {
76
+ return new Promise((resolve, reject) => {
77
+ const server = require("net").createServer()
78
+ server.unref()
79
+ server.on("error", reject)
80
+ server.listen(0, () => {
81
+ const port = server.address().port
82
+ server.close(() => {
83
+ resolve(port)
84
+ })
85
+ })
86
+ })
87
+ }
88
+
89
+ export async function captureEmail(
90
+ mailserver: Mailserver,
91
+ f: () => Promise<void>
92
+ ): Promise<Email> {
93
+ const timeoutMs = 5000
94
+ let timeout: ReturnType<typeof setTimeout> | undefined = undefined
95
+ const cancel = () => {
96
+ if (timeout) {
97
+ clearTimeout(timeout)
98
+ timeout = undefined
99
+ }
100
+ }
101
+ const timeoutPromise = new Promise<never>((_, reject) => {
102
+ timeout = setTimeout(() => {
103
+ reject(new Error("Timed out waiting for email"))
104
+ }, timeoutMs)
105
+ })
106
+ const mailPromise = new Promise<Email>(resolve => {
107
+ // @ts-expect-error - types are wrong
108
+ mailserver.once("new", email => {
109
+ resolve(email as Email)
110
+ cancel()
111
+ })
112
+ })
113
+ const emailPromise = Promise.race([mailPromise, timeoutPromise])
114
+ try {
115
+ await f()
116
+ } finally {
117
+ cancel()
118
+ }
119
+ return await emailPromise
120
+ }
121
+
122
+ export async function startMailserver(
123
+ config: TestConfiguration,
124
+ opts?: MailserverConfig
125
+ ): Promise<Mailserver> {
126
+ if (!opts) {
127
+ opts = {}
128
+ }
129
+ if (!opts.smtp) {
130
+ opts.smtp = await getUnusedPort()
131
+ }
132
+ const mailserver = new MailDev(opts || {})
133
+ await new Promise((resolve, reject) => {
134
+ mailserver.listen(err => {
135
+ if (err) {
136
+ return reject(err)
137
+ }
138
+ resolve(mailserver)
139
+ })
140
+ })
141
+ await config.saveSmtpConfig({
142
+ host: "localhost",
143
+ port: opts.smtp,
144
+ secure: false,
145
+ from: "test@example.com",
146
+ })
147
+ return mailserver
148
+ }
149
+
150
+ export function deleteAllEmail(mailserver: Mailserver) {
151
+ return promisify(mailserver.deleteAllEmail).bind(mailserver)()
152
+ }
153
+
154
+ export function stopMailserver(mailserver: Mailserver) {
155
+ return promisify(mailserver.close).bind(mailserver)()
156
+ }
157
+
158
+ export function getAttachment(
159
+ mailserver: Mailserver,
160
+ email: Email,
161
+ attachment: Attachment
162
+ ) {
163
+ return new Promise<string>(resolve => {
164
+ // @ts-expect-error - types are wrong
165
+ mailserver.getEmailAttachment(
166
+ email.id,
167
+ attachment.generatedFileName,
168
+ (err: any, _contentType: string, stream: ReadableStream) => {
169
+ if (err) {
170
+ throw err
171
+ }
172
+ resolve(new Response(stream).text())
173
+ }
174
+ )
175
+ })
176
+ }
177
+
178
+ export function getAttachments(mailserver: Mailserver, email: Email) {
179
+ return Promise.all(
180
+ email.attachments.map(attachment =>
181
+ getAttachment(mailserver, email, attachment)
182
+ )
183
+ )
184
+ }
@@ -4,16 +4,17 @@ import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
4
4
  import { getSettingsTemplateContext } from "./templates"
5
5
  import { processString } from "@budibase/string-templates"
6
6
  import {
7
- User,
8
7
  SendEmailOpts,
9
8
  SMTPInnerConfig,
10
9
  EmailTemplatePurpose,
10
+ User,
11
11
  } from "@budibase/types"
12
- import { configs, cache, objectStore } from "@budibase/backend-core"
12
+ import { configs, cache, objectStore, HTTPError } from "@budibase/backend-core"
13
13
  import ical from "ical-generator"
14
14
  import _ from "lodash"
15
15
 
16
- const nodemailer = require("nodemailer")
16
+ import nodemailer from "nodemailer"
17
+ import SMTPTransport from "nodemailer/lib/smtp-transport"
17
18
 
18
19
  const TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev()
19
20
  const TYPE = TemplateType.EMAIL
@@ -26,7 +27,7 @@ const FULL_EMAIL_PURPOSES = [
26
27
  ]
27
28
 
28
29
  function createSMTPTransport(config?: SMTPInnerConfig) {
29
- let options: any
30
+ let options: SMTPTransport.Options
30
31
  let secure = config?.secure
31
32
  // default it if not specified
32
33
  if (secure == null) {
@@ -59,22 +60,6 @@ function createSMTPTransport(config?: SMTPInnerConfig) {
59
60
  return nodemailer.createTransport(options)
60
61
  }
61
62
 
62
- async function getLinkCode(
63
- purpose: EmailTemplatePurpose,
64
- email: string,
65
- user: User,
66
- info: any = null
67
- ) {
68
- switch (purpose) {
69
- case EmailTemplatePurpose.PASSWORD_RECOVERY:
70
- return cache.passwordReset.createCode(user._id!, info)
71
- case EmailTemplatePurpose.INVITATION:
72
- return cache.invite.createCode(email, info)
73
- default:
74
- return null
75
- }
76
- }
77
-
78
63
  /**
79
64
  * Builds an email using handlebars and the templates found in the system (default or otherwise).
80
65
  * @param purpose the purpose of the email being built, e.g. invitation, password reset.
@@ -87,8 +72,8 @@ async function getLinkCode(
87
72
  async function buildEmail(
88
73
  purpose: EmailTemplatePurpose,
89
74
  email: string,
90
- context: any,
91
- { user, contents }: any = {}
75
+ context: Record<string, any>,
76
+ { user, contents }: { user?: User; contents?: string } = {}
92
77
  ) {
93
78
  // this isn't a full email
94
79
  if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
@@ -106,8 +91,8 @@ async function buildEmail(
106
91
  throw "Unable to build email, missing base components"
107
92
  }
108
93
 
109
- let name = user ? user.name : undefined
110
- if (user && !name && user.firstName) {
94
+ let name: string | undefined
95
+ if (user && user.firstName) {
111
96
  name = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName
112
97
  }
113
98
  context = {
@@ -158,10 +143,21 @@ export async function sendEmail(
158
143
  }
159
144
  const transport = createSMTPTransport(config)
160
145
  // if there is a link code needed this will retrieve it
161
- const code = await getLinkCode(purpose, email, opts.user, opts?.info)
146
+ let code: string | null = null
147
+ switch (purpose) {
148
+ case EmailTemplatePurpose.PASSWORD_RECOVERY:
149
+ if (!opts.user || !opts.user._id) {
150
+ throw new HTTPError("User must be provided for password recovery.", 400)
151
+ }
152
+ code = await cache.passwordReset.createCode(opts.user._id, opts.info)
153
+ break
154
+ case EmailTemplatePurpose.INVITATION:
155
+ code = await cache.invite.createCode(email, opts.info)
156
+ break
157
+ }
162
158
  let context = await getSettingsTemplateContext(purpose, code)
163
159
 
164
- let message: any = {
160
+ let message: Parameters<typeof transport.sendMail>[0] = {
165
161
  from: opts?.from || config?.from,
166
162
  html: await buildEmail(purpose, email, context, {
167
163
  user: opts?.user,
@@ -1,108 +0,0 @@
1
- jest.unmock("node-fetch")
2
- import { TestConfiguration } from "../../../../tests"
3
- import { objectStore } from "@budibase/backend-core"
4
- import { helpers } from "@budibase/shared-core"
5
-
6
- import tk from "timekeeper"
7
- import { EmailAttachment, EmailTemplatePurpose } from "@budibase/types"
8
-
9
- const fetch = require("node-fetch")
10
-
11
- const nodemailer = require("nodemailer")
12
-
13
- // for the real email tests give them a long time to try complete/fail
14
- jest.setTimeout(30000)
15
-
16
- describe("/api/global/email", () => {
17
- const config = new TestConfiguration()
18
-
19
- beforeAll(async () => {
20
- tk.reset()
21
- await config.beforeAll()
22
- })
23
-
24
- afterAll(async () => {
25
- await config.afterAll()
26
- })
27
-
28
- async function sendRealEmail(
29
- purpose: string,
30
- attachments?: EmailAttachment[]
31
- ) {
32
- let response, text
33
- try {
34
- await helpers.withTimeout(20000, () => config.saveEtherealSmtpConfig())
35
- await helpers.withTimeout(20000, () => config.saveSettingsConfig())
36
- let res
37
- if (attachments) {
38
- res = await config.api.emails
39
- .sendEmail(purpose, attachments)
40
- .timeout(20000)
41
- } else {
42
- res = await config.api.emails.sendEmail(purpose).timeout(20000)
43
- }
44
- // ethereal hiccup, can't test right now
45
- if (res.status >= 300) {
46
- return
47
- }
48
- expect(res.body.message).toBeDefined()
49
- const testUrl = nodemailer.getTestMessageUrl(res.body)
50
- expect(testUrl).toBeDefined()
51
- response = await fetch(testUrl)
52
- text = await response.text()
53
- } catch (err: any) {
54
- // ethereal hiccup, can't test right now
55
- if (parseInt(err.status) >= 300 || (err && err.errno === "ETIME")) {
56
- return
57
- } else {
58
- throw err
59
- }
60
- }
61
- let toCheckFor
62
- switch (purpose) {
63
- case EmailTemplatePurpose.WELCOME:
64
- toCheckFor = `Thanks for getting started with Budibase's Budibase platform.`
65
- break
66
- case EmailTemplatePurpose.INVITATION:
67
- toCheckFor = `Use the button below to set up your account and get started:`
68
- break
69
- case EmailTemplatePurpose.PASSWORD_RECOVERY:
70
- toCheckFor = `You recently requested to reset your password for your Budibase account in your Budibase platform`
71
- break
72
- }
73
- expect(text).toContain(toCheckFor)
74
- }
75
-
76
- it("should be able to send a welcome email", async () => {
77
- await sendRealEmail(EmailTemplatePurpose.WELCOME)
78
- })
79
-
80
- it("should be able to send a invitation email", async () => {
81
- await sendRealEmail(EmailTemplatePurpose.INVITATION)
82
- })
83
-
84
- it("should be able to send a password recovery email", async () => {
85
- await sendRealEmail(EmailTemplatePurpose.PASSWORD_RECOVERY)
86
- })
87
-
88
- it("should be able to send an email with attachments", async () => {
89
- let bucket = "testbucket"
90
- let filename = "test.txt"
91
- await objectStore.upload({
92
- bucket,
93
- filename,
94
- body: Buffer.from("test data"),
95
- })
96
- let presignedUrl = await objectStore.getPresignedUrl(
97
- bucket,
98
- filename,
99
- 60000
100
- )
101
-
102
- let attachmentObject = {
103
- url: presignedUrl,
104
- filename,
105
- }
106
- await sendRealEmail(EmailTemplatePurpose.WELCOME, [attachmentObject])
107
- })
108
- })