@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,102 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { $getTableName } from '../../../database/$getTableName';
|
|
3
|
+
import { $provideSupabase } from '../../../database/$provideSupabase';
|
|
4
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
7
|
+
const MAX_PAGE_SIZE = 100;
|
|
8
|
+
|
|
9
|
+
function parsePositiveInt(value: string | null, fallback: number): number {
|
|
10
|
+
if (!value) return fallback;
|
|
11
|
+
const parsed = parseInt(value, 10);
|
|
12
|
+
if (Number.isNaN(parsed) || parsed <= 0) return fallback;
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* List messages with filters, search and pagination.
|
|
18
|
+
*/
|
|
19
|
+
export async function GET(request: NextRequest) {
|
|
20
|
+
if (!(await isUserAdmin())) {
|
|
21
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const searchParams = request.nextUrl.searchParams;
|
|
26
|
+
|
|
27
|
+
const page = parsePositiveInt(searchParams.get('page'), 1);
|
|
28
|
+
const pageSize = Math.min(MAX_PAGE_SIZE, parsePositiveInt(searchParams.get('pageSize'), DEFAULT_PAGE_SIZE));
|
|
29
|
+
const search = searchParams.get('search')?.trim() || '';
|
|
30
|
+
const channel = searchParams.get('channel');
|
|
31
|
+
const direction = searchParams.get('direction');
|
|
32
|
+
|
|
33
|
+
const supabase = $provideSupabase();
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
let query = supabase.from(await $getTableName('Message')).select('*', { count: 'exact' });
|
|
37
|
+
|
|
38
|
+
if (channel) {
|
|
39
|
+
query = query.eq('channel', channel);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (direction) {
|
|
43
|
+
query = query.eq('direction', direction);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (search) {
|
|
47
|
+
// Search in content, subject (if in metadata?), sender/recipient emails
|
|
48
|
+
// Note: sender and recipients are JSONB, so ilike might not work directly on them unless cast to text
|
|
49
|
+
// Content is TEXT.
|
|
50
|
+
const escaped = search.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
51
|
+
// Assuming simple search on content for now to avoid complexity with JSONB search in generic supabase client
|
|
52
|
+
query = query.ilike('content', `%${escaped}%`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default sort by createdAt desc
|
|
56
|
+
query = query.order('createdAt', { ascending: false });
|
|
57
|
+
|
|
58
|
+
const from = (page - 1) * pageSize;
|
|
59
|
+
const to = from + pageSize - 1;
|
|
60
|
+
|
|
61
|
+
query = query.range(from, to);
|
|
62
|
+
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
const { data: messages, error, count } = (await query) as { data: any[]; error: any; count: number };
|
|
65
|
+
|
|
66
|
+
if (error) {
|
|
67
|
+
console.error('List messages error:', error);
|
|
68
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fetch attempts for these messages
|
|
72
|
+
if (messages && messages.length > 0) {
|
|
73
|
+
const messageIds = messages.map((m) => m.id);
|
|
74
|
+
const { data: attempts, error: attemptsError } = await supabase
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
.from(await $getTableName('MessageSendAttempt'))
|
|
77
|
+
.select('*')
|
|
78
|
+
.in('messageId', messageIds);
|
|
79
|
+
|
|
80
|
+
if (attemptsError) {
|
|
81
|
+
console.error('Fetch message attempts error:', attemptsError);
|
|
82
|
+
// We don't fail the whole request, just log it.
|
|
83
|
+
} else {
|
|
84
|
+
// Attach attempts to messages
|
|
85
|
+
for (const message of messages) {
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
(message as any).sendAttempts = attempts?.filter((a: any) => a.messageId === message.id) || [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return NextResponse.json({
|
|
93
|
+
items: messages ?? [],
|
|
94
|
+
total: count ?? 0,
|
|
95
|
+
page,
|
|
96
|
+
pageSize,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('List messages error:', error);
|
|
100
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -297,6 +297,10 @@ export function Header(props: HeaderProps) {
|
|
|
297
297
|
label: 'Chat history',
|
|
298
298
|
href: '/admin/chat-history',
|
|
299
299
|
},
|
|
300
|
+
{
|
|
301
|
+
label: 'Messages & Emails',
|
|
302
|
+
href: '/admin/messages',
|
|
303
|
+
},
|
|
300
304
|
{
|
|
301
305
|
label: 'Chat feedback',
|
|
302
306
|
href: '/admin/chat-feedback',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AgentsDatabaseSchema } from '@promptbook-local/types';
|
|
2
1
|
import { $isRunningInBrowser } from '@promptbook-local/utils';
|
|
3
2
|
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import { AgentsServerDatabase } from './schema';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Internal cache for `$provideSupabaseForBrowser`
|
|
@@ -8,7 +8,7 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
|
8
8
|
* @private
|
|
9
9
|
* @singleton
|
|
10
10
|
*/
|
|
11
|
-
let supabase: SupabaseClient<
|
|
11
|
+
let supabase: SupabaseClient<AgentsServerDatabase>;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Get supabase client
|
|
@@ -27,7 +27,7 @@ export function $provideSupabaseForBrowser(): typeof supabase {
|
|
|
27
27
|
|
|
28
28
|
if (!supabase) {
|
|
29
29
|
// Create a single supabase client for interacting with your database
|
|
30
|
-
supabase = createClient<
|
|
30
|
+
supabase = createClient<AgentsServerDatabase>(
|
|
31
31
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
32
32
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
33
33
|
);
|
|
@@ -18,7 +18,7 @@ let supabase: SupabaseClient<AgentsServerDatabase>;
|
|
|
18
18
|
*
|
|
19
19
|
* @returns instance of supabase client
|
|
20
20
|
*/
|
|
21
|
-
export function $provideSupabaseForServer():
|
|
21
|
+
export function $provideSupabaseForServer(): SupabaseClient<AgentsServerDatabase> {
|
|
22
22
|
if (!$isRunningInNode()) {
|
|
23
23
|
throw new Error(
|
|
24
24
|
'Function `$provideSupabaseForServer` can not be used in browser, use `$provideSupabaseForBrowser` instead.',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AgentsDatabaseSchema } from '@promptbook-local/types';
|
|
2
1
|
import { $isRunningInWebWorker } from '@promptbook-local/utils';
|
|
3
2
|
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import { AgentsServerDatabase } from './schema';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Internal cache for `$provideSupabaseForWorker`
|
|
@@ -8,7 +8,7 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
|
8
8
|
* @private
|
|
9
9
|
* @singleton
|
|
10
10
|
*/
|
|
11
|
-
let supabase: SupabaseClient<
|
|
11
|
+
let supabase: SupabaseClient<AgentsServerDatabase>;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Get supabase client
|
|
@@ -27,7 +27,7 @@ export function $provideSupabaseForWorker(): typeof supabase {
|
|
|
27
27
|
|
|
28
28
|
if (!supabase) {
|
|
29
29
|
// Create a single supabase client for interacting with your database
|
|
30
|
-
supabase = createClient<
|
|
30
|
+
supabase = createClient<AgentsServerDatabase>(
|
|
31
31
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
32
32
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
33
33
|
);
|
|
@@ -8,13 +8,34 @@ dotenv.config();
|
|
|
8
8
|
async function migrate() {
|
|
9
9
|
console.info('🚀 Starting database migration');
|
|
10
10
|
|
|
11
|
+
// Parse CLI arguments for --only flag
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
let onlyPrefixes: string[] | null = null;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
if (args[i] === '--only' && args[i + 1]) {
|
|
17
|
+
onlyPrefixes = args[i + 1]
|
|
18
|
+
.split(',')
|
|
19
|
+
.map((p) => p.trim())
|
|
20
|
+
.filter((p) => p !== '');
|
|
21
|
+
break;
|
|
22
|
+
} else if (args[i]?.startsWith('--only=')) {
|
|
23
|
+
onlyPrefixes = args[i]
|
|
24
|
+
.substring('--only='.length)
|
|
25
|
+
.split(',')
|
|
26
|
+
.map((p) => p.trim())
|
|
27
|
+
.filter((p) => p !== '');
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
// 1. Get configuration
|
|
12
33
|
const prefixesEnv = process.env.SUPABASE_MIGRATION_PREFIXES;
|
|
13
34
|
if (!prefixesEnv) {
|
|
14
35
|
console.warn('⚠️ SUPABASE_MIGRATION_PREFIXES is not defined. Skipping migration.');
|
|
15
36
|
return;
|
|
16
37
|
}
|
|
17
|
-
|
|
38
|
+
let prefixes = prefixesEnv
|
|
18
39
|
.split(',')
|
|
19
40
|
.map((p) => p.trim())
|
|
20
41
|
.filter((p) => p !== '');
|
|
@@ -24,6 +45,18 @@ async function migrate() {
|
|
|
24
45
|
return;
|
|
25
46
|
}
|
|
26
47
|
|
|
48
|
+
// Filter prefixes if --only flag is provided
|
|
49
|
+
if (onlyPrefixes !== null) {
|
|
50
|
+
const invalidPrefixes = onlyPrefixes.filter((p) => !prefixes.includes(p));
|
|
51
|
+
if (invalidPrefixes.length > 0) {
|
|
52
|
+
console.error(`❌ Invalid prefixes specified in --only: ${invalidPrefixes.join(', ')}`);
|
|
53
|
+
console.error(` Available prefixes: ${prefixes.join(', ')}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
prefixes = onlyPrefixes;
|
|
57
|
+
console.info(`🎯 Running migrations only for: ${prefixes.join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
const connectionString = process.env.POSTGRES_URL || process.env.DATABASE_URL;
|
|
28
61
|
if (!connectionString) {
|
|
29
62
|
console.error('❌ POSTGRES_URL or DATABASE_URL is not defined.');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
-- Table: Message
|
|
3
|
+
CREATE TABLE IF NOT EXISTS "prefix_Message" (
|
|
4
|
+
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
5
|
+
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
6
|
+
"channel" TEXT NOT NULL,
|
|
7
|
+
"direction" TEXT NOT NULL,
|
|
8
|
+
"sender" JSONB NOT NULL,
|
|
9
|
+
"recipients" JSONB,
|
|
10
|
+
"content" TEXT NOT NULL,
|
|
11
|
+
"threadId" TEXT,
|
|
12
|
+
"metadata" JSONB
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
COMMENT ON TABLE "prefix_Message" IS 'A generic message structure for various communication channels';
|
|
16
|
+
COMMENT ON COLUMN "prefix_Message"."channel" IS 'The communication channel of the message (e.g. EMAIL, PROMPTBOOK_CHAT)';
|
|
17
|
+
COMMENT ON COLUMN "prefix_Message"."direction" IS 'Is the message send from the Promptbook or to the Promptbook';
|
|
18
|
+
COMMENT ON COLUMN "prefix_Message"."sender" IS 'Who sent the message';
|
|
19
|
+
COMMENT ON COLUMN "prefix_Message"."recipients" IS 'Who are the recipients of the message';
|
|
20
|
+
COMMENT ON COLUMN "prefix_Message"."content" IS 'The content of the message as markdown';
|
|
21
|
+
COMMENT ON COLUMN "prefix_Message"."threadId" IS 'The thread identifier the message belongs to';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
-- Table: MessageSendAttempt
|
|
25
|
+
CREATE TABLE IF NOT EXISTS "prefix_MessageSendAttempt" (
|
|
26
|
+
"id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
27
|
+
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
28
|
+
|
|
29
|
+
"messageId" BIGINT NOT NULL REFERENCES "prefix_Message"("id") ON DELETE CASCADE,
|
|
30
|
+
"providerName" TEXT NOT NULL,
|
|
31
|
+
"isSuccessful" BOOLEAN NOT NULL,
|
|
32
|
+
"raw" JSONB
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
COMMENT ON TABLE "prefix_MessageSendAttempt" IS 'Stores each attempt to send the message';
|
|
36
|
+
COMMENT ON COLUMN "prefix_MessageSendAttempt"."messageId" IS 'The message that was attempted to be sent';
|
|
37
|
+
COMMENT ON COLUMN "prefix_MessageSendAttempt"."providerName" IS 'The name of the provider used for sending';
|
|
38
|
+
COMMENT ON COLUMN "prefix_MessageSendAttempt"."isSuccessful" IS 'Whether the attempt was successful';
|
|
39
|
+
COMMENT ON COLUMN "prefix_MessageSendAttempt"."raw" IS 'Raw response or error from the provider';
|
|
40
|
+
|
|
41
|
+
ALTER TABLE "prefix_Message" ENABLE ROW LEVEL SECURITY;
|
|
42
|
+
ALTER TABLE "prefix_MessageSendAttempt" ENABLE ROW LEVEL SECURITY;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Source of truth: `schema.sql` *(do not edit table structure here manually)*
|
|
4
4
|
*
|
|
5
5
|
* [💽] Prompt:
|
|
6
|
-
* Re-generate supabase typescript schema from
|
|
6
|
+
* Re-generate supabase typescript schema from `./migrations/*.sql`
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Json helper (Supabase style)
|
|
@@ -120,7 +120,14 @@ export type AgentsServerDatabase = {
|
|
|
120
120
|
agentSource?: string;
|
|
121
121
|
promptbookEngineVersion?: string;
|
|
122
122
|
};
|
|
123
|
-
Relationships: [
|
|
123
|
+
Relationships: [
|
|
124
|
+
{
|
|
125
|
+
foreignKeyName: 'AgentHistory_agentName_fkey';
|
|
126
|
+
columns: ['agentName'];
|
|
127
|
+
referencedRelation: 'Agent';
|
|
128
|
+
referencedColumns: ['agentName'];
|
|
129
|
+
},
|
|
130
|
+
];
|
|
124
131
|
};
|
|
125
132
|
ChatHistory: {
|
|
126
133
|
Row: {
|
|
@@ -174,7 +181,14 @@ export type AgentsServerDatabase = {
|
|
|
174
181
|
source?: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
|
|
175
182
|
apiKey?: string | null;
|
|
176
183
|
};
|
|
177
|
-
Relationships: [
|
|
184
|
+
Relationships: [
|
|
185
|
+
{
|
|
186
|
+
foreignKeyName: 'ChatHistory_agentName_fkey';
|
|
187
|
+
columns: ['agentName'];
|
|
188
|
+
referencedRelation: 'Agent';
|
|
189
|
+
referencedColumns: ['agentName'];
|
|
190
|
+
},
|
|
191
|
+
];
|
|
178
192
|
};
|
|
179
193
|
ChatFeedback: {
|
|
180
194
|
Row: {
|
|
@@ -228,7 +242,14 @@ export type AgentsServerDatabase = {
|
|
|
228
242
|
language?: string | null;
|
|
229
243
|
platform?: string | null;
|
|
230
244
|
};
|
|
231
|
-
Relationships: [
|
|
245
|
+
Relationships: [
|
|
246
|
+
{
|
|
247
|
+
foreignKeyName: 'ChatFeedback_agentName_fkey';
|
|
248
|
+
columns: ['agentName'];
|
|
249
|
+
referencedRelation: 'Agent';
|
|
250
|
+
referencedColumns: ['agentName'];
|
|
251
|
+
},
|
|
252
|
+
];
|
|
232
253
|
};
|
|
233
254
|
User: {
|
|
234
255
|
Row: {
|
|
@@ -408,6 +429,76 @@ export type AgentsServerDatabase = {
|
|
|
408
429
|
},
|
|
409
430
|
];
|
|
410
431
|
};
|
|
432
|
+
Message: {
|
|
433
|
+
Row: {
|
|
434
|
+
id: number;
|
|
435
|
+
createdAt: string;
|
|
436
|
+
channel: string;
|
|
437
|
+
direction: string;
|
|
438
|
+
sender: Json;
|
|
439
|
+
recipients: Json | null;
|
|
440
|
+
content: string;
|
|
441
|
+
threadId: string | null;
|
|
442
|
+
metadata: Json | null;
|
|
443
|
+
};
|
|
444
|
+
Insert: {
|
|
445
|
+
id?: number;
|
|
446
|
+
createdAt?: string;
|
|
447
|
+
channel: string;
|
|
448
|
+
direction: string;
|
|
449
|
+
sender: Json;
|
|
450
|
+
recipients?: Json | null;
|
|
451
|
+
content: string;
|
|
452
|
+
threadId?: string | null;
|
|
453
|
+
metadata?: Json | null;
|
|
454
|
+
};
|
|
455
|
+
Update: {
|
|
456
|
+
id?: number;
|
|
457
|
+
createdAt?: string;
|
|
458
|
+
channel?: string;
|
|
459
|
+
direction?: string;
|
|
460
|
+
sender?: Json;
|
|
461
|
+
recipients?: Json | null;
|
|
462
|
+
content?: string;
|
|
463
|
+
threadId?: string | null;
|
|
464
|
+
metadata?: Json | null;
|
|
465
|
+
};
|
|
466
|
+
Relationships: [];
|
|
467
|
+
};
|
|
468
|
+
MessageSendAttempt: {
|
|
469
|
+
Row: {
|
|
470
|
+
id: number;
|
|
471
|
+
createdAt: string;
|
|
472
|
+
messageId: number;
|
|
473
|
+
providerName: string;
|
|
474
|
+
isSuccessful: boolean;
|
|
475
|
+
raw: Json | null;
|
|
476
|
+
};
|
|
477
|
+
Insert: {
|
|
478
|
+
id?: number;
|
|
479
|
+
createdAt?: string;
|
|
480
|
+
messageId: number;
|
|
481
|
+
providerName: string;
|
|
482
|
+
isSuccessful: boolean;
|
|
483
|
+
raw?: Json | null;
|
|
484
|
+
};
|
|
485
|
+
Update: {
|
|
486
|
+
id?: number;
|
|
487
|
+
createdAt?: string;
|
|
488
|
+
messageId?: number;
|
|
489
|
+
providerName?: string;
|
|
490
|
+
isSuccessful?: boolean;
|
|
491
|
+
raw?: Json | null;
|
|
492
|
+
};
|
|
493
|
+
Relationships: [
|
|
494
|
+
{
|
|
495
|
+
foreignKeyName: 'MessageSendAttempt_messageId_fkey';
|
|
496
|
+
columns: ['messageId'];
|
|
497
|
+
referencedRelation: 'Message';
|
|
498
|
+
referencedColumns: ['id'];
|
|
499
|
+
},
|
|
500
|
+
];
|
|
501
|
+
};
|
|
411
502
|
};
|
|
412
503
|
Views: Record<string, never>;
|
|
413
504
|
Functions: Record<string, never>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Message, string_email, string_person_fullname } from '@promptbook-local/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single email which was received by the application
|
|
5
|
+
*/
|
|
6
|
+
export type InboundEmail = Email & { direction: 'INBOUND' };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Single email which was sended from the application
|
|
10
|
+
*/
|
|
11
|
+
export type OutboundEmail = Email & { direction: 'OUTBOUND' };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Single email
|
|
15
|
+
*/
|
|
16
|
+
type Email = Message<string_email> & {
|
|
17
|
+
/**
|
|
18
|
+
* Channel of the message
|
|
19
|
+
*
|
|
20
|
+
* @default 'EMAIL'
|
|
21
|
+
*/
|
|
22
|
+
readonly channel?: 'EMAIL';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Carbon copy email addresses
|
|
26
|
+
*
|
|
27
|
+
* Note: Not working with BCC (Blind Carbon Copy) because we want to have all emails in the same thread
|
|
28
|
+
* and for hidden emails we can just call $sendEmail multiple times
|
|
29
|
+
*/
|
|
30
|
+
readonly cc: Array<EmailAddress>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Email subject
|
|
34
|
+
*/
|
|
35
|
+
readonly subject: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Email attachments
|
|
39
|
+
*/
|
|
40
|
+
readonly attachments: Array<File>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type EmailAddress = {
|
|
44
|
+
/**
|
|
45
|
+
* Everything outside of `<>` in email address
|
|
46
|
+
*
|
|
47
|
+
* @example "Pavol Hejný <pavol@hejny.cz>" -> "Pavol Hejný"
|
|
48
|
+
* @example "\"Pavol Hejný\" <pavol@hejny.cz>" -> "Pavol Hejný"
|
|
49
|
+
*/
|
|
50
|
+
fullName: string_person_fullname | string | null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Everything after `+` in email address
|
|
54
|
+
*
|
|
55
|
+
* @example "pavol+spam@webgpt.cz" -> ["spam"]
|
|
56
|
+
* @example "pavol+spam+debug@webgpt.cz" -> ["spam","debug"]
|
|
57
|
+
*/
|
|
58
|
+
plus: Array<string>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pure email address
|
|
62
|
+
*
|
|
63
|
+
* @example "pavol@webgpt.cz"
|
|
64
|
+
*/
|
|
65
|
+
baseEmail: string_email;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Full email address without the name but with +
|
|
69
|
+
*
|
|
70
|
+
* @example "pavol+test@webgpt.cz"
|
|
71
|
+
*/
|
|
72
|
+
fullEmail: string_email;
|
|
73
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TODO: [🧠][🏰] Maybe move all of theese functions into `@promptbook/utils`
|
package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.test.ts.todo
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { parseEmailAddress } from './parseEmailAddress';
|
|
3
|
+
|
|
4
|
+
describe('how parseEmailAddress works', () => {
|
|
5
|
+
it('should work with simple email', () => {
|
|
6
|
+
expect(parseEmailAddress('pavol@webgpt.cz')).toEqual({
|
|
7
|
+
fullName: null,
|
|
8
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
9
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
10
|
+
plus: [],
|
|
11
|
+
});
|
|
12
|
+
expect(parseEmailAddress('jirka@webgpt.cz')).toEqual({
|
|
13
|
+
fullName: null,
|
|
14
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
15
|
+
fullEmail: 'jirka@webgpt.cz',
|
|
16
|
+
plus: [],
|
|
17
|
+
});
|
|
18
|
+
expect(parseEmailAddress('tomas@webgpt.cz')).toEqual({
|
|
19
|
+
fullName: null,
|
|
20
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
21
|
+
fullEmail: 'tomas@webgpt.cz',
|
|
22
|
+
plus: [],
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should work with fullname', () => {
|
|
27
|
+
expect(parseEmailAddress('Pavol Hejný <pavol@webgpt.cz>')).toEqual({
|
|
28
|
+
fullName: 'Pavol Hejný',
|
|
29
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
30
|
+
fullEmail: 'pavol@webgpt.cz',
|
|
31
|
+
plus: [],
|
|
32
|
+
});
|
|
33
|
+
expect(parseEmailAddress('Jirka <jirka@webgpt.cz>')).toEqual({
|
|
34
|
+
fullName: 'Jirka',
|
|
35
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
36
|
+
fullEmail: 'jirka@webgpt.cz',
|
|
37
|
+
plus: [],
|
|
38
|
+
});
|
|
39
|
+
expect(parseEmailAddress('"Tomáš Studeník" <tomas@webgpt.cz>')).toEqual({
|
|
40
|
+
fullName: 'Tomáš Studeník',
|
|
41
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
42
|
+
fullEmail: 'tomas@webgpt.cz',
|
|
43
|
+
plus: [],
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should work with plus', () => {
|
|
48
|
+
expect(parseEmailAddress('pavol+test@webgpt.cz')).toEqual({
|
|
49
|
+
fullName: null,
|
|
50
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
51
|
+
fullEmail: 'pavol+test@webgpt.cz',
|
|
52
|
+
plus: ['test'],
|
|
53
|
+
});
|
|
54
|
+
expect(parseEmailAddress('jirka+test@webgpt.cz')).toEqual({
|
|
55
|
+
fullName: null,
|
|
56
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
57
|
+
fullEmail: 'jirka+test@webgpt.cz',
|
|
58
|
+
plus: ['test'],
|
|
59
|
+
});
|
|
60
|
+
expect(parseEmailAddress('tomas+test+ainautes@webgpt.cz')).toEqual({
|
|
61
|
+
fullName: null,
|
|
62
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
63
|
+
fullEmail: 'tomas+test+ainautes@webgpt.cz',
|
|
64
|
+
plus: ['test', 'ainautes'],
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should work with both fullname and plus', () => {
|
|
69
|
+
expect(parseEmailAddress('Pavol Hejný <pavol+foo@webgpt.cz>')).toEqual({
|
|
70
|
+
fullName: 'Pavol Hejný',
|
|
71
|
+
baseEmail: 'pavol@webgpt.cz',
|
|
72
|
+
fullEmail: 'pavol+foo@webgpt.cz',
|
|
73
|
+
plus: ['foo'],
|
|
74
|
+
});
|
|
75
|
+
expect(parseEmailAddress('Jirka <jirka+test@webgpt.cz>')).toEqual({
|
|
76
|
+
fullName: 'Jirka',
|
|
77
|
+
baseEmail: 'jirka@webgpt.cz',
|
|
78
|
+
fullEmail: 'jirka+test@webgpt.cz',
|
|
79
|
+
plus: ['test'],
|
|
80
|
+
});
|
|
81
|
+
expect(parseEmailAddress('"Tomáš Studeník" <tomas+test+ainautes@webgpt.cz>')).toEqual({
|
|
82
|
+
fullName: 'Tomáš Studeník',
|
|
83
|
+
baseEmail: 'tomas@webgpt.cz',
|
|
84
|
+
fullEmail: 'tomas+test+ainautes@webgpt.cz',
|
|
85
|
+
plus: ['test', 'ainautes'],
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('throws on multiple adresses', () => {
|
|
90
|
+
expect(() => parseEmailAddress('Pavol <pavol@webgpt.cz>, Jirka <jirka@webgpt.cz>')).toThrowError(
|
|
91
|
+
/Seems like you are trying to parse multiple email addresses/,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('throws on invalid email adresses', () => {
|
|
96
|
+
expect(() => parseEmailAddress('')).toThrowError(/Invalid email address/);
|
|
97
|
+
expect(() => parseEmailAddress('Pavol Hejný')).toThrowError(/Invalid email address/);
|
|
98
|
+
expect(() => parseEmailAddress('Pavol Hejný <>')).toThrowError(/Invalid email address/);
|
|
99
|
+
expect(() => parseEmailAddress('Pavol Hejný <@webgpt.cz>')).toThrowError(/Invalid email address/);
|
|
100
|
+
expect(() => parseEmailAddress('Pavol Hejný <webgpt.cz>')).toThrowError(/Invalid email address/);
|
|
101
|
+
expect(() => parseEmailAddress('Pavol Hejný <pavol@>')).toThrowError(/Invalid email address/);
|
|
102
|
+
expect(() => parseEmailAddress('Pavol Hejný <a@b>')).toThrowError(/Invalid email address/);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* TODO: [🐫] This test fails because of aliased imports `import type { string_emails } from '@promptbook-local/types';`, fix it
|
|
108
|
+
*/
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { string_email } from '@promptbook-local/types';
|
|
2
|
+
import { isValidEmail, spaceTrim } from '@promptbook-local/utils';
|
|
3
|
+
import { EmailAddress } from '../Email';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses the email address into its components
|
|
7
|
+
*/
|
|
8
|
+
export function parseEmailAddress(value: string_email): EmailAddress {
|
|
9
|
+
if (value.includes(',')) {
|
|
10
|
+
throw new Error('Seems like you are trying to parse multiple email addresses, use parseEmailAddresses instead');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let fullName = value.match(/^(?:"?([^"]+)"?|[^<]+)\s*</)?.[1] ?? null;
|
|
14
|
+
|
|
15
|
+
if (fullName !== null) {
|
|
16
|
+
fullName = fullName.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fullEmail = value.match(/<([^>]+)>/)?.[1] ?? value;
|
|
20
|
+
const plus: Array<string> = [];
|
|
21
|
+
|
|
22
|
+
if (!isValidEmail(fullEmail)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
spaceTrim(
|
|
25
|
+
(block) => `
|
|
26
|
+
Invalid email address "${fullEmail}"
|
|
27
|
+
|
|
28
|
+
Parsed:
|
|
29
|
+
${block(JSON.stringify({ fullName, fullEmail, plus }, null, 4))}
|
|
30
|
+
|
|
31
|
+
`,
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (fullEmail.includes('+')) {
|
|
37
|
+
const [user, domain] = fullEmail.split('@');
|
|
38
|
+
|
|
39
|
+
if (!user || !domain) {
|
|
40
|
+
throw new Error('Can not parse email address');
|
|
41
|
+
// <- TODO: ShouldNeverHappenError
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const userParts = user.split('+');
|
|
45
|
+
userParts.shift();
|
|
46
|
+
|
|
47
|
+
plus.push(...userParts);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let baseEmail = fullEmail;
|
|
51
|
+
|
|
52
|
+
for (const plusItem of plus) {
|
|
53
|
+
baseEmail = baseEmail.replace(`+${plusItem}`, '');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
fullName,
|
|
58
|
+
baseEmail,
|
|
59
|
+
fullEmail,
|
|
60
|
+
plus,
|
|
61
|
+
};
|
|
62
|
+
}
|