@kubun/connector-google-mail 0.12.0

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/lib/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { createGoogleMailActionHandler, type GoogleMailActionHandlerConfig } from './actions.js';
2
+ export { buildHistoryListURL, buildLabelsListURL, buildMessageGetURL, buildMessagesListURL, buildProfileURL, buildRawMessage, createDraft, createLabel, decodeBase64Url, encodeBase64Url, extractEmail, extractEmails, type GmailHistoryListResponse, type GmailHistoryRecord, type GmailLabelResource, type GmailLabelsListResponse, type GmailMessagePart, type GmailMessageResource, type GmailMessagesListResponse, getHeader, mapLabelToEntity, mapMessageToEntity, mapThreadToEntity, modifyLabels, sendMessage, updateLabel, } from './api.js';
3
+ export { type CreateGoogleMailConnectorOptions, createGoogleMailConnector } from './connector.js';
4
+ export { googleMailClusters, googleMailLabelDefinition, googleMailMessageDefinition, googleMailThreadDefinition, } from './model.js';
5
+ export { createGoogleMailProvider, DEFAULT_QUERY, type GoogleMailProviderConfig, type MailCheckpoint, } from './provider.js';
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { createGoogleMailActionHandler } from './actions.js';
2
+ export { buildHistoryListURL, buildLabelsListURL, buildMessageGetURL, buildMessagesListURL, buildProfileURL, buildRawMessage, createDraft, createLabel, decodeBase64Url, encodeBase64Url, extractEmail, extractEmails, getHeader, mapLabelToEntity, mapMessageToEntity, mapThreadToEntity, modifyLabels, sendMessage, updateLabel } from './api.js';
3
+ export { createGoogleMailConnector } from './connector.js';
4
+ export { googleMailClusters, googleMailLabelDefinition, googleMailMessageDefinition, googleMailThreadDefinition } from './model.js';
5
+ export { createGoogleMailProvider, DEFAULT_QUERY } from './provider.js';
package/lib/model.d.ts ADDED
@@ -0,0 +1,227 @@
1
+ export declare const googleMailThreadDefinition: {
2
+ readonly name: "GoogleMailThread";
3
+ readonly behavior: "default";
4
+ readonly interfaces: [string, string];
5
+ readonly schema: {
6
+ readonly type: "object";
7
+ readonly properties: {
8
+ readonly title: {
9
+ readonly type: "string";
10
+ readonly maxLength: 200;
11
+ };
12
+ readonly sourceService: {
13
+ readonly $ref: "#/definitions/SourceService";
14
+ };
15
+ readonly sourceID: {
16
+ readonly $ref: "#/definitions/SourceID";
17
+ };
18
+ readonly sourceURL: {
19
+ readonly $ref: "#/definitions/SourceURL";
20
+ };
21
+ readonly lastSyncedAt: {
22
+ readonly $ref: "#/definitions/DateTime";
23
+ };
24
+ };
25
+ readonly required: ["sourceService", "sourceID", "lastSyncedAt"];
26
+ readonly additionalProperties: false;
27
+ readonly definitions: {
28
+ readonly SourceService: {
29
+ readonly type: "string";
30
+ readonly title: "SourceService";
31
+ readonly minLength: 1;
32
+ readonly maxLength: 50;
33
+ };
34
+ readonly SourceID: {
35
+ readonly type: "string";
36
+ readonly title: "SourceID";
37
+ readonly minLength: 1;
38
+ readonly maxLength: 500;
39
+ };
40
+ readonly SourceURL: import("json-schema-typed").JSONSchema.String;
41
+ readonly DateTime: import("json-schema-typed").JSONSchema.String;
42
+ };
43
+ };
44
+ readonly fieldsMeta: {};
45
+ };
46
+ export declare const googleMailMessageDefinition: {
47
+ readonly name: "GoogleMailMessage";
48
+ readonly behavior: "default";
49
+ readonly interfaces: [string, string];
50
+ readonly schema: {
51
+ readonly type: "object";
52
+ readonly properties: {
53
+ readonly body: {
54
+ readonly type: "string";
55
+ };
56
+ readonly sender: {
57
+ readonly type: "string";
58
+ readonly minLength: 1;
59
+ readonly maxLength: 320;
60
+ };
61
+ readonly subject: {
62
+ readonly type: "string";
63
+ };
64
+ readonly snippet: {
65
+ readonly type: "string";
66
+ };
67
+ readonly to: {
68
+ readonly type: "array";
69
+ readonly title: "To";
70
+ readonly items: {
71
+ readonly type: "string";
72
+ };
73
+ };
74
+ readonly cc: {
75
+ readonly type: "array";
76
+ readonly title: "Cc";
77
+ readonly items: {
78
+ readonly type: "string";
79
+ };
80
+ };
81
+ readonly labels: {
82
+ readonly type: "array";
83
+ readonly title: "Labels";
84
+ readonly items: {
85
+ readonly type: "string";
86
+ };
87
+ };
88
+ readonly sentAt: {
89
+ readonly $ref: "#/definitions/DateTime";
90
+ };
91
+ readonly unread: {
92
+ readonly type: "boolean";
93
+ };
94
+ readonly attachments: {
95
+ readonly type: "array";
96
+ readonly title: "Attachments";
97
+ readonly items: {
98
+ readonly $ref: "#/definitions/Attachment";
99
+ };
100
+ };
101
+ readonly threadID: {
102
+ readonly type: "string";
103
+ };
104
+ readonly thread: {
105
+ readonly $ref: "#/definitions/ThreadDocID";
106
+ };
107
+ readonly sourceService: {
108
+ readonly $ref: "#/definitions/SourceService";
109
+ };
110
+ readonly sourceID: {
111
+ readonly $ref: "#/definitions/SourceID";
112
+ };
113
+ readonly sourceURL: {
114
+ readonly $ref: "#/definitions/SourceURL";
115
+ };
116
+ readonly lastSyncedAt: {
117
+ readonly $ref: "#/definitions/DateTime";
118
+ };
119
+ };
120
+ readonly required: ["body", "sender", "threadID", "thread", "sourceService", "sourceID", "lastSyncedAt"];
121
+ readonly additionalProperties: false;
122
+ readonly definitions: {
123
+ readonly ThreadDocID: import("json-schema-typed").JSONSchema.String;
124
+ readonly Attachment: {
125
+ readonly type: "object";
126
+ readonly title: "Attachment";
127
+ readonly properties: {
128
+ readonly filename: {
129
+ readonly type: "string";
130
+ };
131
+ readonly mimeType: {
132
+ readonly type: "string";
133
+ };
134
+ };
135
+ readonly required: ["filename", "mimeType"];
136
+ readonly additionalProperties: false;
137
+ };
138
+ readonly SourceService: {
139
+ readonly type: "string";
140
+ readonly title: "SourceService";
141
+ readonly minLength: 1;
142
+ readonly maxLength: 50;
143
+ };
144
+ readonly SourceID: {
145
+ readonly type: "string";
146
+ readonly title: "SourceID";
147
+ readonly minLength: 1;
148
+ readonly maxLength: 500;
149
+ };
150
+ readonly SourceURL: import("json-schema-typed").JSONSchema.String;
151
+ readonly DateTime: import("json-schema-typed").JSONSchema.String;
152
+ };
153
+ };
154
+ readonly fieldsMeta: {
155
+ readonly thread: {
156
+ readonly type: "document";
157
+ readonly model: "#0";
158
+ };
159
+ };
160
+ };
161
+ export declare const googleMailLabelDefinition: {
162
+ readonly name: "GoogleMailLabel";
163
+ readonly behavior: "default";
164
+ readonly interfaces: [string];
165
+ readonly schema: {
166
+ readonly type: "object";
167
+ readonly properties: {
168
+ readonly name: {
169
+ readonly type: "string";
170
+ readonly minLength: 1;
171
+ readonly maxLength: 225;
172
+ };
173
+ readonly type: {
174
+ readonly type: "string";
175
+ readonly maxLength: 20;
176
+ };
177
+ readonly messageListVisibility: {
178
+ readonly type: "string";
179
+ readonly maxLength: 20;
180
+ };
181
+ readonly labelListVisibility: {
182
+ readonly type: "string";
183
+ readonly maxLength: 30;
184
+ };
185
+ readonly textColor: {
186
+ readonly type: "string";
187
+ readonly maxLength: 20;
188
+ };
189
+ readonly backgroundColor: {
190
+ readonly type: "string";
191
+ readonly maxLength: 20;
192
+ };
193
+ readonly sourceService: {
194
+ readonly $ref: "#/definitions/SourceService";
195
+ };
196
+ readonly sourceID: {
197
+ readonly $ref: "#/definitions/SourceID";
198
+ };
199
+ readonly sourceURL: {
200
+ readonly $ref: "#/definitions/SourceURL";
201
+ };
202
+ readonly lastSyncedAt: {
203
+ readonly $ref: "#/definitions/DateTime";
204
+ };
205
+ };
206
+ readonly required: ["name", "sourceService", "sourceID", "lastSyncedAt"];
207
+ readonly additionalProperties: false;
208
+ readonly definitions: {
209
+ readonly SourceService: {
210
+ readonly type: "string";
211
+ readonly title: "SourceService";
212
+ readonly minLength: 1;
213
+ readonly maxLength: 50;
214
+ };
215
+ readonly SourceID: {
216
+ readonly type: "string";
217
+ readonly title: "SourceID";
218
+ readonly minLength: 1;
219
+ readonly maxLength: 500;
220
+ };
221
+ readonly SourceURL: import("json-schema-typed").JSONSchema.String;
222
+ readonly DateTime: import("json-schema-typed").JSONSchema.String;
223
+ };
224
+ };
225
+ readonly fieldsMeta: {};
226
+ };
227
+ export declare const googleMailClusters: import("@kubun/protocol").ClustersRecord;
package/lib/model.js ADDED
@@ -0,0 +1,245 @@
1
+ import { connectorClusters, connectorModelIDs } from '@kubun/connector';
2
+ import { modelIDs, modelsClusters } from '@kubun/models';
3
+ import { GraphBuilder } from '@kubun/protocol';
4
+ import { dateTimeSchema, docIDSchema, urlSchema } from '@kubun/scalars';
5
+ const commonDefinitions = {
6
+ SourceService: {
7
+ type: 'string',
8
+ title: 'SourceService',
9
+ minLength: 1,
10
+ maxLength: 50
11
+ },
12
+ SourceID: {
13
+ type: 'string',
14
+ title: 'SourceID',
15
+ minLength: 1,
16
+ maxLength: 500
17
+ },
18
+ SourceURL: urlSchema,
19
+ DateTime: dateTimeSchema
20
+ };
21
+ export const googleMailThreadDefinition = {
22
+ name: 'GoogleMailThread',
23
+ behavior: 'default',
24
+ interfaces: [
25
+ connectorModelIDs.ExternalEntity,
26
+ modelIDs.ThreadKind
27
+ ],
28
+ schema: {
29
+ type: 'object',
30
+ properties: {
31
+ title: {
32
+ type: 'string',
33
+ maxLength: 200
34
+ },
35
+ sourceService: {
36
+ $ref: '#/definitions/SourceService'
37
+ },
38
+ sourceID: {
39
+ $ref: '#/definitions/SourceID'
40
+ },
41
+ sourceURL: {
42
+ $ref: '#/definitions/SourceURL'
43
+ },
44
+ lastSyncedAt: {
45
+ $ref: '#/definitions/DateTime'
46
+ }
47
+ },
48
+ required: [
49
+ 'sourceService',
50
+ 'sourceID',
51
+ 'lastSyncedAt'
52
+ ],
53
+ additionalProperties: false,
54
+ definitions: {
55
+ ...commonDefinitions
56
+ }
57
+ },
58
+ fieldsMeta: {}
59
+ };
60
+ export const googleMailMessageDefinition = {
61
+ name: 'GoogleMailMessage',
62
+ behavior: 'default',
63
+ interfaces: [
64
+ connectorModelIDs.ExternalEntity,
65
+ modelIDs.MessageKind
66
+ ],
67
+ schema: {
68
+ type: 'object',
69
+ properties: {
70
+ body: {
71
+ type: 'string'
72
+ },
73
+ sender: {
74
+ type: 'string',
75
+ minLength: 1,
76
+ maxLength: 320
77
+ },
78
+ subject: {
79
+ type: 'string'
80
+ },
81
+ snippet: {
82
+ type: 'string'
83
+ },
84
+ to: {
85
+ type: 'array',
86
+ title: 'To',
87
+ items: {
88
+ type: 'string'
89
+ }
90
+ },
91
+ cc: {
92
+ type: 'array',
93
+ title: 'Cc',
94
+ items: {
95
+ type: 'string'
96
+ }
97
+ },
98
+ labels: {
99
+ type: 'array',
100
+ title: 'Labels',
101
+ items: {
102
+ type: 'string'
103
+ }
104
+ },
105
+ sentAt: {
106
+ $ref: '#/definitions/DateTime'
107
+ },
108
+ unread: {
109
+ type: 'boolean'
110
+ },
111
+ attachments: {
112
+ type: 'array',
113
+ title: 'Attachments',
114
+ items: {
115
+ $ref: '#/definitions/Attachment'
116
+ }
117
+ },
118
+ threadID: {
119
+ type: 'string'
120
+ },
121
+ thread: {
122
+ $ref: '#/definitions/ThreadDocID'
123
+ },
124
+ sourceService: {
125
+ $ref: '#/definitions/SourceService'
126
+ },
127
+ sourceID: {
128
+ $ref: '#/definitions/SourceID'
129
+ },
130
+ sourceURL: {
131
+ $ref: '#/definitions/SourceURL'
132
+ },
133
+ lastSyncedAt: {
134
+ $ref: '#/definitions/DateTime'
135
+ }
136
+ },
137
+ required: [
138
+ 'body',
139
+ 'sender',
140
+ 'threadID',
141
+ 'thread',
142
+ 'sourceService',
143
+ 'sourceID',
144
+ 'lastSyncedAt'
145
+ ],
146
+ additionalProperties: false,
147
+ definitions: {
148
+ ...commonDefinitions,
149
+ ThreadDocID: docIDSchema,
150
+ Attachment: {
151
+ type: 'object',
152
+ title: 'Attachment',
153
+ properties: {
154
+ filename: {
155
+ type: 'string'
156
+ },
157
+ mimeType: {
158
+ type: 'string'
159
+ }
160
+ },
161
+ required: [
162
+ 'filename',
163
+ 'mimeType'
164
+ ],
165
+ additionalProperties: false
166
+ }
167
+ }
168
+ },
169
+ fieldsMeta: {
170
+ thread: {
171
+ type: 'document',
172
+ model: '#0'
173
+ }
174
+ }
175
+ };
176
+ export const googleMailLabelDefinition = {
177
+ name: 'GoogleMailLabel',
178
+ behavior: 'default',
179
+ interfaces: [
180
+ connectorModelIDs.ExternalEntity
181
+ ],
182
+ schema: {
183
+ type: 'object',
184
+ properties: {
185
+ name: {
186
+ type: 'string',
187
+ minLength: 1,
188
+ maxLength: 225
189
+ },
190
+ type: {
191
+ type: 'string',
192
+ maxLength: 20
193
+ },
194
+ messageListVisibility: {
195
+ type: 'string',
196
+ maxLength: 20
197
+ },
198
+ labelListVisibility: {
199
+ type: 'string',
200
+ maxLength: 30
201
+ },
202
+ textColor: {
203
+ type: 'string',
204
+ maxLength: 20
205
+ },
206
+ backgroundColor: {
207
+ type: 'string',
208
+ maxLength: 20
209
+ },
210
+ sourceService: {
211
+ $ref: '#/definitions/SourceService'
212
+ },
213
+ sourceID: {
214
+ $ref: '#/definitions/SourceID'
215
+ },
216
+ sourceURL: {
217
+ $ref: '#/definitions/SourceURL'
218
+ },
219
+ lastSyncedAt: {
220
+ $ref: '#/definitions/DateTime'
221
+ }
222
+ },
223
+ required: [
224
+ 'name',
225
+ 'sourceService',
226
+ 'sourceID',
227
+ 'lastSyncedAt'
228
+ ],
229
+ additionalProperties: false,
230
+ definitions: {
231
+ ...commonDefinitions
232
+ }
233
+ },
234
+ fieldsMeta: {}
235
+ };
236
+ const builder = new GraphBuilder({
237
+ externalClusters: {
238
+ ...connectorClusters,
239
+ ...modelsClusters
240
+ }
241
+ });
242
+ builder.add(googleMailThreadDefinition); // #0
243
+ builder.add(googleMailMessageDefinition); // #1
244
+ builder.add(googleMailLabelDefinition); // #2
245
+ export const googleMailClusters = builder.build();
@@ -0,0 +1,12 @@
1
+ import type { Credential, ServerDataProvider, SyncBoundary } from '@kubun/connector';
2
+ export declare const DEFAULT_QUERY = "in:inbox";
3
+ export type MailCheckpoint = {
4
+ historyId: string;
5
+ };
6
+ export type GoogleMailProviderConfig = {
7
+ credential: Credential;
8
+ boundary?: SyncBoundary;
9
+ fetch?: typeof globalThis.fetch;
10
+ query?: string;
11
+ };
12
+ export declare function createGoogleMailProvider(config: GoogleMailProviderConfig): ServerDataProvider;
@@ -0,0 +1,190 @@
1
+ import { resolveModelID } from '@kubun/connector';
2
+ import { buildHistoryListURL, buildLabelsListURL, buildMessageGetURL, buildMessagesListURL, buildProfileURL, getHeader, mapLabelToEntity, mapMessageToEntity, mapThreadToEntity, parseGoogleError } from './api.js';
3
+ import { googleMailClusters } from './model.js';
4
+ export const DEFAULT_QUERY = 'in:inbox';
5
+ const THREAD_MODEL_ID = resolveModelID(googleMailClusters, 'GoogleMailThread');
6
+ const MESSAGE_MODEL_ID = resolveModelID(googleMailClusters, 'GoogleMailMessage');
7
+ const LABEL_MODEL_ID = resolveModelID(googleMailClusters, 'GoogleMailLabel');
8
+ async function fetchJSON(url, credential, fetchFn, signal) {
9
+ const response = await fetchFn(url, {
10
+ headers: {
11
+ Authorization: `Bearer ${credential.accessToken}`
12
+ },
13
+ signal
14
+ });
15
+ if (!response.ok) {
16
+ throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
17
+ }
18
+ return await response.json();
19
+ }
20
+ function applyMaxAge(query, boundary) {
21
+ if (boundary?.maxAge == null) return query;
22
+ const match = /^P(\d+)D$/i.exec(boundary.maxAge);
23
+ if (match == null) return query;
24
+ if (/\b(newer_than:|after:)/i.test(query)) return query;
25
+ return `${query} newer_than:${match[1]}d`;
26
+ }
27
+ function maxHistoryId(messages) {
28
+ let max = -1;
29
+ for (const m of messages){
30
+ const n = m.historyId != null ? Number.parseInt(m.historyId, 10) : Number.NaN;
31
+ if (!Number.isNaN(n) && n > max) max = n;
32
+ }
33
+ return max < 0 ? undefined : String(max);
34
+ }
35
+ export function createGoogleMailProvider(config) {
36
+ const fetchFn = config.fetch ?? globalThis.fetch;
37
+ async function listMessageIDs(query, signal) {
38
+ const ids = [];
39
+ let pageToken;
40
+ do {
41
+ const data = await fetchJSON(buildMessagesListURL({
42
+ query,
43
+ pageToken
44
+ }), config.credential, fetchFn, signal);
45
+ for (const m of data.messages ?? [])ids.push(m);
46
+ pageToken = data.nextPageToken;
47
+ }while (pageToken != null)
48
+ return ids;
49
+ }
50
+ async function getMessage(id, signal) {
51
+ return fetchJSON(buildMessageGetURL({
52
+ messageID: id
53
+ }), config.credential, fetchFn, signal);
54
+ }
55
+ async function profileHistoryId(signal) {
56
+ const profile = await fetchJSON(buildProfileURL(), config.credential, fetchFn, signal);
57
+ return profile.historyId ?? '0';
58
+ }
59
+ // Labels have no incremental history API, so every sync re-lists them in full.
60
+ // The list endpoint is a single cheap call; it omits color (captured only on
61
+ // create/edit) but carries id, name, type, and the visibility fields.
62
+ async function labelBatch(signal) {
63
+ const data = await fetchJSON(buildLabelsListURL(), config.credential, fetchFn, signal);
64
+ return {
65
+ modelID: LABEL_MODEL_ID,
66
+ entities: (data.labels ?? []).map(mapLabelToEntity),
67
+ hasMore: true
68
+ };
69
+ }
70
+ async function* fetchAllEntities(params) {
71
+ yield await labelBatch(params.signal);
72
+ const query = applyMaxAge(config.query ?? DEFAULT_QUERY, params.boundary);
73
+ const ids = await listMessageIDs(query, params.signal);
74
+ const messages = [];
75
+ for (const { id } of ids){
76
+ messages.push(await getMessage(id, params.signal));
77
+ }
78
+ // Thread batch: one thread per distinct threadId, titled by its first message's subject.
79
+ const threadSubjects = new Map();
80
+ for (const m of messages){
81
+ if (!threadSubjects.has(m.threadId)) {
82
+ threadSubjects.set(m.threadId, getHeader(m.payload?.headers, 'Subject'));
83
+ }
84
+ }
85
+ yield {
86
+ modelID: THREAD_MODEL_ID,
87
+ entities: [
88
+ ...threadSubjects
89
+ ].map(([threadId, subject])=>mapThreadToEntity(threadId, subject)),
90
+ hasMore: true
91
+ };
92
+ const checkpoint = {
93
+ historyId: maxHistoryId(messages) ?? await profileHistoryId(params.signal)
94
+ };
95
+ yield {
96
+ modelID: MESSAGE_MODEL_ID,
97
+ entities: messages.map(mapMessageToEntity),
98
+ hasMore: false,
99
+ checkpoint
100
+ };
101
+ }
102
+ async function* fetchChangesEntities(params) {
103
+ const checkpoint = params.checkpoint;
104
+ if (checkpoint == null) {
105
+ yield* fetchAllEntities(params);
106
+ return;
107
+ }
108
+ yield await labelBatch(params.signal);
109
+ const addedIDs = new Map() // id -> threadId
110
+ ;
111
+ const deletedIDs = new Set();
112
+ let latestHistoryId = checkpoint.historyId;
113
+ let pageToken;
114
+ do {
115
+ const response = await fetchFn(buildHistoryListURL({
116
+ startHistoryId: checkpoint.historyId,
117
+ pageToken
118
+ }), {
119
+ headers: {
120
+ Authorization: `Bearer ${config.credential.accessToken}`
121
+ },
122
+ signal: params.signal
123
+ });
124
+ if (response.status === 404) {
125
+ yield* fetchAllEntities(params);
126
+ return;
127
+ }
128
+ if (!response.ok) {
129
+ throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
130
+ }
131
+ const data = await response.json();
132
+ for (const record of data.history ?? []){
133
+ for (const ref of [
134
+ ...record.messagesAdded ?? [],
135
+ ...record.labelsAdded ?? [],
136
+ ...record.labelsRemoved ?? []
137
+ ]){
138
+ addedIDs.set(ref.message.id, ref.message.threadId);
139
+ }
140
+ for (const ref of record.messagesDeleted ?? []){
141
+ deletedIDs.add(ref.message.id);
142
+ addedIDs.delete(ref.message.id);
143
+ }
144
+ }
145
+ if (data.historyId != null) latestHistoryId = data.historyId;
146
+ pageToken = data.nextPageToken;
147
+ }while (pageToken != null)
148
+ const survivingIDs = [
149
+ ...addedIDs.keys()
150
+ ].filter((id)=>!deletedIDs.has(id));
151
+ const messages = [];
152
+ for (const id of survivingIDs){
153
+ messages.push(await getMessage(id, params.signal));
154
+ }
155
+ const threadSubjects = new Map();
156
+ for (const m of messages){
157
+ if (!threadSubjects.has(m.threadId)) {
158
+ threadSubjects.set(m.threadId, getHeader(m.payload?.headers, 'Subject'));
159
+ }
160
+ }
161
+ yield {
162
+ modelID: THREAD_MODEL_ID,
163
+ entities: [
164
+ ...threadSubjects
165
+ ].map(([threadId, subject])=>mapThreadToEntity(threadId, subject)),
166
+ hasMore: true
167
+ };
168
+ yield {
169
+ modelID: MESSAGE_MODEL_ID,
170
+ entities: messages.map(mapMessageToEntity),
171
+ deleted: [
172
+ ...deletedIDs
173
+ ].map((id)=>({
174
+ sourceService: 'google-mail',
175
+ sourceID: id
176
+ })),
177
+ hasMore: false,
178
+ checkpoint: {
179
+ historyId: latestHistoryId
180
+ }
181
+ };
182
+ }
183
+ return {
184
+ supportedStrategies: [
185
+ 'on-demand'
186
+ ],
187
+ fetchAll: fetchAllEntities,
188
+ fetchChanges: fetchChangesEntities
189
+ };
190
+ }