-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathAdaptor.js
More file actions
251 lines (220 loc) · 7.14 KB
/
Copy pathAdaptor.js
File metadata and controls
251 lines (220 loc) · 7.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import {
execute as commonExecute,
composeNextState,
} from '@openfn/language-common';
import {
normalizeOauthConfig,
expandReferences,
} from '@openfn/language-common/util';
import {
getMessagesResult,
getMessageResult,
getContentIndicators,
getMessageContent,
buildAndSendMessage,
createConnection,
removeConnection,
} from './Utils.js';
/**
* Used to isolate the type of content to retrieve from the message.
* @typedef {Object} MessageContent
* @public
* @property {string} [type] - Message content type. Valid types: from, date, subject, body, archive, file.
* @property {string} [name] - A custom description for the content type.
* @property {RegExp|string} [archive] - Identifier to isolate the desired attachment when type is 'archive'.
* Use a regular expression for pattern matching or a string for a literal match. Required if type is 'archive'.
* @property {RegExp|string} [file] - Identifier to isolate the desired attachment when type is 'file' or 'archive'.
* Use a regular expression for pattern matching or a string for a literal match. Required if type is 'file' or 'archive'.
* @property {number?} [maxLength] - Maximum number of characters to retrieve from the content.
*/
/**
* Configurable options provided to the Gmail adaptor.
* @typedef {Object} Options
* @public
* @property {string?} [query] - Gmail search query string.
* @property {Array<string|MessageContent>} [contents=['from', 'date', 'subject', 'body']]
* An array of strings or MessageContent objects used to specify which parts of the message to retrieve.
* @property {Array<string>} [processedIds] - Ignore message ids which have already been processed.
* @property {string?} [email] - The user account to retrieve messages from. Defaults to the authenticated user.
* @property {int?} [maxResults] - Maximum number of messages to process per request. Default is 1000.
*/
/**
* Downloads contents from messages of a Gmail account.
* @public
* @function
* @param {Options} options - Customized options including desired contents and query.
* @state {Array} data - The returned message objects, of the form `{ messageId, contents } `
* @state {Array<string>} processedIds - An array of string ids processed by this request
* @returns {Operation}
* @example <caption>Get a message with a specific subject</caption>
* getContentsFromMessages(
* {
* query: 'subject:my+test+message'
* }
* )
* @example <caption>Get messages after a specific date, with subject and report.txt attachment</caption>
* getContentsFromMessages(
* {
* query: 'after:15/01/2025',
* contents: [
* 'subject',
* { type: 'file', name: 'metadata', file: 'report.txt'}
* ]
* }
* )
*/
export function getContentsFromMessages(options) {
return async state => {
const [resolvedOptions] = expandReferences(state, options);
const defaultOptions = {
contents: ['from', 'date', 'subject'],
userId: 'me',
maxResults: 1000,
};
const opts = {
userId: resolvedOptions.email ?? defaultOptions.userId,
query: resolvedOptions.query,
processedIds: resolvedOptions.processedIds,
maxResults: resolvedOptions.maxResults ?? defaultOptions.maxResults,
};
const contentIndicators = getContentIndicators(
defaultOptions.contents,
resolvedOptions.contents
);
const contents = [];
const newIds = [];
const previousIds = Array.isArray(opts.processedIds)
? opts.processedIds
: [];
let nextPageToken = null;
doNextPageToken: do {
const messagesResult = await getMessagesResult(
opts.userId,
opts.query,
nextPageToken
);
if (!messagesResult.messages?.length) {
console.log('No messages found.');
break;
}
nextPageToken = messagesResult.nextPageToken;
for (const message of messagesResult.messages) {
newIds.push(message.id);
if (previousIds.includes(message.id)) {
continue;
}
const content = {
messageId: message.id,
};
const messageResult = await getMessageResult(opts.userId, message.id);
for (const contentIndicator of contentIndicators) {
const messageContent = await getMessageContent(
messageResult,
contentIndicator
);
if (messageContent && content[contentIndicator.name]) {
throw new Error(
`Duplicate content name detected: ${contentIndicator.name}`
);
}
content[contentIndicator.name] ??= messageContent;
}
contents.push(content);
if (contents.length >= opts.maxResults) {
break doNextPageToken;
}
}
} while (nextPageToken);
return {
...composeNextState(state, contents),
processedIds: newIds,
};
};
}
/**
* Configurable fields for composing an outbound Gmail message.
* @typedef {Object} SendMessageOptions
* @property {string} to - Recipient email address.
* @property {string} subject - Subject line of the email.
* @property {string} body - Email body content.
* @property {Array<{ filename: string, content: string|Buffer }>} [attachments] - Optional list of files to attach.
*/
/**
* Sends a Gmail message using the provided configuration.
* Supports attachments and standard email fields like subject, body, and recipients.
*
* @public
* @function
* @param {SendMessageOptions|SendMessageOptions[]} message - The message configuration object or array of objects.
* @state {Object} data - The Gmail API response from sending the message.
* @returns {Operation}
* @example
* sendMessage({
* to: 'recipient@example.org',
* subject: 'Test Message',
* body: 'Hello from OpenFn!',
* attachments: [
* { filename: 'test.txt', content: 'Some text content' }
* ]
* })
*/
export function sendMessage(message) {
return async state => {
const [resolvedMessage] = expandReferences(state, message);
const messages = Array.isArray(resolvedMessage)
? resolvedMessage
: [resolvedMessage];
const results = [];
for (const msg of messages) {
const result = await buildAndSendMessage(msg);
results.push(result);
}
return {
...composeNextState(state, results),
};
};
}
/**
* Execute a sequence of operations.
* Wraps `language-common/execute`, and prepends initial state for http.
* @private
* @param {...Function} operations - Operations to be performed.
* @returns {Operation}
*/
export function execute(...operations) {
const initialState = {
references: [],
data: null,
};
return state => {
const isServiceAccount =
state.configuration?.private_key && state.configuration?.client_email;
return commonExecute(
createConnection,
...operations,
removeConnection
)({
...initialState,
...state,
configuration: isServiceAccount
? state.configuration
: normalizeOauthConfig(state.configuration),
});
};
}
export {
alterState,
combine,
cursor,
dataPath,
dataValue,
each,
field,
fields,
fn,
fnIf,
lastReferenceValue,
log,
merge,
sourceValue,
} from '@openfn/language-common';