@promptbook/cli 0.104.0-3 → 0.104.0-5
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/apps/agents-server/src/app/admin/messages/MessagesClient.tsx +294 -0
- package/apps/agents-server/src/app/admin/messages/page.tsx +13 -0
- package/apps/agents-server/src/app/admin/messages/send-email/SendEmailClient.tsx +104 -0
- package/apps/agents-server/src/app/admin/messages/send-email/actions.ts +35 -0
- package/apps/agents-server/src/app/admin/messages/send-email/page.tsx +13 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +4 -0
- package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/route.ts +139 -0
- package/apps/agents-server/src/app/api/messages/route.ts +102 -0
- package/apps/agents-server/src/components/Header/Header.tsx +4 -0
- package/apps/agents-server/src/database/$provideSupabaseForBrowser.ts +3 -3
- package/apps/agents-server/src/database/$provideSupabaseForServer.ts +1 -1
- package/apps/agents-server/src/database/$provideSupabaseForWorker.ts +3 -3
- package/apps/agents-server/src/database/migrate.ts +34 -1
- package/apps/agents-server/src/database/migrations/2025-11-0001-initial-schema.sql +1 -3
- package/apps/agents-server/src/database/migrations/2025-11-0002-metadata-table.sql +1 -3
- package/apps/agents-server/src/database/migrations/2025-12-0402-message-table.sql +42 -0
- package/apps/agents-server/src/database/schema.ts +95 -4
- package/apps/agents-server/src/message-providers/email/_common/Email.ts +73 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/TODO.txt +1 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.test.ts.todo +108 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.ts +62 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.test.ts.todo +117 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.ts +19 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.test.ts.todo +119 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.ts +19 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.test.ts.todo +74 -0
- package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.ts +14 -0
- package/apps/agents-server/src/message-providers/email/sendgrid/SendgridMessageProvider.ts +44 -0
- package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +51 -0
- package/apps/agents-server/src/message-providers/index.ts +13 -0
- package/apps/agents-server/src/message-providers/interfaces/MessageProvider.ts +11 -0
- package/apps/agents-server/src/utils/messages/sendMessage.ts +91 -0
- package/apps/agents-server/src/utils/messagesAdmin.ts +72 -0
- package/apps/agents-server/src/utils/normalization/filenameToPrompt.test.ts +36 -0
- package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +6 -2
- package/esm/index.es.js +8098 -8067
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +18 -15
- package/esm/typings/src/llm-providers/_multiple/MultipleLlmExecutionTools.d.ts +6 -2
- package/esm/typings/src/llm-providers/remote/RemoteLlmExecutionTools.d.ts +1 -0
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +8114 -8083
- package/umd/index.umd.js.map +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { parseEmailAddresses } from './parseEmailAddresses';
|
|
3
|
+
|
|
4
|
+
describe('how parseEmailAddresses works', () => {
|
|
5
|
+
it('should work with single email', () => {
|
|
6
|
+
expect(parseEmailAddresses('pavol@webgpt.cz')).toEqual([
|
|
7
|
+
{
|
|
8
|
+
fullName: null,
|
|
9
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
10
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
11
|
+
plus: [],
|
|
12
|
+
},
|
|
13
|
+
]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should work with simple emails', () => {
|
|
17
|
+
expect(parseEmailAddresses('pavol@webgpt.cz, jirka@webgpt.cz, tomas@webgpt.cz')).toEqual([
|
|
18
|
+
{
|
|
19
|
+
fullName: null,
|
|
20
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
21
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
22
|
+
plus: [],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
fullName: null,
|
|
26
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
27
|
+
fullEmail: 'jirka@webgpt.cz',
|
|
28
|
+
plus: [],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
fullName: null,
|
|
32
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
33
|
+
fullEmail: 'tomas@webgpt.cz',
|
|
34
|
+
plus: [],
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should work with fullname', () => {
|
|
40
|
+
expect(
|
|
41
|
+
parseEmailAddresses(
|
|
42
|
+
'Pavol Hejný <pavol@webgpt.cz>, Jirka <jirka@webgpt.cz>, "Tomáš Studeník" <tomas@webgpt.cz>',
|
|
43
|
+
),
|
|
44
|
+
).toEqual([
|
|
45
|
+
{
|
|
46
|
+
fullName: 'Pavol Hejný',
|
|
47
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
48
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
49
|
+
plus: [],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
fullName: 'Jirka',
|
|
53
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
54
|
+
fullEmail: 'jirka@webgpt.cz',
|
|
55
|
+
plus: [],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
fullName: 'Tomáš Studeník',
|
|
59
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
60
|
+
fullEmail: 'tomas@webgpt.cz',
|
|
61
|
+
plus: [],
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('not confused by comma', () => {
|
|
67
|
+
expect(parseEmailAddresses(', pavol@webgpt.cz, ')).toEqual([
|
|
68
|
+
{
|
|
69
|
+
fullName: null,
|
|
70
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
71
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
72
|
+
plus: [],
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('works on real-life example', () => {
|
|
78
|
+
expect(
|
|
79
|
+
parseEmailAddresses(
|
|
80
|
+
'"bob" <bob@bot.webgpt.cz>, "pavolto" <pavol+to@ptbk.io>, "Pavol" <pavol@collboard.com>',
|
|
81
|
+
),
|
|
82
|
+
).toEqual([
|
|
83
|
+
{
|
|
84
|
+
fullName: 'bob',
|
|
85
|
+
fullEmail: 'bob@bot.webgpt.cz',
|
|
86
|
+
baseEmail: 'bob@bot.webgpt.cz',
|
|
87
|
+
plus: [],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
fullName: 'pavolto',
|
|
91
|
+
fullEmail: 'pavol+to@ptbk.io',
|
|
92
|
+
baseEmail: 'pavol@ptbk.io',
|
|
93
|
+
plus: ['to'],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
fullName: 'Pavol',
|
|
97
|
+
fullEmail: 'pavol@collboard.com',
|
|
98
|
+
baseEmail: 'pavol@collboard.com',
|
|
99
|
+
plus: [],
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('throws on invalid email adresses', () => {
|
|
105
|
+
expect(() => parseEmailAddresses('Pavol, Hejný')).toThrowError(/Invalid email address/);
|
|
106
|
+
expect(() => parseEmailAddresses('Pavol Hejný <>')).toThrowError(/Invalid email address/);
|
|
107
|
+
expect(() => parseEmailAddresses('Pavol Hejný, <@webgpt.cz>')).toThrowError(/Invalid email address/);
|
|
108
|
+
expect(() => parseEmailAddresses('Pavol Hejný <webgpt.cz>')).toThrowError(/Invalid email address/);
|
|
109
|
+
expect(() => parseEmailAddresses('Pavol Hejný <pavol@>')).toThrowError(/Invalid email address/);
|
|
110
|
+
expect(() => parseEmailAddresses('Pavol Hejný <a@b>,')).toThrowError(/Invalid email address/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* TODO: [🐫] This test fails because of aliased imports `import type { string_emails } from '@promptbook-local/types';`, fix it
|
|
117
|
+
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { string_emails } from '@promptbook-local/types';
|
|
2
|
+
import { spaceTrim } from 'spacetrim';
|
|
3
|
+
import type { EmailAddress } from '../Email';
|
|
4
|
+
import { parseEmailAddress } from './parseEmailAddress';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses the email addresses into its components
|
|
8
|
+
*/
|
|
9
|
+
export function parseEmailAddresses(value: string_emails): Array<EmailAddress> {
|
|
10
|
+
const emailAddresses = value
|
|
11
|
+
.split(',')
|
|
12
|
+
.map((email) => spaceTrim(email))
|
|
13
|
+
.filter((email) => email !== '')
|
|
14
|
+
.map((email) => parseEmailAddress(email));
|
|
15
|
+
|
|
16
|
+
// console.log('parseEmailAddresses', value, '->', emailAddresses);
|
|
17
|
+
|
|
18
|
+
return emailAddresses;
|
|
19
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { stringifyEmailAddress } from './stringifyEmailAddress';
|
|
3
|
+
|
|
4
|
+
describe('how stringifyEmailAddress works', () => {
|
|
5
|
+
it('should work with simple email', () => {
|
|
6
|
+
expect(
|
|
7
|
+
stringifyEmailAddress({
|
|
8
|
+
fullName: null,
|
|
9
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
10
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
11
|
+
plus: [],
|
|
12
|
+
}),
|
|
13
|
+
).toBe('pavol@webgpt.cz');
|
|
14
|
+
expect(
|
|
15
|
+
stringifyEmailAddress({
|
|
16
|
+
fullName: null,
|
|
17
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
18
|
+
fullEmail: 'jirka@webgpt.cz',
|
|
19
|
+
plus: [],
|
|
20
|
+
}),
|
|
21
|
+
).toBe('jirka@webgpt.cz');
|
|
22
|
+
expect(
|
|
23
|
+
stringifyEmailAddress({
|
|
24
|
+
fullName: null,
|
|
25
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
26
|
+
fullEmail: 'tomas@webgpt.cz',
|
|
27
|
+
plus: [],
|
|
28
|
+
}),
|
|
29
|
+
).toBe('tomas@webgpt.cz');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should work with fullname', () => {
|
|
33
|
+
expect(
|
|
34
|
+
stringifyEmailAddress({
|
|
35
|
+
fullName: 'Pavol Hejný',
|
|
36
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
37
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
38
|
+
plus: [],
|
|
39
|
+
}),
|
|
40
|
+
).toBe('"Pavol Hejný" <pavol@webgpt.cz>');
|
|
41
|
+
expect(
|
|
42
|
+
stringifyEmailAddress({
|
|
43
|
+
fullName: 'Jirka',
|
|
44
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
45
|
+
fullEmail: 'jirka@webgpt.cz',
|
|
46
|
+
plus: [],
|
|
47
|
+
}),
|
|
48
|
+
).toBe('"Jirka" <jirka@webgpt.cz>');
|
|
49
|
+
expect(
|
|
50
|
+
stringifyEmailAddress({
|
|
51
|
+
fullName: 'Tomáš Studeník',
|
|
52
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
53
|
+
fullEmail: 'tomas@webgpt.cz',
|
|
54
|
+
plus: [],
|
|
55
|
+
}),
|
|
56
|
+
).toBe('"Tomáš Studeník" <tomas@webgpt.cz>');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should work with plus', () => {
|
|
60
|
+
expect(
|
|
61
|
+
stringifyEmailAddress({
|
|
62
|
+
fullName: null,
|
|
63
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
64
|
+
fullEmail: 'pavol+test@webgpt.cz',
|
|
65
|
+
plus: ['test'],
|
|
66
|
+
}),
|
|
67
|
+
).toBe('pavol+test@webgpt.cz');
|
|
68
|
+
expect(
|
|
69
|
+
stringifyEmailAddress({
|
|
70
|
+
fullName: null,
|
|
71
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
72
|
+
fullEmail: 'jirka+test@webgpt.cz',
|
|
73
|
+
plus: ['test'],
|
|
74
|
+
}),
|
|
75
|
+
).toBe('jirka+test@webgpt.cz');
|
|
76
|
+
expect(
|
|
77
|
+
stringifyEmailAddress({
|
|
78
|
+
fullName: null,
|
|
79
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
80
|
+
fullEmail: 'tomas+test+ainautes@webgpt.cz',
|
|
81
|
+
plus: ['test', 'ainautes'],
|
|
82
|
+
}),
|
|
83
|
+
).toBe('tomas+test+ainautes@webgpt.cz');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should work with both fullname and plus', () => {
|
|
87
|
+
expect(
|
|
88
|
+
stringifyEmailAddress({
|
|
89
|
+
fullName: 'Pavol Hejný',
|
|
90
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
91
|
+
fullEmail: 'pavol+test@webgpt.cz',
|
|
92
|
+
plus: ['test'],
|
|
93
|
+
}),
|
|
94
|
+
).toBe('"Pavol Hejný" <pavol+test@webgpt.cz>');
|
|
95
|
+
expect(
|
|
96
|
+
stringifyEmailAddress({
|
|
97
|
+
fullName: 'Jirka',
|
|
98
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
99
|
+
fullEmail: 'jirka+test@webgpt.cz',
|
|
100
|
+
plus: ['test'],
|
|
101
|
+
}),
|
|
102
|
+
).toBe('"Jirka" <jirka+test@webgpt.cz>');
|
|
103
|
+
expect(
|
|
104
|
+
stringifyEmailAddress({
|
|
105
|
+
fullName: 'Tomáš Studeník',
|
|
106
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
107
|
+
fullEmail: 'tomas+test+ainautes@webgpt.cz',
|
|
108
|
+
plus: ['test', 'ainautes'],
|
|
109
|
+
}),
|
|
110
|
+
).toBe('"Tomáš Studeník" <tomas+test+ainautes@webgpt.cz>');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// TODO: [🎾] Implement and test here escaping
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* TODO: [🐫] This test fails because of aliased imports `import type { string_emails } from '@promptbook-local/types';`, fix it
|
|
119
|
+
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { string_email } from '@promptbook-local/types';
|
|
2
|
+
import type { EmailAddress } from '../Email';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Makes string email from EmailAddress
|
|
6
|
+
*/
|
|
7
|
+
export function stringifyEmailAddress(emailAddress: EmailAddress): string_email {
|
|
8
|
+
const { fullEmail, fullName } = emailAddress;
|
|
9
|
+
|
|
10
|
+
if (fullName !== null) {
|
|
11
|
+
return `"${fullName}" <${fullEmail}>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return fullEmail;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* TODO: [🎾] Implement and test here escaping
|
|
19
|
+
*/
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { stringifyEmailAddresses } from './stringifyEmailAddresses';
|
|
3
|
+
|
|
4
|
+
describe('how stringifyEmailAddresses works', () => {
|
|
5
|
+
it('should work with single email', () => {
|
|
6
|
+
expect(
|
|
7
|
+
stringifyEmailAddresses([
|
|
8
|
+
{
|
|
9
|
+
fullName: null,
|
|
10
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
11
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
12
|
+
plus: [],
|
|
13
|
+
},
|
|
14
|
+
]),
|
|
15
|
+
).toEqual('pavol@webgpt.cz');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should work with simple emails', () => {
|
|
19
|
+
expect(
|
|
20
|
+
stringifyEmailAddresses([
|
|
21
|
+
{
|
|
22
|
+
fullName: null,
|
|
23
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
24
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
25
|
+
plus: [],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
fullName: null,
|
|
29
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
30
|
+
fullEmail: 'jirka@webgpt.cz',
|
|
31
|
+
plus: [],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
fullName: null,
|
|
35
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
36
|
+
fullEmail: 'tomas@webgpt.cz',
|
|
37
|
+
plus: [],
|
|
38
|
+
},
|
|
39
|
+
]),
|
|
40
|
+
).toEqual('pavol@webgpt.cz, jirka@webgpt.cz, tomas@webgpt.cz');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should work with fullname', () => {
|
|
44
|
+
expect(
|
|
45
|
+
stringifyEmailAddresses([
|
|
46
|
+
{
|
|
47
|
+
fullName: 'Pavol Hejný',
|
|
48
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
49
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
50
|
+
plus: [],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
fullName: 'Jiří Jahn',
|
|
54
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
55
|
+
fullEmail: 'jirka@webgpt.cz',
|
|
56
|
+
plus: [],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
fullName: 'Tomáš Studeník',
|
|
60
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
61
|
+
fullEmail: 'tomas@webgpt.cz',
|
|
62
|
+
plus: [],
|
|
63
|
+
},
|
|
64
|
+
]),
|
|
65
|
+
).toEqual('"Pavol Hejný" <pavol@webgpt.cz>, "Jiří Jahn" <jirka@webgpt.cz>, "Tomáš Studeník" <tomas@webgpt.cz>');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// TODO: [🎾] Implement and test here escaping
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* TODO: [🐫] This test fails because of aliased imports `import type { string_emails } from '@promptbook-local/types';`, fix it
|
|
74
|
+
*/
|
package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { string_emails } from '@promptbook-local/types';
|
|
2
|
+
import type { EmailAddress } from '../Email';
|
|
3
|
+
import { stringifyEmailAddress } from './stringifyEmailAddress';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Makes string email from multiple EmailAddress
|
|
7
|
+
*/
|
|
8
|
+
export function stringifyEmailAddresses(emailAddresses: Array<EmailAddress>): string_emails {
|
|
9
|
+
return emailAddresses.map((emailAddress) => stringifyEmailAddress(emailAddress)).join(', ');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* TODO: [🎾] Implement and test here escaping
|
|
14
|
+
*/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { removeMarkdownFormatting } from '@promptbook-local/markdown-utils';
|
|
2
|
+
import type { really_any } from '@promptbook-local/types';
|
|
3
|
+
import sendgridEmailClient from '@sendgrid/mail';
|
|
4
|
+
import { marked } from 'marked';
|
|
5
|
+
import { MessageProvider } from '../../interfaces/MessageProvider';
|
|
6
|
+
import { OutboundEmail } from '../_common/Email';
|
|
7
|
+
import { parseEmailAddress } from '../_common/utils/parseEmailAddress';
|
|
8
|
+
|
|
9
|
+
export class SendgridMessageProvider implements MessageProvider {
|
|
10
|
+
constructor(private readonly apiKey: string) {
|
|
11
|
+
sendgridEmailClient.setApiKey(this.apiKey);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public async send(message: OutboundEmail): Promise<really_any> {
|
|
15
|
+
const sender = message.sender;
|
|
16
|
+
const recipients = (Array.isArray(message.recipients) ? message.recipients : [message.recipients]).filter(
|
|
17
|
+
Boolean,
|
|
18
|
+
) as really_any[];
|
|
19
|
+
|
|
20
|
+
const text = removeMarkdownFormatting(message.content);
|
|
21
|
+
const html = await marked.parse(message.content);
|
|
22
|
+
|
|
23
|
+
const { fullEmail, fullName } = parseEmailAddress(sender);
|
|
24
|
+
|
|
25
|
+
const response = await sendgridEmailClient.send({
|
|
26
|
+
from: {
|
|
27
|
+
email: fullEmail,
|
|
28
|
+
name: fullName || undefined,
|
|
29
|
+
},
|
|
30
|
+
to: recipients.map((r) => {
|
|
31
|
+
const { fullEmail, fullName } = parseEmailAddress(r.email || r.baseEmail || r);
|
|
32
|
+
return {
|
|
33
|
+
email: fullEmail,
|
|
34
|
+
name: r.name || fullName || undefined,
|
|
35
|
+
};
|
|
36
|
+
}),
|
|
37
|
+
subject: message.metadata?.subject || 'No Subject',
|
|
38
|
+
text,
|
|
39
|
+
html,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { removeMarkdownFormatting } from '@promptbook-local/markdown-utils';
|
|
2
|
+
import type { really_any } from '@promptbook-local/types';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
// @ts-expect-error: Zeptomail types are not resolving correctly
|
|
5
|
+
import { SendMailClient } from 'zeptomail';
|
|
6
|
+
import { MessageProvider } from '../../interfaces/MessageProvider';
|
|
7
|
+
import { OutboundEmail } from '../_common/Email';
|
|
8
|
+
|
|
9
|
+
export class ZeptomailMessageProvider implements MessageProvider {
|
|
10
|
+
constructor(private readonly apiKey: string) {}
|
|
11
|
+
|
|
12
|
+
public async send(message: OutboundEmail): Promise<really_any> {
|
|
13
|
+
try {
|
|
14
|
+
const client = new SendMailClient({ url: 'api.zeptomail.com/', token: this.apiKey });
|
|
15
|
+
|
|
16
|
+
const sender = message.sender as really_any;
|
|
17
|
+
const recipients = (Array.isArray(message.recipients) ? message.recipients : [message.recipients]).filter(
|
|
18
|
+
Boolean,
|
|
19
|
+
) as really_any[];
|
|
20
|
+
|
|
21
|
+
const textbody = removeMarkdownFormatting(message.content);
|
|
22
|
+
const htmlbody = await marked.parse(message.content);
|
|
23
|
+
|
|
24
|
+
const response = await client.sendMail({
|
|
25
|
+
from: {
|
|
26
|
+
address: sender.email || sender.baseEmail || sender,
|
|
27
|
+
name: sender.name || sender.fullName || undefined,
|
|
28
|
+
},
|
|
29
|
+
to: recipients.map((r) => ({
|
|
30
|
+
email_address: {
|
|
31
|
+
address: r.email || r.baseEmail || r,
|
|
32
|
+
name: r.name || r.fullName || undefined,
|
|
33
|
+
},
|
|
34
|
+
})),
|
|
35
|
+
subject: message.metadata?.subject || 'No Subject',
|
|
36
|
+
textbody,
|
|
37
|
+
htmlbody,
|
|
38
|
+
track_clicks: true,
|
|
39
|
+
track_opens: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return response;
|
|
43
|
+
} catch (raw: really_any) {
|
|
44
|
+
if (!('error' in raw)) {
|
|
45
|
+
throw raw;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(raw.error.message, raw.error.details);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SendgridMessageProvider } from './email/sendgrid/SendgridMessageProvider';
|
|
2
|
+
import { ZeptomailMessageProvider } from './email/zeptomail/ZeptomailMessageProvider';
|
|
3
|
+
import { MessageProvider } from './interfaces/MessageProvider';
|
|
4
|
+
|
|
5
|
+
export const EMAIL_PROVIDERS: Record<string, MessageProvider> = {};
|
|
6
|
+
|
|
7
|
+
if (process.env.ZEPTOMAIL_API_KEY) {
|
|
8
|
+
EMAIL_PROVIDERS['ZEPTOMAIL'] = new ZeptomailMessageProvider(process.env.ZEPTOMAIL_API_KEY);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (process.env.SENDGRID_API_KEY) {
|
|
12
|
+
EMAIL_PROVIDERS['SENDGRID'] = new SendgridMessageProvider(process.env.SENDGRID_API_KEY);
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Message, really_any, string_email } from '@promptbook-local/types';
|
|
2
|
+
|
|
3
|
+
export type MessageProvider = {
|
|
4
|
+
/**
|
|
5
|
+
* Sends a message through the provider
|
|
6
|
+
*
|
|
7
|
+
* @param message The message to send
|
|
8
|
+
* @returns Raw response from the provider
|
|
9
|
+
*/
|
|
10
|
+
send(message: Message<string_email>): Promise<really_any>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { really_any } from '@promptbook-local/types';
|
|
2
|
+
import { serializeError } from '@promptbook-local/utils';
|
|
3
|
+
import { assertsError } from '../../../../../src/errors/assertsError';
|
|
4
|
+
import { $getTableName } from '../../database/$getTableName';
|
|
5
|
+
import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
|
|
6
|
+
import { EMAIL_PROVIDERS } from '../../message-providers';
|
|
7
|
+
import { OutboundEmail } from '../../message-providers/email/_common/Email';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sends a message
|
|
11
|
+
*/
|
|
12
|
+
export async function sendMessage(message: OutboundEmail): Promise<void> {
|
|
13
|
+
const supabase = await $provideSupabaseForServer();
|
|
14
|
+
|
|
15
|
+
// 1. Insert message
|
|
16
|
+
const { data: insertedMessage, error: insertError } = await supabase
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
.from(await $getTableName('Message'))
|
|
19
|
+
.insert({
|
|
20
|
+
channel: message.channel || 'UNKNOWN',
|
|
21
|
+
direction: message.direction || 'OUTBOUND',
|
|
22
|
+
sender: message.sender,
|
|
23
|
+
recipients: message.recipients,
|
|
24
|
+
content: message.content,
|
|
25
|
+
threadId: message.threadId,
|
|
26
|
+
metadata: message.metadata,
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
} as any)
|
|
29
|
+
.select()
|
|
30
|
+
.single();
|
|
31
|
+
|
|
32
|
+
if (insertError) {
|
|
33
|
+
throw new Error(`Failed to insert message: ${insertError.message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!insertedMessage) {
|
|
37
|
+
throw new Error('Failed to insert message: No data returned');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. If outbound and email, try to send
|
|
41
|
+
if (message.direction === 'OUTBOUND' && message.channel === 'EMAIL') {
|
|
42
|
+
const providers = Object.keys(EMAIL_PROVIDERS);
|
|
43
|
+
|
|
44
|
+
if (providers.length === 0) {
|
|
45
|
+
console.warn('No email providers configured');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let isSent = false;
|
|
50
|
+
|
|
51
|
+
for (const providerName of providers) {
|
|
52
|
+
const provider = EMAIL_PROVIDERS[providerName];
|
|
53
|
+
let isSuccessful = false;
|
|
54
|
+
let raw: really_any = null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
console.log(`📤 Sending email via ${providerName}`);
|
|
58
|
+
raw = await provider.send(message);
|
|
59
|
+
isSuccessful = true;
|
|
60
|
+
isSent = true;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
assertsError(error);
|
|
63
|
+
console.error(`Failed to send email via ${providerName}`, error);
|
|
64
|
+
raw = { error: serializeError(error) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Log attempt
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
await supabase.from(await $getTableName('MessageSendAttempt')).insert({
|
|
70
|
+
// @ts-expect-error: insertedMessage is any
|
|
71
|
+
messageId: insertedMessage.id,
|
|
72
|
+
providerName,
|
|
73
|
+
isSuccessful,
|
|
74
|
+
raw,
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
} as any);
|
|
77
|
+
|
|
78
|
+
if (isSuccessful) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isSent) {
|
|
84
|
+
throw new Error('Failed to send email via any provider');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* TODO: !!!! Move to `message-providers` and rename `message-providers` -> `messages`
|
|
91
|
+
*/
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Json } from '../database/schema';
|
|
2
|
+
|
|
3
|
+
export type MessageRow = {
|
|
4
|
+
id: number;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
channel: string;
|
|
7
|
+
direction: string;
|
|
8
|
+
sender: Json;
|
|
9
|
+
recipients: Json;
|
|
10
|
+
content: string;
|
|
11
|
+
threadId: string | null;
|
|
12
|
+
metadata: Json;
|
|
13
|
+
// Joined fields
|
|
14
|
+
sendAttempts?: MessageSendAttemptRow[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type MessageSendAttemptRow = {
|
|
18
|
+
id: number;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
messageId: number;
|
|
21
|
+
providerName: string;
|
|
22
|
+
isSuccessful: boolean;
|
|
23
|
+
raw: Json;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type MessagesListResponse = {
|
|
27
|
+
items: MessageRow[];
|
|
28
|
+
total: number;
|
|
29
|
+
page: number;
|
|
30
|
+
pageSize: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type MessagesListParams = {
|
|
34
|
+
page?: number;
|
|
35
|
+
pageSize?: number;
|
|
36
|
+
search?: string;
|
|
37
|
+
channel?: string;
|
|
38
|
+
direction?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build query string for messages listing.
|
|
43
|
+
*/
|
|
44
|
+
function buildQuery(params: MessagesListParams): string {
|
|
45
|
+
const searchParams = new URLSearchParams();
|
|
46
|
+
|
|
47
|
+
if (params.page && params.page > 0) searchParams.set('page', String(params.page));
|
|
48
|
+
if (params.pageSize && params.pageSize > 0) searchParams.set('pageSize', String(params.pageSize));
|
|
49
|
+
if (params.search) searchParams.set('search', params.search);
|
|
50
|
+
if (params.channel) searchParams.set('channel', params.channel);
|
|
51
|
+
if (params.direction) searchParams.set('direction', params.direction);
|
|
52
|
+
|
|
53
|
+
const qs = searchParams.toString();
|
|
54
|
+
return qs ? `?${qs}` : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fetch messages from the admin API.
|
|
59
|
+
*/
|
|
60
|
+
export async function $fetchMessages(params: MessagesListParams = {}): Promise<MessagesListResponse> {
|
|
61
|
+
const qs = buildQuery(params);
|
|
62
|
+
const response = await fetch(`/api/messages${qs}`, {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const data = await response.json().catch(() => ({}));
|
|
68
|
+
throw new Error(data.error || 'Failed to load messages');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (await response.json()) as MessagesListResponse;
|
|
72
|
+
}
|