@oneuptime/common 9.3.15 → 9.3.17
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/Models/DatabaseModels/Monitor.ts +109 -0
- package/Server/EnvironmentConfig.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1768335589018-AddIncomingEmailMonitor.ts +53 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/MultipartFormData.ts +17 -0
- package/Server/Services/InboundEmail/InboundEmailProvider.ts +64 -0
- package/Server/Services/InboundEmail/InboundEmailProviderFactory.ts +80 -0
- package/Server/Services/InboundEmail/Providers/SendGridInboundProvider.ts +211 -0
- package/Server/Services/MonitorService.ts +4 -0
- package/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.ts +248 -0
- package/Server/Utils/Monitor/DataToProcess.ts +2 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +13 -0
- package/Types/Monitor/CriteriaFilter.ts +7 -0
- package/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest.ts +25 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +67 -0
- package/Types/Monitor/MonitorType.ts +9 -0
- package/UI/Components/ModelDetail/CardModelDetail.tsx +34 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +33 -1
- package/UI/Config.ts +3 -0
- package/build/dist/Models/DatabaseModels/Monitor.js +110 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +10 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1768335589018-AddIncomingEmailMonitor.js +24 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1768335589018-AddIncomingEmailMonitor.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MultipartFormData.js +13 -0
- package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -0
- package/build/dist/Server/Services/InboundEmail/InboundEmailProvider.js +12 -0
- package/build/dist/Server/Services/InboundEmail/InboundEmailProvider.js.map +1 -0
- package/build/dist/Server/Services/InboundEmail/InboundEmailProviderFactory.js +59 -0
- package/build/dist/Server/Services/InboundEmail/InboundEmailProviderFactory.js.map +1 -0
- package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js +148 -0
- package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js.map +1 -0
- package/build/dist/Server/Services/MonitorService.js +3 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.js +164 -0
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +10 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +6 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest.js +2 -0
- package/build/dist/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +62 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +8 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +29 -0
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +29 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Config.js +1 -0
- package/build/dist/UI/Config.js.map +1 -1
- package/package.json +3 -1
|
@@ -20,6 +20,7 @@ import TableMetadata from "../../Types/Database/TableMetadata";
|
|
|
20
20
|
import TenantColumn from "../../Types/Database/TenantColumn";
|
|
21
21
|
import IconProp from "../../Types/Icon/IconProp";
|
|
22
22
|
import { JSONObject } from "../../Types/JSON";
|
|
23
|
+
import IncomingEmailMonitorRequest from "../../Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest";
|
|
23
24
|
import IncomingMonitorRequest from "../../Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
|
|
24
25
|
import MonitorSteps from "../../Types/Monitor/MonitorSteps";
|
|
25
26
|
import MonitorType from "../../Types/Monitor/MonitorType";
|
|
@@ -952,6 +953,114 @@ export default class Monitor extends BaseModel {
|
|
|
952
953
|
})
|
|
953
954
|
public incomingMonitorRequest?: IncomingMonitorRequest = undefined;
|
|
954
955
|
|
|
956
|
+
// Incoming Email Monitor fields
|
|
957
|
+
|
|
958
|
+
@ColumnAccessControl({
|
|
959
|
+
create: [],
|
|
960
|
+
read: [
|
|
961
|
+
Permission.ProjectOwner,
|
|
962
|
+
Permission.ProjectAdmin,
|
|
963
|
+
Permission.ProjectMember,
|
|
964
|
+
Permission.ReadProjectMonitor,
|
|
965
|
+
],
|
|
966
|
+
update: [
|
|
967
|
+
Permission.ProjectOwner,
|
|
968
|
+
Permission.ProjectAdmin,
|
|
969
|
+
Permission.ProjectMember,
|
|
970
|
+
Permission.EditProjectMonitor,
|
|
971
|
+
],
|
|
972
|
+
})
|
|
973
|
+
@Index()
|
|
974
|
+
@TableColumn({
|
|
975
|
+
type: TableColumnType.ObjectID,
|
|
976
|
+
required: false,
|
|
977
|
+
isDefaultValueColumn: false,
|
|
978
|
+
computed: true,
|
|
979
|
+
title: "Incoming Email Secret Key",
|
|
980
|
+
description:
|
|
981
|
+
"This field is for Incoming Email Monitor only. Secret Key used to generate unique email address.",
|
|
982
|
+
})
|
|
983
|
+
@Column({
|
|
984
|
+
type: ColumnType.ObjectID,
|
|
985
|
+
nullable: true,
|
|
986
|
+
transformer: ObjectID.getDatabaseTransformer(),
|
|
987
|
+
})
|
|
988
|
+
public incomingEmailSecretKey?: ObjectID = undefined;
|
|
989
|
+
|
|
990
|
+
@ColumnAccessControl({
|
|
991
|
+
create: [],
|
|
992
|
+
read: [
|
|
993
|
+
Permission.ProjectOwner,
|
|
994
|
+
Permission.ProjectAdmin,
|
|
995
|
+
Permission.ProjectMember,
|
|
996
|
+
Permission.ReadProjectMonitor,
|
|
997
|
+
],
|
|
998
|
+
update: [],
|
|
999
|
+
})
|
|
1000
|
+
@Index()
|
|
1001
|
+
@TableColumn({
|
|
1002
|
+
type: TableColumnType.Date,
|
|
1003
|
+
required: false,
|
|
1004
|
+
isDefaultValueColumn: false,
|
|
1005
|
+
title: "Incoming Email Monitor Last Email Received At",
|
|
1006
|
+
description:
|
|
1007
|
+
"This field is for Incoming Email Monitor only. When was the last email received?",
|
|
1008
|
+
})
|
|
1009
|
+
@Column({
|
|
1010
|
+
type: ColumnType.Date,
|
|
1011
|
+
nullable: true,
|
|
1012
|
+
})
|
|
1013
|
+
public incomingEmailMonitorLastEmailReceivedAt?: Date = undefined;
|
|
1014
|
+
|
|
1015
|
+
@ColumnAccessControl({
|
|
1016
|
+
create: [],
|
|
1017
|
+
read: [
|
|
1018
|
+
Permission.ProjectOwner,
|
|
1019
|
+
Permission.ProjectAdmin,
|
|
1020
|
+
Permission.ProjectMember,
|
|
1021
|
+
Permission.ReadProjectMonitor,
|
|
1022
|
+
],
|
|
1023
|
+
update: [],
|
|
1024
|
+
})
|
|
1025
|
+
@TableColumn({
|
|
1026
|
+
type: TableColumnType.JSON,
|
|
1027
|
+
required: false,
|
|
1028
|
+
title: "Incoming Email Monitor Request",
|
|
1029
|
+
description:
|
|
1030
|
+
"This field is for Incoming Email Monitor only. Last email data received.",
|
|
1031
|
+
})
|
|
1032
|
+
@Column({
|
|
1033
|
+
type: ColumnType.JSON,
|
|
1034
|
+
nullable: true,
|
|
1035
|
+
})
|
|
1036
|
+
public incomingEmailMonitorRequest?: IncomingEmailMonitorRequest = undefined;
|
|
1037
|
+
|
|
1038
|
+
@ColumnAccessControl({
|
|
1039
|
+
create: [],
|
|
1040
|
+
read: [
|
|
1041
|
+
Permission.ProjectOwner,
|
|
1042
|
+
Permission.ProjectAdmin,
|
|
1043
|
+
Permission.ProjectMember,
|
|
1044
|
+
Permission.ReadProjectMonitor,
|
|
1045
|
+
],
|
|
1046
|
+
update: [],
|
|
1047
|
+
})
|
|
1048
|
+
@Index()
|
|
1049
|
+
@TableColumn({
|
|
1050
|
+
type: TableColumnType.Date,
|
|
1051
|
+
required: false,
|
|
1052
|
+
isDefaultValueColumn: false,
|
|
1053
|
+
title:
|
|
1054
|
+
"When was the last time we checked the heartbeat for incoming email?",
|
|
1055
|
+
description:
|
|
1056
|
+
"This field is for Incoming Email monitor only. When was the last time we checked the heartbeat?",
|
|
1057
|
+
})
|
|
1058
|
+
@Column({
|
|
1059
|
+
type: ColumnType.Date,
|
|
1060
|
+
nullable: true,
|
|
1061
|
+
})
|
|
1062
|
+
public incomingEmailMonitorHeartbeatCheckedAt?: Date = undefined;
|
|
1063
|
+
|
|
955
1064
|
@ColumnAccessControl({
|
|
956
1065
|
create: [
|
|
957
1066
|
Permission.ProjectOwner,
|
|
@@ -48,6 +48,7 @@ const FRONTEND_ENV_ALLOW_LIST: Array<string> = [
|
|
|
48
48
|
"GITHUB_APP_NAME",
|
|
49
49
|
"CAPTCHA_ENABLED",
|
|
50
50
|
"CAPTCHA_SITE_KEY",
|
|
51
|
+
"INBOUND_EMAIL_DOMAIN",
|
|
51
52
|
];
|
|
52
53
|
|
|
53
54
|
const FRONTEND_ENV_ALLOW_PREFIXES: Array<string> = [
|
|
@@ -527,3 +528,18 @@ export const VapidSubject: string =
|
|
|
527
528
|
export const EnterpriseLicenseValidationUrl: URL = URL.fromString(
|
|
528
529
|
"https://oneuptime.com/api/enterprise-license/validate",
|
|
529
530
|
);
|
|
531
|
+
|
|
532
|
+
// Inbound Email Configuration for Incoming Email Monitor
|
|
533
|
+
export enum InboundEmailProviderType {
|
|
534
|
+
SendGrid = "SendGrid",
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export const InboundEmailProvider: InboundEmailProviderType =
|
|
538
|
+
(process.env["INBOUND_EMAIL_PROVIDER"] as InboundEmailProviderType) ||
|
|
539
|
+
InboundEmailProviderType.SendGrid;
|
|
540
|
+
|
|
541
|
+
export const InboundEmailDomain: string | undefined =
|
|
542
|
+
process.env["INBOUND_EMAIL_DOMAIN"] || undefined;
|
|
543
|
+
|
|
544
|
+
export const InboundEmailWebhookSecret: string | undefined =
|
|
545
|
+
process.env["INBOUND_EMAIL_WEBHOOK_SECRET"] || undefined;
|
package/Server/Infrastructure/Postgres/SchemaMigrations/1768335589018-AddIncomingEmailMonitor.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class AddIncomingEmailMonitor1768335589018
|
|
4
|
+
implements MigrationInterface
|
|
5
|
+
{
|
|
6
|
+
public name = "AddIncomingEmailMonitor1768335589018";
|
|
7
|
+
|
|
8
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
9
|
+
await queryRunner.query(
|
|
10
|
+
`ALTER TABLE "Monitor" ADD "incomingEmailSecretKey" uuid`,
|
|
11
|
+
);
|
|
12
|
+
await queryRunner.query(
|
|
13
|
+
`CREATE INDEX "IDX_Monitor_incomingEmailSecretKey" ON "Monitor" ("incomingEmailSecretKey")`,
|
|
14
|
+
);
|
|
15
|
+
await queryRunner.query(
|
|
16
|
+
`ALTER TABLE "Monitor" ADD "incomingEmailMonitorLastEmailReceivedAt" TIMESTAMP WITH TIME ZONE`,
|
|
17
|
+
);
|
|
18
|
+
await queryRunner.query(
|
|
19
|
+
`CREATE INDEX "IDX_Monitor_incomingEmailMonitorLastEmailReceivedAt" ON "Monitor" ("incomingEmailMonitorLastEmailReceivedAt")`,
|
|
20
|
+
);
|
|
21
|
+
await queryRunner.query(
|
|
22
|
+
`ALTER TABLE "Monitor" ADD "incomingEmailMonitorRequest" jsonb`,
|
|
23
|
+
);
|
|
24
|
+
await queryRunner.query(
|
|
25
|
+
`ALTER TABLE "Monitor" ADD "incomingEmailMonitorHeartbeatCheckedAt" TIMESTAMP WITH TIME ZONE`,
|
|
26
|
+
);
|
|
27
|
+
await queryRunner.query(
|
|
28
|
+
`CREATE INDEX "IDX_Monitor_incomingEmailMonitorHeartbeatCheckedAt" ON "Monitor" ("incomingEmailMonitorHeartbeatCheckedAt")`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
33
|
+
await queryRunner.query(
|
|
34
|
+
`DROP INDEX "IDX_Monitor_incomingEmailMonitorHeartbeatCheckedAt"`,
|
|
35
|
+
);
|
|
36
|
+
await queryRunner.query(
|
|
37
|
+
`ALTER TABLE "Monitor" DROP COLUMN "incomingEmailMonitorHeartbeatCheckedAt"`,
|
|
38
|
+
);
|
|
39
|
+
await queryRunner.query(
|
|
40
|
+
`ALTER TABLE "Monitor" DROP COLUMN "incomingEmailMonitorRequest"`,
|
|
41
|
+
);
|
|
42
|
+
await queryRunner.query(
|
|
43
|
+
`DROP INDEX "IDX_Monitor_incomingEmailMonitorLastEmailReceivedAt"`,
|
|
44
|
+
);
|
|
45
|
+
await queryRunner.query(
|
|
46
|
+
`ALTER TABLE "Monitor" DROP COLUMN "incomingEmailMonitorLastEmailReceivedAt"`,
|
|
47
|
+
);
|
|
48
|
+
await queryRunner.query(`DROP INDEX "IDX_Monitor_incomingEmailSecretKey"`);
|
|
49
|
+
await queryRunner.query(
|
|
50
|
+
`ALTER TABLE "Monitor" DROP COLUMN "incomingEmailSecretKey"`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -219,6 +219,7 @@ import { MigrationName1767979055522 } from "./1767979055522-MigrationName";
|
|
|
219
219
|
import { MigrationName1767979448478 } from "./1767979448478-MigrationName";
|
|
220
220
|
import { IncreaseClientSecretLength1768216593272 } from "./1768216593272-IncreaseClientSecretLength";
|
|
221
221
|
import { AddOAuthProviderType1768217403078 } from "./1768217403078-AddOAuthProviderType";
|
|
222
|
+
import { AddIncomingEmailMonitor1768335589018 } from "./1768335589018-AddIncomingEmailMonitor";
|
|
222
223
|
|
|
223
224
|
export default [
|
|
224
225
|
InitialMigration,
|
|
@@ -442,4 +443,5 @@ export default [
|
|
|
442
443
|
MigrationName1767896933148,
|
|
443
444
|
IncreaseClientSecretLength1768216593272,
|
|
444
445
|
AddOAuthProviderType1768217403078,
|
|
446
|
+
AddIncomingEmailMonitor1768335589018,
|
|
445
447
|
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import multer from "multer";
|
|
2
|
+
import { RequestHandler } from "express";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Configure multer for handling multipart/form-data
|
|
6
|
+
* Uses memory storage to store files in memory as Buffer objects
|
|
7
|
+
*/
|
|
8
|
+
const upload: multer.Multer = multer({ storage: multer.memoryStorage() });
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Middleware for handling any file uploads (multipart/form-data)
|
|
12
|
+
* This is useful for webhooks that send data as multipart/form-data (e.g., SendGrid inbound email)
|
|
13
|
+
*/
|
|
14
|
+
const MultipartFormDataMiddleware: RequestHandler =
|
|
15
|
+
upload.any() as unknown as RequestHandler;
|
|
16
|
+
|
|
17
|
+
export default MultipartFormDataMiddleware;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
2
|
+
|
|
3
|
+
export interface ParsedInboundEmail {
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
subject: string;
|
|
7
|
+
body: string;
|
|
8
|
+
bodyHtml?: string | undefined;
|
|
9
|
+
headers?: Record<string, string> | undefined;
|
|
10
|
+
rawEmail?: string | undefined;
|
|
11
|
+
attachments?:
|
|
12
|
+
| Array<{
|
|
13
|
+
filename: string;
|
|
14
|
+
contentType: string;
|
|
15
|
+
size: number;
|
|
16
|
+
}>
|
|
17
|
+
| undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface InboundEmailProviderConfig {
|
|
21
|
+
webhookSecret?: string | undefined;
|
|
22
|
+
inboundDomain: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default abstract class InboundEmailProvider {
|
|
26
|
+
protected config: InboundEmailProviderConfig;
|
|
27
|
+
|
|
28
|
+
public constructor(config: InboundEmailProviderConfig) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse raw webhook/request data into ParsedInboundEmail
|
|
34
|
+
*/
|
|
35
|
+
public abstract parseInboundEmail(
|
|
36
|
+
rawData: JSONObject,
|
|
37
|
+
): Promise<ParsedInboundEmail>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate webhook signature/authentication
|
|
41
|
+
*/
|
|
42
|
+
public abstract validateWebhook(data: {
|
|
43
|
+
headers: Record<string, string>;
|
|
44
|
+
body: JSONObject | string;
|
|
45
|
+
}): Promise<boolean>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract monitor secret key from email address
|
|
49
|
+
* e.g., monitor-abc123@inbound.oneuptime.com -> abc123
|
|
50
|
+
*/
|
|
51
|
+
public abstract extractSecretKeyFromEmail(email: string): string | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate inbound email address for a monitor
|
|
55
|
+
*/
|
|
56
|
+
public abstract generateMonitorEmailAddress(secretKey: string): string;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the inbound domain
|
|
60
|
+
*/
|
|
61
|
+
public getInboundDomain(): string {
|
|
62
|
+
return this.config.inboundDomain;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import InboundEmailProvider from "./InboundEmailProvider";
|
|
2
|
+
import SendGridInboundProvider from "./Providers/SendGridInboundProvider";
|
|
3
|
+
import {
|
|
4
|
+
InboundEmailProviderType,
|
|
5
|
+
InboundEmailProvider as InboundEmailProviderConfig,
|
|
6
|
+
InboundEmailDomain,
|
|
7
|
+
InboundEmailWebhookSecret,
|
|
8
|
+
} from "../../EnvironmentConfig";
|
|
9
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
10
|
+
|
|
11
|
+
export default class InboundEmailProviderFactory {
|
|
12
|
+
private static instance: InboundEmailProvider | null = null;
|
|
13
|
+
|
|
14
|
+
public static getProvider(): InboundEmailProvider {
|
|
15
|
+
if (this.instance) {
|
|
16
|
+
return this.instance;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!InboundEmailDomain) {
|
|
20
|
+
throw new BadDataException(
|
|
21
|
+
"Inbound email is not configured. Please set the INBOUND_EMAIL_DOMAIN environment variable.",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch (InboundEmailProviderConfig) {
|
|
26
|
+
case InboundEmailProviderType.SendGrid:
|
|
27
|
+
this.instance = new SendGridInboundProvider({
|
|
28
|
+
inboundDomain: InboundEmailDomain,
|
|
29
|
+
webhookSecret: InboundEmailWebhookSecret,
|
|
30
|
+
});
|
|
31
|
+
break;
|
|
32
|
+
|
|
33
|
+
default:
|
|
34
|
+
throw new BadDataException(
|
|
35
|
+
`Unknown inbound email provider: ${InboundEmailProviderConfig}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return this.instance;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if inbound email is configured
|
|
44
|
+
*/
|
|
45
|
+
public static isConfigured(): boolean {
|
|
46
|
+
return Boolean(InboundEmailDomain);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate the email address for a monitor based on its secret key
|
|
51
|
+
*/
|
|
52
|
+
public static generateMonitorEmailAddress(secretKey: string): string {
|
|
53
|
+
const provider: InboundEmailProvider = this.getProvider();
|
|
54
|
+
|
|
55
|
+
return provider.generateMonitorEmailAddress(secretKey);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract the secret key from an email address
|
|
60
|
+
*/
|
|
61
|
+
public static extractSecretKeyFromEmail(email: string): string | null {
|
|
62
|
+
const provider: InboundEmailProvider = this.getProvider();
|
|
63
|
+
|
|
64
|
+
return provider.extractSecretKeyFromEmail(email);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the inbound domain
|
|
69
|
+
*/
|
|
70
|
+
public static getInboundDomain(): string | undefined {
|
|
71
|
+
return InboundEmailDomain;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reset the singleton instance (useful for testing)
|
|
76
|
+
*/
|
|
77
|
+
public static resetInstance(): void {
|
|
78
|
+
this.instance = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import InboundEmailProvider, {
|
|
2
|
+
InboundEmailProviderConfig,
|
|
3
|
+
ParsedInboundEmail,
|
|
4
|
+
} from "../InboundEmailProvider";
|
|
5
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
6
|
+
|
|
7
|
+
// SendGrid uses the base config - add SendGrid-specific options here if needed in the future
|
|
8
|
+
export type SendGridInboundConfig = InboundEmailProviderConfig;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SendGrid Inbound Parse provider implementation
|
|
12
|
+
*
|
|
13
|
+
* This provider handles email parsing from SendGrid's Inbound Parse webhook.
|
|
14
|
+
* SendGrid sends emails to the webhook as multipart/form-data with the following fields:
|
|
15
|
+
* - from: Email sender
|
|
16
|
+
* - to: Email recipient(s)
|
|
17
|
+
* - subject: Email subject
|
|
18
|
+
* - text: Plain text body
|
|
19
|
+
* - html: HTML body
|
|
20
|
+
* - headers: Email headers as a string
|
|
21
|
+
* - envelope: JSON envelope with from/to addresses
|
|
22
|
+
* - attachments: Number of attachments
|
|
23
|
+
* - attachment-info: JSON with attachment metadata
|
|
24
|
+
*/
|
|
25
|
+
export default class SendGridInboundProvider extends InboundEmailProvider {
|
|
26
|
+
public constructor(config: SendGridInboundConfig) {
|
|
27
|
+
super(config);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async parseInboundEmail(
|
|
31
|
+
rawData: JSONObject,
|
|
32
|
+
): Promise<ParsedInboundEmail> {
|
|
33
|
+
/*
|
|
34
|
+
* SendGrid Inbound Parse webhook format
|
|
35
|
+
* Reference: https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const from: string = this.extractEmailAddress(
|
|
39
|
+
rawData["from"]?.toString() || "",
|
|
40
|
+
);
|
|
41
|
+
const to: string = this.extractEmailAddress(
|
|
42
|
+
rawData["to"]?.toString() || "",
|
|
43
|
+
);
|
|
44
|
+
const subject: string = rawData["subject"]?.toString() || "";
|
|
45
|
+
const body: string = rawData["text"]?.toString() || "";
|
|
46
|
+
const bodyHtml: string | undefined = rawData["html"]?.toString();
|
|
47
|
+
const headers: Record<string, string> | undefined = this.parseHeaders(
|
|
48
|
+
rawData["headers"]?.toString(),
|
|
49
|
+
);
|
|
50
|
+
const attachments:
|
|
51
|
+
| Array<{ filename: string; contentType: string; size: number }>
|
|
52
|
+
| undefined = this.parseAttachments(rawData);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
from,
|
|
56
|
+
to,
|
|
57
|
+
subject,
|
|
58
|
+
body,
|
|
59
|
+
bodyHtml,
|
|
60
|
+
headers,
|
|
61
|
+
attachments,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public async validateWebhook(data: {
|
|
66
|
+
headers: Record<string, string>;
|
|
67
|
+
body: JSONObject | string;
|
|
68
|
+
}): Promise<boolean> {
|
|
69
|
+
/*
|
|
70
|
+
* SendGrid Inbound Parse doesn't provide webhook signature verification by default
|
|
71
|
+
* We can optionally validate using a custom header if configured
|
|
72
|
+
*/
|
|
73
|
+
if (this.config.webhookSecret) {
|
|
74
|
+
const providedSecret: string | undefined =
|
|
75
|
+
data.headers["x-oneuptime-webhook-secret"];
|
|
76
|
+
|
|
77
|
+
return providedSecret === this.config.webhookSecret;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* If no webhook secret is configured, accept all requests
|
|
82
|
+
* This is acceptable for SendGrid as the webhook URL itself is a secret
|
|
83
|
+
*/
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public extractSecretKeyFromEmail(email: string): string | null {
|
|
88
|
+
/*
|
|
89
|
+
* Extract from: monitor-{secretKey}@inbound.domain.com
|
|
90
|
+
* Handle email formats like "name@domain" or "Name <name@domain>"
|
|
91
|
+
*/
|
|
92
|
+
const emailAddress: string = this.extractEmailAddress(email);
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* Create regex pattern that matches the email prefix format
|
|
96
|
+
* The secret key is a UUID-like string
|
|
97
|
+
*/
|
|
98
|
+
const pattern: RegExp = new RegExp(
|
|
99
|
+
`^monitor-([a-zA-Z0-9-]+)@${this.escapeRegex(this.config.inboundDomain)}$`,
|
|
100
|
+
"i",
|
|
101
|
+
);
|
|
102
|
+
const match: RegExpMatchArray | null = emailAddress.match(pattern);
|
|
103
|
+
|
|
104
|
+
return match ? match[1]! : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public generateMonitorEmailAddress(secretKey: string): string {
|
|
108
|
+
return `monitor-${secretKey}@${this.config.inboundDomain}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract email address from various formats
|
|
113
|
+
* Examples:
|
|
114
|
+
* - "user@domain.com" -> "user@domain.com"
|
|
115
|
+
* - "User Name <user@domain.com>" -> "user@domain.com"
|
|
116
|
+
* - "<user@domain.com>" -> "user@domain.com"
|
|
117
|
+
*/
|
|
118
|
+
private extractEmailAddress(email: string): string {
|
|
119
|
+
const match: RegExpMatchArray | null = email.match(/<([^>]+)>/);
|
|
120
|
+
|
|
121
|
+
if (match && match[1]) {
|
|
122
|
+
return match[1].toLowerCase().trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return email.toLowerCase().trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse SendGrid headers string into a key-value object
|
|
130
|
+
* SendGrid sends headers as a string with each header on a new line
|
|
131
|
+
*/
|
|
132
|
+
private parseHeaders(
|
|
133
|
+
headersString?: string,
|
|
134
|
+
): Record<string, string> | undefined {
|
|
135
|
+
if (!headersString) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const headers: Record<string, string> = {};
|
|
140
|
+
const lines: string[] = headersString.split("\n");
|
|
141
|
+
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
const colonIndex: number = line.indexOf(":");
|
|
144
|
+
|
|
145
|
+
if (colonIndex > 0) {
|
|
146
|
+
const key: string = line.substring(0, colonIndex).trim();
|
|
147
|
+
const value: string = line.substring(colonIndex + 1).trim();
|
|
148
|
+
|
|
149
|
+
headers[key] = value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse attachments from SendGrid webhook data
|
|
158
|
+
*/
|
|
159
|
+
private parseAttachments(
|
|
160
|
+
rawData: JSONObject,
|
|
161
|
+
):
|
|
162
|
+
| Array<{ filename: string; contentType: string; size: number }>
|
|
163
|
+
| undefined {
|
|
164
|
+
const attachmentCount: number = parseInt(
|
|
165
|
+
rawData["attachments"]?.toString() || "0",
|
|
166
|
+
10,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (attachmentCount === 0) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Parse attachment-info JSON if available
|
|
174
|
+
const attachmentInfoStr: string | undefined =
|
|
175
|
+
rawData["attachment-info"]?.toString();
|
|
176
|
+
|
|
177
|
+
if (!attachmentInfoStr) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const attachmentInfo: JSONObject = JSON.parse(attachmentInfoStr);
|
|
183
|
+
const attachments: Array<{
|
|
184
|
+
filename: string;
|
|
185
|
+
contentType: string;
|
|
186
|
+
size: number;
|
|
187
|
+
}> = [];
|
|
188
|
+
|
|
189
|
+
for (const key of Object.keys(attachmentInfo)) {
|
|
190
|
+
const info: JSONObject = attachmentInfo[key] as JSONObject;
|
|
191
|
+
|
|
192
|
+
attachments.push({
|
|
193
|
+
filename: info["filename"]?.toString() || "unknown",
|
|
194
|
+
contentType: info["type"]?.toString() || "application/octet-stream",
|
|
195
|
+
size: parseInt(info["content-length"]?.toString() || "0", 10),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return attachments.length > 0 ? attachments : undefined;
|
|
200
|
+
} catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Escape special regex characters in a string
|
|
207
|
+
*/
|
|
208
|
+
private escapeRegex(str: string): string {
|
|
209
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -482,6 +482,10 @@ export class Service extends DatabaseService<Model> {
|
|
|
482
482
|
createBy.data.incomingRequestSecretKey = ObjectID.generate();
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
if (createBy.data.monitorType === MonitorType.IncomingEmail) {
|
|
486
|
+
createBy.data.incomingEmailSecretKey = ObjectID.generate();
|
|
487
|
+
}
|
|
488
|
+
|
|
485
489
|
if (!createBy.props.tenantId) {
|
|
486
490
|
throw new BadDataException("ProjectId required to create monitor.");
|
|
487
491
|
}
|