@divyacon/piece-gmail 0.0.0-stage → 0.1.1

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 (188) hide show
  1. package/LICENSE.MIT-AP +24 -0
  2. package/README.md +8 -2
  3. package/dist/index.d.ts +6 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +111 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/actions/ai-get-message-action.d.ts +7 -0
  8. package/dist/lib/actions/ai-get-message-action.d.ts.map +1 -0
  9. package/dist/lib/actions/ai-get-message-action.js +24 -0
  10. package/dist/lib/actions/ai-get-message-action.js.map +1 -0
  11. package/dist/lib/actions/ai-reply-to-thread-action.d.ts +16 -0
  12. package/dist/lib/actions/ai-reply-to-thread-action.d.ts.map +1 -0
  13. package/dist/lib/actions/ai-reply-to-thread-action.js +24 -0
  14. package/dist/lib/actions/ai-reply-to-thread-action.js.map +1 -0
  15. package/dist/lib/actions/ai-search-email-action.d.ts +21 -0
  16. package/dist/lib/actions/ai-search-email-action.d.ts.map +1 -0
  17. package/dist/lib/actions/ai-search-email-action.js +24 -0
  18. package/dist/lib/actions/ai-search-email-action.js.map +1 -0
  19. package/dist/lib/actions/ai-send-email-action.d.ts +18 -0
  20. package/dist/lib/actions/ai-send-email-action.d.ts.map +1 -0
  21. package/dist/lib/actions/ai-send-email-action.js +24 -0
  22. package/dist/lib/actions/ai-send-email-action.js.map +1 -0
  23. package/dist/lib/actions/create-draft-action.d.ts +16 -0
  24. package/dist/lib/actions/create-draft-action.d.ts.map +1 -0
  25. package/dist/lib/actions/create-draft-action.js +137 -0
  26. package/dist/lib/actions/create-draft-action.js.map +1 -0
  27. package/dist/lib/actions/create-draft-reply-action.d.ts +17 -0
  28. package/dist/lib/actions/create-draft-reply-action.d.ts.map +1 -0
  29. package/dist/lib/actions/create-draft-reply-action.js +269 -0
  30. package/dist/lib/actions/create-draft-reply-action.js.map +1 -0
  31. package/dist/lib/actions/delete-draft-action.d.ts +7 -0
  32. package/dist/lib/actions/delete-draft-action.d.ts.map +1 -0
  33. package/dist/lib/actions/delete-draft-action.js +62 -0
  34. package/dist/lib/actions/delete-draft-action.js.map +1 -0
  35. package/dist/lib/actions/forward-message-action.d.ts +12 -0
  36. package/dist/lib/actions/forward-message-action.d.ts.map +1 -0
  37. package/dist/lib/actions/forward-message-action.js +139 -0
  38. package/dist/lib/actions/forward-message-action.js.map +1 -0
  39. package/dist/lib/actions/get-attachment-action.d.ts +9 -0
  40. package/dist/lib/actions/get-attachment-action.d.ts.map +1 -0
  41. package/dist/lib/actions/get-attachment-action.js +80 -0
  42. package/dist/lib/actions/get-attachment-action.js.map +1 -0
  43. package/dist/lib/actions/get-draft-action.d.ts +7 -0
  44. package/dist/lib/actions/get-draft-action.d.ts.map +1 -0
  45. package/dist/lib/actions/get-draft-action.js +56 -0
  46. package/dist/lib/actions/get-draft-action.js.map +1 -0
  47. package/dist/lib/actions/get-label-action.d.ts +7 -0
  48. package/dist/lib/actions/get-label-action.d.ts.map +1 -0
  49. package/dist/lib/actions/get-label-action.js +55 -0
  50. package/dist/lib/actions/get-label-action.js.map +1 -0
  51. package/dist/lib/actions/get-mail-action.d.ts +7 -0
  52. package/dist/lib/actions/get-mail-action.d.ts.map +1 -0
  53. package/dist/lib/actions/get-mail-action.js +44 -0
  54. package/dist/lib/actions/get-mail-action.js.map +1 -0
  55. package/dist/lib/actions/get-profile-action.d.ts +5 -0
  56. package/dist/lib/actions/get-profile-action.d.ts.map +1 -0
  57. package/dist/lib/actions/get-profile-action.js +43 -0
  58. package/dist/lib/actions/get-profile-action.js.map +1 -0
  59. package/dist/lib/actions/get-thread-action.d.ts +9 -0
  60. package/dist/lib/actions/get-thread-action.d.ts.map +1 -0
  61. package/dist/lib/actions/get-thread-action.js +53 -0
  62. package/dist/lib/actions/get-thread-action.js.map +1 -0
  63. package/dist/lib/actions/list-drafts-action.d.ts +8 -0
  64. package/dist/lib/actions/list-drafts-action.d.ts.map +1 -0
  65. package/dist/lib/actions/list-drafts-action.js +62 -0
  66. package/dist/lib/actions/list-drafts-action.js.map +1 -0
  67. package/dist/lib/actions/list-history-action.d.ts +9 -0
  68. package/dist/lib/actions/list-history-action.d.ts.map +1 -0
  69. package/dist/lib/actions/list-history-action.js +74 -0
  70. package/dist/lib/actions/list-history-action.js.map +1 -0
  71. package/dist/lib/actions/list-labels-action.d.ts +5 -0
  72. package/dist/lib/actions/list-labels-action.d.ts.map +1 -0
  73. package/dist/lib/actions/list-labels-action.js +47 -0
  74. package/dist/lib/actions/list-labels-action.js.map +1 -0
  75. package/dist/lib/actions/list-threads-action.d.ts +10 -0
  76. package/dist/lib/actions/list-threads-action.d.ts.map +1 -0
  77. package/dist/lib/actions/list-threads-action.js +74 -0
  78. package/dist/lib/actions/list-threads-action.js.map +1 -0
  79. package/dist/lib/actions/reply-to-email-action.d.ts +16 -0
  80. package/dist/lib/actions/reply-to-email-action.d.ts.map +1 -0
  81. package/dist/lib/actions/reply-to-email-action.js +202 -0
  82. package/dist/lib/actions/reply-to-email-action.js.map +1 -0
  83. package/dist/lib/actions/request-approval-in-email.d.ts +15 -0
  84. package/dist/lib/actions/request-approval-in-email.d.ts.map +1 -0
  85. package/dist/lib/actions/request-approval-in-email.js +174 -0
  86. package/dist/lib/actions/request-approval-in-email.js.map +1 -0
  87. package/dist/lib/actions/search-email-action.d.ts +22 -0
  88. package/dist/lib/actions/search-email-action.d.ts.map +1 -0
  89. package/dist/lib/actions/search-email-action.js +179 -0
  90. package/dist/lib/actions/search-email-action.js.map +1 -0
  91. package/dist/lib/actions/send-draft-action.d.ts +7 -0
  92. package/dist/lib/actions/send-draft-action.d.ts.map +1 -0
  93. package/dist/lib/actions/send-draft-action.js +57 -0
  94. package/dist/lib/actions/send-draft-action.js.map +1 -0
  95. package/dist/lib/actions/send-email-action.d.ts +18 -0
  96. package/dist/lib/actions/send-email-action.d.ts.map +1 -0
  97. package/dist/lib/actions/send-email-action.js +196 -0
  98. package/dist/lib/actions/send-email-action.js.map +1 -0
  99. package/dist/lib/actions/stop-watch-action.d.ts +5 -0
  100. package/dist/lib/actions/stop-watch-action.d.ts.map +1 -0
  101. package/dist/lib/actions/stop-watch-action.js +41 -0
  102. package/dist/lib/actions/stop-watch-action.js.map +1 -0
  103. package/dist/lib/actions/update-draft-action.d.ts +17 -0
  104. package/dist/lib/actions/update-draft-action.d.ts.map +1 -0
  105. package/dist/lib/actions/update-draft-action.js +193 -0
  106. package/dist/lib/actions/update-draft-action.js.map +1 -0
  107. package/dist/lib/auth.d.ts +12 -0
  108. package/dist/lib/auth.d.ts.map +1 -0
  109. package/dist/lib/auth.js +108 -0
  110. package/dist/lib/auth.js.map +1 -0
  111. package/dist/lib/common/data.d.ts +132 -0
  112. package/dist/lib/common/data.d.ts.map +1 -0
  113. package/dist/lib/common/data.js +209 -0
  114. package/dist/lib/common/data.js.map +1 -0
  115. package/dist/lib/common/mime.d.ts +29 -0
  116. package/dist/lib/common/mime.d.ts.map +1 -0
  117. package/dist/lib/common/mime.js +59 -0
  118. package/dist/lib/common/mime.js.map +1 -0
  119. package/dist/lib/common/models.d.ts +86 -0
  120. package/dist/lib/common/models.d.ts.map +1 -0
  121. package/dist/lib/common/models.js +26 -0
  122. package/dist/lib/common/models.js.map +1 -0
  123. package/dist/lib/common/props.d.ts +28 -0
  124. package/dist/lib/common/props.d.ts.map +1 -0
  125. package/dist/lib/common/props.js +231 -0
  126. package/dist/lib/common/props.js.map +1 -0
  127. package/dist/lib/output-schemas.d.ts +29 -0
  128. package/dist/lib/output-schemas.d.ts.map +1 -0
  129. package/dist/lib/output-schemas.js +1029 -0
  130. package/dist/lib/output-schemas.js.map +1 -0
  131. package/dist/lib/triggers/new-attachment.d.ts +144 -0
  132. package/dist/lib/triggers/new-attachment.d.ts.map +1 -0
  133. package/dist/lib/triggers/new-attachment.js +150 -0
  134. package/dist/lib/triggers/new-attachment.js.map +1 -0
  135. package/dist/lib/triggers/new-conversation.d.ts +59 -0
  136. package/dist/lib/triggers/new-conversation.d.ts.map +1 -0
  137. package/dist/lib/triggers/new-conversation.js +308 -0
  138. package/dist/lib/triggers/new-conversation.js.map +1 -0
  139. package/dist/lib/triggers/new-email.d.ts +52 -0
  140. package/dist/lib/triggers/new-email.d.ts.map +1 -0
  141. package/dist/lib/triggers/new-email.js +135 -0
  142. package/dist/lib/triggers/new-email.js.map +1 -0
  143. package/dist/lib/triggers/new-label.d.ts +15 -0
  144. package/dist/lib/triggers/new-label.d.ts.map +1 -0
  145. package/dist/lib/triggers/new-label.js +82 -0
  146. package/dist/lib/triggers/new-label.js.map +1 -0
  147. package/dist/lib/triggers/new-labeled-email.d.ts +35 -0
  148. package/dist/lib/triggers/new-labeled-email.d.ts.map +1 -0
  149. package/dist/lib/triggers/new-labeled-email.js +147 -0
  150. package/dist/lib/triggers/new-labeled-email.js.map +1 -0
  151. package/package.json +54 -4
  152. package/src/index.ts +107 -0
  153. package/src/lib/actions/ai-get-message-action.ts +20 -0
  154. package/src/lib/actions/ai-reply-to-thread-action.ts +20 -0
  155. package/src/lib/actions/ai-search-email-action.ts +21 -0
  156. package/src/lib/actions/ai-send-email-action.ts +20 -0
  157. package/src/lib/actions/create-draft-action.ts +148 -0
  158. package/src/lib/actions/create-draft-reply-action.ts +314 -0
  159. package/src/lib/actions/delete-draft-action.ts +57 -0
  160. package/src/lib/actions/forward-message-action.ts +159 -0
  161. package/src/lib/actions/get-attachment-action.ts +83 -0
  162. package/src/lib/actions/get-draft-action.ts +53 -0
  163. package/src/lib/actions/get-label-action.ts +53 -0
  164. package/src/lib/actions/get-mail-action.ts +52 -0
  165. package/src/lib/actions/get-profile-action.ts +39 -0
  166. package/src/lib/actions/get-thread-action.ts +47 -0
  167. package/src/lib/actions/list-drafts-action.ts +72 -0
  168. package/src/lib/actions/list-history-action.ts +83 -0
  169. package/src/lib/actions/list-labels-action.ts +43 -0
  170. package/src/lib/actions/list-threads-action.ts +89 -0
  171. package/src/lib/actions/reply-to-email-action.ts +228 -0
  172. package/src/lib/actions/request-approval-in-email.ts +197 -0
  173. package/src/lib/actions/search-email-action.ts +219 -0
  174. package/src/lib/actions/send-draft-action.ts +54 -0
  175. package/src/lib/actions/send-email-action.ts +213 -0
  176. package/src/lib/actions/stop-watch-action.ts +37 -0
  177. package/src/lib/actions/update-draft-action.ts +211 -0
  178. package/src/lib/auth.ts +116 -0
  179. package/src/lib/common/data.ts +268 -0
  180. package/src/lib/common/mime.ts +82 -0
  181. package/src/lib/common/models.ts +91 -0
  182. package/src/lib/common/props.ts +262 -0
  183. package/src/lib/output-schemas.ts +1061 -0
  184. package/src/lib/triggers/new-attachment.ts +203 -0
  185. package/src/lib/triggers/new-conversation.ts +413 -0
  186. package/src/lib/triggers/new-email.ts +173 -0
  187. package/src/lib/triggers/new-label.ts +83 -0
  188. package/src/lib/triggers/new-labeled-email.ts +197 -0
