@promptbook/cli 0.104.0-3 → 0.104.0-4

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 (27) hide show
  1. package/apps/agents-server/src/database/migrate.ts +34 -1
  2. package/apps/agents-server/src/database/migrations/2025-12-0402-message-table.sql +42 -0
  3. package/apps/agents-server/src/message-providers/email/_common/Email.ts +73 -0
  4. package/apps/agents-server/src/message-providers/email/_common/utils/TODO.txt +1 -0
  5. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.test.ts.todo +108 -0
  6. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.ts +62 -0
  7. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.test.ts.todo +117 -0
  8. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.ts +19 -0
  9. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.test.ts.todo +119 -0
  10. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.ts +19 -0
  11. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.test.ts.todo +74 -0
  12. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.ts +14 -0
  13. package/apps/agents-server/src/message-providers/email/sendgrid/SendgridMessageProvider.ts +44 -0
  14. package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +43 -0
  15. package/apps/agents-server/src/message-providers/index.ts +13 -0
  16. package/apps/agents-server/src/message-providers/interfaces/MessageProvider.ts +11 -0
  17. package/apps/agents-server/src/utils/messages/sendMessage.ts +91 -0
  18. package/apps/agents-server/src/utils/normalization/filenameToPrompt.test.ts +36 -0
  19. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +6 -2
  20. package/esm/index.es.js +32 -2
  21. package/esm/index.es.js.map +1 -1
  22. package/esm/typings/src/llm-providers/_multiple/MultipleLlmExecutionTools.d.ts +6 -2
  23. package/esm/typings/src/llm-providers/remote/RemoteLlmExecutionTools.d.ts +1 -0
  24. package/esm/typings/src/version.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/umd/index.umd.js +32 -2
  27. package/umd/index.umd.js.map +1 -1
@@ -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
- const prefixes = prefixesEnv
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;
@@ -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`
@@ -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
+ }
@@ -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
+ */