@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.
Files changed (56) hide show
  1. package/Models/DatabaseModels/Monitor.ts +109 -0
  2. package/Server/EnvironmentConfig.ts +16 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1768335589018-AddIncomingEmailMonitor.ts +53 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Middleware/MultipartFormData.ts +17 -0
  6. package/Server/Services/InboundEmail/InboundEmailProvider.ts +64 -0
  7. package/Server/Services/InboundEmail/InboundEmailProviderFactory.ts +80 -0
  8. package/Server/Services/InboundEmail/Providers/SendGridInboundProvider.ts +211 -0
  9. package/Server/Services/MonitorService.ts +4 -0
  10. package/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.ts +248 -0
  11. package/Server/Utils/Monitor/DataToProcess.ts +2 -0
  12. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +13 -0
  13. package/Types/Monitor/CriteriaFilter.ts +7 -0
  14. package/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest.ts +25 -0
  15. package/Types/Monitor/MonitorCriteriaInstance.ts +67 -0
  16. package/Types/Monitor/MonitorType.ts +9 -0
  17. package/UI/Components/ModelDetail/CardModelDetail.tsx +34 -0
  18. package/UI/Components/ModelTable/BaseModelTable.tsx +33 -1
  19. package/UI/Config.ts +3 -0
  20. package/build/dist/Models/DatabaseModels/Monitor.js +110 -0
  21. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  22. package/build/dist/Server/EnvironmentConfig.js +10 -0
  23. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  24. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1768335589018-AddIncomingEmailMonitor.js +24 -0
  25. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1768335589018-AddIncomingEmailMonitor.js.map +1 -0
  26. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  27. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  28. package/build/dist/Server/Middleware/MultipartFormData.js +13 -0
  29. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -0
  30. package/build/dist/Server/Services/InboundEmail/InboundEmailProvider.js +12 -0
  31. package/build/dist/Server/Services/InboundEmail/InboundEmailProvider.js.map +1 -0
  32. package/build/dist/Server/Services/InboundEmail/InboundEmailProviderFactory.js +59 -0
  33. package/build/dist/Server/Services/InboundEmail/InboundEmailProviderFactory.js.map +1 -0
  34. package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js +148 -0
  35. package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js.map +1 -0
  36. package/build/dist/Server/Services/MonitorService.js +3 -0
  37. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  38. package/build/dist/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.js +164 -0
  39. package/build/dist/Server/Utils/Monitor/Criteria/IncomingEmailCriteria.js.map +1 -0
  40. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +10 -0
  41. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  42. package/build/dist/Types/Monitor/CriteriaFilter.js +6 -0
  43. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  44. package/build/dist/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest.js +2 -0
  45. package/build/dist/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest.js.map +1 -0
  46. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +62 -0
  47. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  48. package/build/dist/Types/Monitor/MonitorType.js +8 -0
  49. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  50. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +29 -0
  51. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  52. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +29 -1
  53. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  54. package/build/dist/UI/Config.js +1 -0
  55. package/build/dist/UI/Config.js.map +1 -1
  56. 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;
@@ -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
  }