@@ -0,0 +1,203 @@
1
+ import {
2
+ createTrigger,
3
+ TriggerStrategy,
4
+ Property,
5
+ FilesService,
6
+ } from '@agnocon/pieces-framework';
7
+ import { GmailProps } from '../common/props';
8
+ import { gmailAuth, createGoogleClient, GmailAuthValue } from '../auth';
9
+ import { gmail as googleGmail } from '@googleapis/gmail';
10
+ import {
11
+ parseStream,
12
+ convertAttachment,
13
+ getFirstFiveOrAll,
14
+ } from '../common/data';
15
+ import { GmailLabel } from '../common/models';
16
+ import { newAttachmentTriggerOutputSchema } from '../output-schemas';
17
+
18
+ type Props = {
19
+ from?: string;
20
+ to?: string;
21
+ subject?: string;
22
+ label?: GmailLabel;
23
+ category?: string;
24
+ filenameExtension?: string;
25
+ };
26
+
27
+ export const gmailNewAttachmentTrigger = createTrigger({
28
+ auth: gmailAuth,
29
+ name: 'new_attachment',
30
+ displayName: 'New Attachment',
31
+ description: 'Triggers when an email with an attachment arrives.',
32
+ aiMetadata: {
33
+ description:
34
+ 'Fires when a new email carrying one or more attachments arrives, optionally narrowed by sender, recipient, subject, label, category, or file extension. Each event represents a single attachment (a multi-attachment email emits one event per attachment) along with its source message.',
35
+ },
36
+ props: {
37
+ from: {
38
+ ...GmailProps.from,
39
+ description: 'Filter by sender email.',
40
+ displayName: 'From',
41
+ required: false,
42
+ },
43
+ to: {
44
+ ...GmailProps.to,
45
+ description: 'Filter by recipient email.',
46
+ displayName: 'To',
47
+ required: false,
48
+ },
49
+ subject: Property.ShortText({
50
+ displayName: 'Subject Contains',
51
+ description:
52
+ 'Only trigger for emails containing this text in the subject.',
53
+ required: false,
54
+ }),
55
+ label: GmailProps.label({
56
+ description: 'Filter by Gmail label.',
57
+ displayName: 'Label',
58
+ required: false,
59
+ }),
60
+ category: {
61
+ ...GmailProps.category,
62
+ description: 'Filter by Gmail category.',
63
+ displayName: 'Category',
64
+ required: false,
65
+ },
66
+ filenameExtension: Property.ShortText({
67
+ displayName: 'File Extension',
68
+ description:
69
+ 'Only trigger for attachments with this file extension (e.g., pdf, jpg, docx).',
70
+ required: false,
71
+ }),
72
+ },
73
+ outputSchema: newAttachmentTriggerOutputSchema,
74
+ sampleData: {},
75
+ type: TriggerStrategy.POLLING,
76
+ async onEnable(context) {
77
+ await context.store.put('lastPoll', Date.now());
78
+ },
79
+ async onDisable(context) {
80
+ return;
81
+ },
82
+ async run(context) {
83
+ const lastFetchEpochMS = (await context.store.get<number>('lastPoll')) ?? 0;
84
+
85
+ const items = await pollRecentMessages({
86
+ auth: context.auth,
87
+ props: context.propsValue,
88
+ files: context.files,
89
+ lastFetchEpochMS: lastFetchEpochMS,
90
+ });
91
+
92
+ const newLastEpochMilliSeconds = items.reduce(
93
+ (acc, item) => Math.max(acc, item.epochMilliSeconds),
94
+ lastFetchEpochMS
95
+ );
96
+ await context.store.put('lastPoll', newLastEpochMilliSeconds);
97
+ return items
98
+ .filter((f) => f.epochMilliSeconds > lastFetchEpochMS)
99
+ .map((item) => item.data);
100
+ },
101
+ async test(context) {
102
+ const lastFetchEpochMS = (await context.store.get<number>('lastPoll')) ?? 0;
103
+
104
+ const items = await pollRecentMessages({
105
+ auth: context.auth,
106
+ props: context.propsValue,
107
+ files: context.files,
108
+ lastFetchEpochMS: lastFetchEpochMS,
109
+ });
110
+
111
+ return getFirstFiveOrAll(items.map((item) => item.data));
112
+ },
113
+ });
114
+
115
+ async function pollRecentMessages({
116
+ auth,
117
+ props,
118
+ files,
119
+ lastFetchEpochMS,
120
+ }: {
121
+ auth: GmailAuthValue;
122
+ props: Props;
123
+ files: FilesService;
124
+ lastFetchEpochMS: number;
125
+ }): Promise<
126
+ {
127
+ epochMilliSeconds: number;
128
+ data: unknown;
129
+ }[]
130
+ > {
131
+ const authClient = await createGoogleClient(auth);
132
+
133
+ const gmail = googleGmail({ version: 'v1', auth: authClient });
134
+
135
+ // construct query
136
+ const query = ['has:attachment'];
137
+ const maxResults = lastFetchEpochMS === 0 ? 5 : 20;
138
+ const afterUnixSeconds = Math.floor(lastFetchEpochMS / 1000);
139
+
140
+ if (props.from) query.push(`from:(${props.from})`);
141
+ if (props.to) query.push(`to:(${props.to})`);
142
+ if (props.subject) query.push(`subject:(${props.subject})`);
143
+ if (props.label) query.push(`label:${props.label.name}`);
144
+ if (props.category) query.push(`category:${props.category}`);
145
+ if (props.filenameExtension)
146
+ query.push(`filename:${props.filenameExtension}`);
147
+ if (afterUnixSeconds != null && afterUnixSeconds > 0)
148
+ query.push(`after:${afterUnixSeconds}`);
149
+
150
+ // List Messages
151
+ const messagesResponse = await gmail.users.messages.list({
152
+ userId: 'me',
153
+ q: query.join(' '),
154
+ maxResults,
155
+ });
156
+
157
+ // Reverse to process oldest-first so partial progress doesn't skip messages
158
+ const messages = (messagesResponse.data.messages || []).slice().reverse();
159
+
160
+ const pollingResponse = [];
161
+ for (const message of messages) {
162
+ try {
163
+ const rawMailResponse = await gmail.users.messages.get({
164
+ userId: 'me',
165
+ id: message.id!,
166
+ format: 'raw',
167
+ });
168
+
169
+ const parsedMailResponse = await parseStream(
170
+ Buffer.from(rawMailResponse.data.raw as string, 'base64').toString(
171
+ 'utf-8'
172
+ )
173
+ );
174
+
175
+ const { attachments, ...restOfParsedMailResponse } = parsedMailResponse;
176
+ const parsedAttachments = await convertAttachment(attachments, files);
177
+
178
+ for (const attachment of parsedAttachments) {
179
+ pollingResponse.push({
180
+ epochMilliSeconds: Number(rawMailResponse.data.internalDate),
181
+ data: {
182
+ attachment,
183
+ message: {
184
+ id: message.id,
185
+ ...restOfParsedMailResponse,
186
+ },
187
+ },
188
+ });
189
+ }
190
+ } catch (error: any) {
191
+ const isRateLimit =
192
+ error.status === 429 ||
193
+ (error.status === 403 &&
194
+ /quota|rate.?limit/i.test(error.message ?? ''));
195
+ if (isRateLimit) {
196
+ break;
197
+ }
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ return pollingResponse;
203
+ }
@@ -0,0 +1,413 @@
1
+ import {
2
+ createTrigger,
3
+ TriggerStrategy,
4
+ FilesService,
5
+ Property,
6
+ } from '@agnocon/pieces-framework';
7
+ import { GmailProps } from '../common/props';
8
+ import { gmailAuth, createGoogleClient } from '../auth';
9
+ import { gmail as googleGmail } from '@googleapis/gmail';
10
+ import { parseStream, convertAttachment } from '../common/data';
11
+
12
+ async function enrichNewConversation({
13
+ gmail,
14
+ threadId,
15
+ files,
16
+ conversationInfo,
17
+ }: {
18
+ gmail: any;
19
+ threadId: string;
20
+ files: FilesService;
21
+ conversationInfo: {
22
+ createdAt: number;
23
+ historyId: string;
24
+ };
25
+ }) {
26
+ const threadResponse = await gmail.users.threads.get({
27
+ userId: 'me',
28
+ id: threadId,
29
+ format: 'full',
30
+ });
31
+
32
+ const thread = threadResponse.data;
33
+ const messages = thread.messages || [];
34
+
35
+ const firstMessage = messages[0];
36
+ if (!firstMessage?.id) {
37
+ throw new Error('No messages found in thread');
38
+ }
39
+
40
+ const rawMessageResponse = await gmail.users.messages.get({
41
+ userId: 'me',
42
+ id: firstMessage.id,
43
+ format: 'raw',
44
+ });
45
+
46
+ const parsedFirstMessage = await parseStream(
47
+ Buffer.from(rawMessageResponse.data.raw as string, 'base64').toString(
48
+ 'utf-8'
49
+ )
50
+ );
51
+
52
+ const headers = firstMessage.payload?.headers || [];
53
+ const headerMap = headers.reduce(
54
+ (acc: { [key: string]: string }, header: any) => {
55
+ if (header.name && header.value) {
56
+ acc[header.name.toLowerCase()] = header.value;
57
+ }
58
+ return acc;
59
+ },
60
+ {}
61
+ );
62
+
63
+ return {
64
+ conversation: {
65
+ threadId: threadId,
66
+ messageCount: messages.length,
67
+ snippet: thread.snippet || '',
68
+ historyId: thread.historyId,
69
+ participants: extractParticipants(messages),
70
+ subject: headerMap['subject'] || '',
71
+ starter: {
72
+ from: headerMap['from'] || '',
73
+ to: headerMap['to'] || '',
74
+ cc: headerMap['cc'] || '',
75
+ bcc: headerMap['bcc'] || '',
76
+ date: headerMap['date'] || '',
77
+ messageId: firstMessage.id,
78
+ },
79
+ },
80
+ firstMessage: {
81
+ ...parsedFirstMessage,
82
+ messageId: firstMessage.id,
83
+ attachments: await convertAttachment(
84
+ parsedFirstMessage.attachments,
85
+ files
86
+ ),
87
+ },
88
+ conversationInfo: {
89
+ ...conversationInfo,
90
+ triggeredAt: Date.now(),
91
+ },
92
+ };
93
+ }
94
+
95
+ function extractParticipants(messages: any[]): {
96
+ from: Set<string>;
97
+ to: Set<string>;
98
+ cc: Set<string>;
99
+ } {
100
+ const participants = {
101
+ from: new Set<string>(),
102
+ to: new Set<string>(),
103
+ cc: new Set<string>(),
104
+ };
105
+
106
+ messages.forEach((message) => {
107
+ const headers = message.payload?.headers || [];
108
+ headers.forEach((header: any) => {
109
+ if (header.name && header.value) {
110
+ const name = header.name.toLowerCase();
111
+ const value = header.value;
112
+
113
+ if (name === 'from') {
114
+ participants.from.add(value);
115
+ } else if (name === 'to') {
116
+ value
117
+ .split(',')
118
+ .forEach((email: string) => participants.to.add(email.trim()));
119
+ } else if (name === 'cc') {
120
+ value
121
+ .split(',')
122
+ .forEach((email: string) => participants.cc.add(email.trim()));
123
+ }
124
+ }
125
+ });
126
+ });
127
+
128
+ return {
129
+ from: participants.from,
130
+ to: participants.to,
131
+ cc: participants.cc,
132
+ };
133
+ }
134
+
135
+ export const gmailNewConversationTrigger = createTrigger({
136
+ auth: gmailAuth,
137
+ name: 'new_conversation',
138
+ displayName: 'New Conversation',
139
+ description: 'Triggers when a new email conversation (thread) begins',
140
+ props: {
141
+ from: {
142
+ ...GmailProps.from,
143
+ description: 'Filter by sender email (optional)',
144
+ displayName: 'From',
145
+ required: false,
146
+ },
147
+ subject: Property.ShortText({
148
+ displayName: 'Subject Contains',
149
+ description:
150
+ 'Only trigger for conversations containing this text in the subject (optional)',
151
+ required: false,
152
+ }),
153
+ maxAgeHours: Property.Number({
154
+ displayName: 'Maximum Age (Hours)',
155
+ description:
156
+ 'Only trigger for conversations started within this many hours',
157
+ required: false,
158
+ defaultValue: 24,
159
+ }),
160
+ },
161
+ sampleData: {},
162
+ type: TriggerStrategy.POLLING,
163
+ onEnable: async (context) => {
164
+ const authClient = await createGoogleClient(context.auth);
165
+ const gmail = googleGmail({ version: 'v1', auth: authClient });
166
+
167
+ const profile = await gmail.users.getProfile({ userId: 'me' });
168
+ await context.store.put('lastHistoryId', profile.data.historyId);
169
+ await context.store.put('processedThreads', []);
170
+ },
171
+ onDisable: async (context) => {
172
+ await context.store.delete('lastHistoryId');
173
+ await context.store.delete('processedThreads');
174
+ },
175
+ run: async (context) => {
176
+ const authClient = await createGoogleClient(context.auth);
177
+ const gmail = googleGmail({ version: 'v1', auth: authClient });
178
+
179
+ const lastHistoryId = await context.store.get('lastHistoryId');
180
+ const processedThreads =
181
+ (await context.store.get<string[]>('processedThreads')) || [];
182
+ const maxAge = (context.propsValue.maxAgeHours || 24) * 60 * 60 * 1000;
183
+ const cutoffTime = Date.now() - maxAge;
184
+
185
+ try {
186
+ const historyResponse = await gmail.users.history.list({
187
+ userId: 'me',
188
+ startHistoryId: lastHistoryId as string,
189
+ historyTypes: ['messageAdded'],
190
+ maxResults: 100,
191
+ });
192
+
193
+ const newConversations: string[] = [];
194
+
195
+ if (historyResponse.data.history) {
196
+ for (const history of historyResponse.data.history) {
197
+ if (history.messagesAdded) {
198
+ for (const added of history.messagesAdded) {
199
+ const threadId = added.message?.threadId;
200
+ if (threadId && !processedThreads.includes(threadId)) {
201
+ const threadResponse = await gmail.users.threads.get({
202
+ userId: 'me',
203
+ id: threadId,
204
+ format: 'minimal',
205
+ });
206
+
207
+ if (threadResponse.data.messages?.length === 1) {
208
+ newConversations.push(threadId);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ const results: any[] = [];
217
+ for (const threadId of newConversations) {
218
+ const threadResponse = await gmail.users.threads.get({
219
+ userId: 'me',
220
+ id: threadId,
221
+ format: 'full',
222
+ });
223
+
224
+ const thread = threadResponse.data;
225
+ const firstMessage = thread.messages?.[0];
226
+ if (!firstMessage) continue;
227
+
228
+ const messageDate = parseInt(firstMessage.internalDate || '0');
229
+ if (messageDate < cutoffTime) continue;
230
+
231
+ const headers = firstMessage.payload?.headers || [];
232
+ const headerMap: { [key: string]: string } = {};
233
+ headers.forEach((h: any) => {
234
+ if (h.name && h.value) {
235
+ headerMap[h.name.toLowerCase()] = h.value;
236
+ }
237
+ });
238
+
239
+ if (context.propsValue.from) {
240
+ const from = headerMap['from'] || '';
241
+ if (
242
+ !from.toLowerCase().includes(context.propsValue.from.toLowerCase())
243
+ ) {
244
+ continue;
245
+ }
246
+ }
247
+
248
+ if (context.propsValue.subject) {
249
+ const subject = headerMap['subject'] || '';
250
+ if (
251
+ !subject
252
+ .toLowerCase()
253
+ .includes(context.propsValue.subject.toLowerCase())
254
+ ) {
255
+ continue;
256
+ }
257
+ }
258
+
259
+ const rawResponse = await gmail.users.messages.get({
260
+ userId: 'me',
261
+ id: firstMessage.id!,
262
+ format: 'raw',
263
+ });
264
+
265
+ const parsedMessage = await parseStream(
266
+ Buffer.from(rawResponse.data.raw as string, 'base64').toString(
267
+ 'utf-8'
268
+ )
269
+ );
270
+
271
+ results.push({
272
+ id: `conversation_${threadId}`,
273
+ data: {
274
+ thread: {
275
+ id: threadId,
276
+ snippet: thread.snippet,
277
+ messageCount: 1,
278
+ },
279
+ message: {
280
+ ...parsedMessage,
281
+ id: firstMessage.id,
282
+ threadId: threadId,
283
+ date: new Date(messageDate).toISOString(),
284
+ attachments: await convertAttachment(
285
+ parsedMessage.attachments,
286
+ context.files
287
+ ),
288
+ },
289
+ conversation: {
290
+ starter: {
291
+ from: headerMap['from'],
292
+ to: headerMap['to'],
293
+ subject: headerMap['subject'],
294
+ date: headerMap['date'],
295
+ },
296
+ },
297
+ },
298
+ });
299
+
300
+ processedThreads.push(threadId);
301
+ }
302
+
303
+ if (historyResponse.data.historyId) {
304
+ await context.store.put(
305
+ 'lastHistoryId',
306
+ historyResponse.data.historyId
307
+ );
308
+ }
309
+
310
+ const recentThreads = processedThreads.slice(-1000);
311
+ await context.store.put('processedThreads', recentThreads);
312
+
313
+ return results;
314
+ } catch (error: any) {
315
+ if (error.code === 404) {
316
+ const profile = await gmail.users.getProfile({ userId: 'me' });
317
+ await context.store.put('lastHistoryId', profile.data.historyId);
318
+ return [];
319
+ }
320
+ throw error;
321
+ }
322
+ },
323
+ test: async (context) => {
324
+ const authClient = await createGoogleClient(context.auth);
325
+ const gmail = googleGmail({ version: 'v1', auth: authClient });
326
+
327
+ const maxAge = (context.propsValue.maxAgeHours || 24) * 60 * 60 * 1000;
328
+ const cutoffSeconds = Math.floor((Date.now() - maxAge) / 1000);
329
+
330
+ let query = `after:${cutoffSeconds}`;
331
+ if (context.propsValue.from) {
332
+ query += ` from:(${context.propsValue.from})`;
333
+ }
334
+ if (context.propsValue.subject) {
335
+ query += ` subject:(${context.propsValue.subject})`;
336
+ }
337
+
338
+ const threadsResponse = await gmail.users.threads.list({
339
+ userId: 'me',
340
+ q: query,
341
+ maxResults: 5,
342
+ });
343
+
344
+ const results: any[] = [];
345
+ if (threadsResponse.data.threads) {
346
+ for (const thread of threadsResponse.data.threads) {
347
+ const threadId = thread.id!;
348
+
349
+ const fullThread = await gmail.users.threads.get({
350
+ userId: 'me',
351
+ id: threadId,
352
+ format: 'full',
353
+ });
354
+
355
+ if (fullThread.data.messages?.length === 1) {
356
+ const firstMessage = fullThread.data.messages[0];
357
+ const headers = firstMessage.payload?.headers || [];
358
+ const headerMap: { [key: string]: string } = {};
359
+ headers.forEach((h: any) => {
360
+ if (h.name && h.value) {
361
+ headerMap[h.name.toLowerCase()] = h.value;
362
+ }
363
+ });
364
+
365
+ const rawResponse = await gmail.users.messages.get({
366
+ userId: 'me',
367
+ id: firstMessage.id!,
368
+ format: 'raw',
369
+ });
370
+
371
+ const parsedMessage = await parseStream(
372
+ Buffer.from(rawResponse.data.raw as string, 'base64').toString(
373
+ 'utf-8'
374
+ )
375
+ );
376
+
377
+ results.push({
378
+ id: `test_conversation_${threadId}`,
379
+ data: {
380
+ thread: {
381
+ id: threadId,
382
+ snippet: fullThread.data.snippet,
383
+ messageCount: 1,
384
+ },
385
+ message: {
386
+ ...parsedMessage,
387
+ id: firstMessage.id,
388
+ threadId: threadId,
389
+ date: new Date(
390
+ parseInt(firstMessage.internalDate || '0')
391
+ ).toISOString(),
392
+ attachments: await convertAttachment(
393
+ parsedMessage.attachments,
394
+ context.files
395
+ ),
396
+ },
397
+ conversation: {
398
+ starter: {
399
+ from: headerMap['from'],
400
+ to: headerMap['to'],
401
+ subject: headerMap['subject'],
402
+ date: headerMap['date'],
403
+ },
404
+ },
405
+ },
406
+ });
407
+ }
408
+ }
409
+ }
410
+
411
+ return results;
412
+ },
413
+ });