@kubun/connector-google-mail 0.12.0 → 0.13.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/actions.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import type { ActionHandler } from '@kubun/connector';
1
+ import type { ActionHandler, WriteAttachment } from '@kubun/connector';
2
2
  export type GoogleMailActionHandlerConfig = {
3
3
  credential: {
4
4
  accessToken: string;
5
5
  scopes: Array<string>;
6
6
  };
7
7
  fetch?: typeof globalThis.fetch;
8
+ writeAttachment?: WriteAttachment;
8
9
  };
9
10
  export declare function createGoogleMailActionHandler(config: GoogleMailActionHandlerConfig): ActionHandler;
package/lib/actions.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createDraft, createLabel, modifyLabels, sendMessage, updateLabel } from './api.js';
1
+ import { createDraft, createLabel, importMessageAttachment, modifyLabels, sendMessage, updateLabel } from './api.js';
2
2
  export function createGoogleMailActionHandler(config) {
3
3
  return {
4
4
  async create (model, params) {
@@ -22,7 +22,7 @@ export function createGoogleMailActionHandler(config) {
22
22
  fetch: config.fetch
23
23
  });
24
24
  },
25
- async update (model, sourceID, params) {
25
+ async update (model, sourceID, params, existing) {
26
26
  if (model === 'GoogleMailLabel') {
27
27
  return updateLabel({
28
28
  accessToken: config.credential.accessToken,
@@ -31,6 +31,22 @@ export function createGoogleMailActionHandler(config) {
31
31
  fetch: config.fetch
32
32
  });
33
33
  }
34
+ // On GoogleMailMessage, a providerRef selects an attachment to import: its
35
+ // bytes are downloaded and written to the blob store, filling that entry's
36
+ // blob field. Without it, the update modifies labels as before.
37
+ if (typeof params.providerRef === 'string') {
38
+ if (config.writeAttachment == null) {
39
+ throw new Error('Google Mail: cannot import attachment, no blob plugin installed');
40
+ }
41
+ return importMessageAttachment({
42
+ accessToken: config.credential.accessToken,
43
+ messageID: sourceID,
44
+ providerRef: params.providerRef,
45
+ existing,
46
+ writeAttachment: config.writeAttachment,
47
+ fetch: config.fetch
48
+ });
49
+ }
34
50
  return modifyLabels({
35
51
  accessToken: config.credential.accessToken,
36
52
  messageID: sourceID,
package/lib/api.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { EntityRecord } from '@kubun/connector';
1
+ import type { EntityRecord, WriteAttachment } from '@kubun/connector';
2
2
  export declare const MAIL_BASE_URL = "https://gmail.googleapis.com/gmail/v1";
3
3
  export declare function parseGoogleError(response: Response): Promise<string>;
4
4
  export declare function decodeBase64Url(data: string): string;
5
+ export declare function decodeBase64UrlBytes(data: string): Uint8Array;
5
6
  export declare function encodeBase64Url(text: string): string;
6
7
  export declare function buildMessagesListURL(params: {
7
8
  query: string;
@@ -10,6 +11,10 @@ export declare function buildMessagesListURL(params: {
10
11
  export declare function buildMessageGetURL(params: {
11
12
  messageID: string;
12
13
  }): string;
14
+ export declare function buildAttachmentGetURL(params: {
15
+ messageID: string;
16
+ attachmentID: string;
17
+ }): string;
13
18
  export declare function buildHistoryListURL(params: {
14
19
  startHistoryId: string;
15
20
  pageToken?: string;
@@ -87,6 +92,14 @@ export declare function getHeader(headers: Array<{
87
92
  export declare function extractEmail(headerValue: string): string;
88
93
  export declare function extractEmails(headerValue: string | undefined): Array<string>;
89
94
  export declare function mapMessageToEntity(message: GmailMessageResource): EntityRecord;
95
+ export declare function importMessageAttachment(config: {
96
+ accessToken: string;
97
+ messageID: string;
98
+ providerRef: string;
99
+ existing: Record<string, unknown> | undefined;
100
+ writeAttachment: WriteAttachment;
101
+ fetch?: typeof globalThis.fetch;
102
+ }): Promise<EntityRecord>;
90
103
  export declare function mapThreadToEntity(threadID: string, subject: string | undefined): EntityRecord;
91
104
  export declare function buildRawMessage(params: {
92
105
  to: string | Array<string>;
package/lib/api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { b64uFromUTF, b64uToUTF, fromB64U } from '@sozai/codec';
1
2
  export const MAIL_BASE_URL = 'https://gmail.googleapis.com/gmail/v1';
2
3
  export async function parseGoogleError(response) {
3
4
  try {
@@ -8,10 +9,13 @@ export async function parseGoogleError(response) {
8
9
  }
9
10
  }
10
11
  export function decodeBase64Url(data) {
11
- return Buffer.from(data, 'base64url').toString('utf8');
12
+ return b64uToUTF(data);
13
+ }
14
+ export function decodeBase64UrlBytes(data) {
15
+ return fromB64U(data);
12
16
  }
13
17
  export function encodeBase64Url(text) {
14
- return Buffer.from(text, 'utf8').toString('base64url');
18
+ return b64uFromUTF(text);
15
19
  }
16
20
  export function buildMessagesListURL(params) {
17
21
  const url = new URL(`${MAIL_BASE_URL}/users/me/messages`);
@@ -24,6 +28,9 @@ export function buildMessageGetURL(params) {
24
28
  url.searchParams.set('format', 'full');
25
29
  return url.toString();
26
30
  }
31
+ export function buildAttachmentGetURL(params) {
32
+ return `${MAIL_BASE_URL}/users/me/messages/${encodeURIComponent(params.messageID)}/attachments/${encodeURIComponent(params.attachmentID)}`;
33
+ }
27
34
  export function buildHistoryListURL(params) {
28
35
  const url = new URL(`${MAIL_BASE_URL}/users/me/history`);
29
36
  url.searchParams.set('startHistoryId', params.startHistoryId);
@@ -82,9 +89,13 @@ function extractAttachments(payload) {
82
89
  const out = [];
83
90
  function walk(part) {
84
91
  if (part.filename != null && part.filename !== '' && part.body?.attachmentId != null) {
92
+ // providerRef is the Gmail part id used later to fetch bytes on demand
93
+ // (users.messages.attachments.get); the blob field stays absent until import.
85
94
  out.push({
86
95
  filename: part.filename,
87
- mimeType: part.mimeType ?? 'application/octet-stream'
96
+ mimeType: part.mimeType ?? 'application/octet-stream',
97
+ providerRef: part.body.attachmentId,
98
+ size: part.body.size ?? 0
88
99
  });
89
100
  }
90
101
  for (const child of part.parts ?? [])walk(child);
@@ -117,6 +128,51 @@ export function mapMessageToEntity(message) {
117
128
  }
118
129
  };
119
130
  }
131
+ function streamOfBytes(bytes) {
132
+ return new ReadableStream({
133
+ start (controller) {
134
+ controller.enqueue(bytes);
135
+ controller.close();
136
+ }
137
+ });
138
+ }
139
+ // Download one attachment's bytes from Gmail, write them to the blob store, and
140
+ // return the message entity with that attachment's `blob` set. Other attachments'
141
+ // already-imported blobs are preserved from `existing`. Gmail returns the whole
142
+ // attachment as a single base64url `data` field, so the bytes are buffered here.
143
+ export async function importMessageAttachment(config) {
144
+ const attachments = config.existing?.attachments ?? [];
145
+ const target = attachments.find((a)=>a.providerRef === config.providerRef);
146
+ if (target == null) {
147
+ throw new Error(`Google Mail: no attachment with providerRef "${config.providerRef}"`);
148
+ }
149
+ const fetchFn = config.fetch ?? globalThis.fetch;
150
+ const response = await fetchFn(buildAttachmentGetURL({
151
+ messageID: config.messageID,
152
+ attachmentID: config.providerRef
153
+ }), {
154
+ headers: {
155
+ Authorization: `Bearer ${config.accessToken}`
156
+ }
157
+ });
158
+ if (!response.ok) throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
159
+ const body = await response.json();
160
+ if (body.data == null) throw new Error('Google Mail: attachment response has no data');
161
+ const bytes = decodeBase64UrlBytes(body.data);
162
+ const attachmentID = await config.writeAttachment(streamOfBytes(bytes), {
163
+ mimeType: target.mimeType ?? 'application/octet-stream'
164
+ });
165
+ const updated = attachments.map((a)=>a.providerRef === config.providerRef ? {
166
+ ...a,
167
+ blob: attachmentID
168
+ } : a);
169
+ return {
170
+ ...config.existing ?? {},
171
+ sourceService: 'google-mail',
172
+ sourceID: config.messageID,
173
+ attachments: updated
174
+ };
175
+ }
120
176
  export function mapThreadToEntity(threadID, subject) {
121
177
  return {
122
178
  sourceService: 'google-mail',
package/lib/connector.js CHANGED
@@ -66,7 +66,7 @@ export function createGoogleMailConnector(options) {
66
66
  },
67
67
  {
68
68
  name: 'update',
69
- description: 'On GoogleMailMessage: add/remove labels. On GoogleMailLabel: rename or restyle the label.',
69
+ description: 'On GoogleMailMessage: add/remove labels, or import an attachment when providerRef is set. On GoogleMailLabel: rename or restyle the label.',
70
70
  input: {
71
71
  type: 'object',
72
72
  properties: {
@@ -83,6 +83,10 @@ export function createGoogleMailConnector(options) {
83
83
  type: 'string'
84
84
  }
85
85
  },
86
+ // Import the attachment identified by this Gmail part id into the blob store.
87
+ providerRef: {
88
+ type: 'string'
89
+ },
86
90
  // GoogleMailLabel
87
91
  name: {
88
92
  type: 'string'
@@ -107,7 +111,8 @@ export function createGoogleMailConnector(options) {
107
111
  actionHandler: (config)=>{
108
112
  return createGoogleMailActionHandler({
109
113
  credential: config.credential,
110
- fetch: options?.fetch
114
+ fetch: options?.fetch,
115
+ writeAttachment: config.writeAttachment
111
116
  });
112
117
  },
113
118
  auth: {
package/lib/model.d.ts CHANGED
@@ -37,7 +37,11 @@ export declare const googleMailThreadDefinition: {
37
37
  readonly minLength: 1;
38
38
  readonly maxLength: 500;
39
39
  };
40
- readonly SourceURL: import("json-schema-typed").JSONSchema.String;
40
+ readonly SourceURL: {
41
+ readonly type: "string";
42
+ readonly format: "uri";
43
+ readonly title: "URL";
44
+ };
41
45
  readonly DateTime: import("json-schema-typed").JSONSchema.String;
42
46
  };
43
47
  };
@@ -120,7 +124,11 @@ export declare const googleMailMessageDefinition: {
120
124
  readonly required: ["body", "sender", "threadID", "thread", "sourceService", "sourceID", "lastSyncedAt"];
121
125
  readonly additionalProperties: false;
122
126
  readonly definitions: {
123
- readonly ThreadDocID: import("json-schema-typed").JSONSchema.String;
127
+ readonly ThreadDocID: {
128
+ readonly type: "string";
129
+ readonly title: "DocID";
130
+ readonly pattern: "^k[0-9a-z]{10,120}$";
131
+ };
124
132
  readonly Attachment: {
125
133
  readonly type: "object";
126
134
  readonly title: "Attachment";
@@ -131,8 +139,18 @@ export declare const googleMailMessageDefinition: {
131
139
  readonly mimeType: {
132
140
  readonly type: "string";
133
141
  };
142
+ readonly providerRef: {
143
+ readonly type: "string";
144
+ };
145
+ readonly size: {
146
+ readonly type: "number";
147
+ };
148
+ readonly blob: {
149
+ readonly type: "string";
150
+ readonly title: "attachmentid";
151
+ };
134
152
  };
135
- readonly required: ["filename", "mimeType"];
153
+ readonly required: ["filename", "mimeType", "providerRef", "size"];
136
154
  readonly additionalProperties: false;
137
155
  };
138
156
  readonly SourceService: {
@@ -147,7 +165,11 @@ export declare const googleMailMessageDefinition: {
147
165
  readonly minLength: 1;
148
166
  readonly maxLength: 500;
149
167
  };
150
- readonly SourceURL: import("json-schema-typed").JSONSchema.String;
168
+ readonly SourceURL: {
169
+ readonly type: "string";
170
+ readonly format: "uri";
171
+ readonly title: "URL";
172
+ };
151
173
  readonly DateTime: import("json-schema-typed").JSONSchema.String;
152
174
  };
153
175
  };
@@ -218,7 +240,11 @@ export declare const googleMailLabelDefinition: {
218
240
  readonly minLength: 1;
219
241
  readonly maxLength: 500;
220
242
  };
221
- readonly SourceURL: import("json-schema-typed").JSONSchema.String;
243
+ readonly SourceURL: {
244
+ readonly type: "string";
245
+ readonly format: "uri";
246
+ readonly title: "URL";
247
+ };
222
248
  readonly DateTime: import("json-schema-typed").JSONSchema.String;
223
249
  };
224
250
  };
package/lib/model.js CHANGED
@@ -156,11 +156,25 @@ export const googleMailMessageDefinition = {
156
156
  },
157
157
  mimeType: {
158
158
  type: 'string'
159
+ },
160
+ // Gmail part id; the stable selector an import action uses to fetch bytes.
161
+ providerRef: {
162
+ type: 'string'
163
+ },
164
+ size: {
165
+ type: 'number'
166
+ },
167
+ // Content-addressed blob id, absent until an explicit import fills it.
168
+ blob: {
169
+ type: 'string',
170
+ title: 'attachmentid'
159
171
  }
160
172
  },
161
173
  required: [
162
174
  'filename',
163
- 'mimeType'
175
+ 'mimeType',
176
+ 'providerRef',
177
+ 'size'
164
178
  ],
165
179
  additionalProperties: false
166
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/connector-google-mail",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -15,11 +15,12 @@
15
15
  "LICENSE.md"
16
16
  ],
17
17
  "dependencies": {
18
- "@kubun/connector": "^0.12.1",
19
- "@kubun/models": "^0.12.1",
20
- "@kubun/provider-google": "^0.12.0",
18
+ "@sozai/codec": "^0.4.0",
19
+ "@kubun/connector": "^0.13.0",
20
+ "@kubun/models": "^0.13.0",
21
21
  "@kubun/protocol": "^0.13.0",
22
- "@kubun/scalars": "^0.12.0"
22
+ "@kubun/scalars": "^0.13.0",
23
+ "@kubun/provider-google": "^0.13.0"
23
24
  },
24
25
  "publishConfig": {
25
26
  "access": "public"