@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/LICENSE.md +57 -0
- package/lib/actions.d.ts +9 -0
- package/lib/actions.js +43 -0
- package/lib/api.d.ts +125 -0
- package/lib/api.js +313 -0
- package/lib/connector.d.ts +6 -0
- package/lib/connector.js +133 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +5 -0
- package/lib/model.d.ts +227 -0
- package/lib/model.js +245 -0
- package/lib/provider.d.ts +12 -0
- package/lib/provider.js +190 -0
- package/package.json +37 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# The Prosperity Public License 3.0.0
|
|
2
|
+
|
|
3
|
+
Contributor: Paul Le Cam
|
|
4
|
+
|
|
5
|
+
Source Code: https://github.com/PaulLeCam/kubun
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
This license allows you to use and share this software for noncommercial purposes for free and to try this software for commercial purposes for thirty days.
|
|
10
|
+
|
|
11
|
+
## Agreement
|
|
12
|
+
|
|
13
|
+
In order to receive this license, you have to agree to its rules. Those rules are both obligations under that agreement and conditions to your license. Don't do anything with this software that triggers a rule you can't or won't follow.
|
|
14
|
+
|
|
15
|
+
## Notices
|
|
16
|
+
|
|
17
|
+
Make sure everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license and the contributor and source code lines above.
|
|
18
|
+
|
|
19
|
+
## Commercial Trial
|
|
20
|
+
|
|
21
|
+
Limit your use of this software for commercial purposes to a thirty-day trial period. If you use this software for work, your company gets one trial period for all personnel, not one trial per person.
|
|
22
|
+
|
|
23
|
+
## Contributions Back
|
|
24
|
+
|
|
25
|
+
Developing feedback, changes, or additions that you contribute back to the contributor on the terms of a standardized public software license such as [the Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0), [the Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html), [the MIT license](https://spdx.org/licenses/MIT.html), or [the two-clause BSD license](https://spdx.org/licenses/BSD-2-Clause.html) doesn't count as use for a commercial purpose.
|
|
26
|
+
|
|
27
|
+
## Personal Uses
|
|
28
|
+
|
|
29
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, doesn't count as use for a commercial purpose.
|
|
30
|
+
|
|
31
|
+
## Noncommercial Organizations
|
|
32
|
+
|
|
33
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution doesn't count as use for a commercial purpose regardless of the source of funding or obligations resulting from the funding.
|
|
34
|
+
|
|
35
|
+
## Defense
|
|
36
|
+
|
|
37
|
+
Don't make any legal claim against anyone accusing this software, with or without changes, alone or with other technology, of infringing any patent.
|
|
38
|
+
|
|
39
|
+
## Copyright
|
|
40
|
+
|
|
41
|
+
The contributor licenses you to do everything with this software that would otherwise infringe their copyright in it.
|
|
42
|
+
|
|
43
|
+
## Patent
|
|
44
|
+
|
|
45
|
+
The contributor licenses you to do everything with this software that would otherwise infringe any patents they can license or become able to license.
|
|
46
|
+
|
|
47
|
+
## Reliability
|
|
48
|
+
|
|
49
|
+
The contributor can't revoke this license.
|
|
50
|
+
|
|
51
|
+
## Excuse
|
|
52
|
+
|
|
53
|
+
You're excused for unknowingly breaking [Notices](#notices) if you take all practical steps to comply within thirty days of learning you broke the rule.
|
|
54
|
+
|
|
55
|
+
## No Liability
|
|
56
|
+
|
|
57
|
+
***As far as the law allows, this software comes as is, without any warranty or condition, and the contributor won't be liable to anyone for any damages related to this software or this license, under any kind of legal claim.***
|
package/lib/actions.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ActionHandler } from '@kubun/connector';
|
|
2
|
+
export type GoogleMailActionHandlerConfig = {
|
|
3
|
+
credential: {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
scopes: Array<string>;
|
|
6
|
+
};
|
|
7
|
+
fetch?: typeof globalThis.fetch;
|
|
8
|
+
};
|
|
9
|
+
export declare function createGoogleMailActionHandler(config: GoogleMailActionHandlerConfig): ActionHandler;
|
package/lib/actions.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createDraft, createLabel, modifyLabels, sendMessage, updateLabel } from './api.js';
|
|
2
|
+
export function createGoogleMailActionHandler(config) {
|
|
3
|
+
return {
|
|
4
|
+
async create (model, params) {
|
|
5
|
+
if (model === 'GoogleMailLabel') {
|
|
6
|
+
return createLabel({
|
|
7
|
+
accessToken: config.credential.accessToken,
|
|
8
|
+
params,
|
|
9
|
+
fetch: config.fetch
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
if (params.draft === true) {
|
|
13
|
+
return createDraft({
|
|
14
|
+
accessToken: config.credential.accessToken,
|
|
15
|
+
params,
|
|
16
|
+
fetch: config.fetch
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return sendMessage({
|
|
20
|
+
accessToken: config.credential.accessToken,
|
|
21
|
+
params,
|
|
22
|
+
fetch: config.fetch
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
async update (model, sourceID, params) {
|
|
26
|
+
if (model === 'GoogleMailLabel') {
|
|
27
|
+
return updateLabel({
|
|
28
|
+
accessToken: config.credential.accessToken,
|
|
29
|
+
labelID: sourceID,
|
|
30
|
+
params,
|
|
31
|
+
fetch: config.fetch
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return modifyLabels({
|
|
35
|
+
accessToken: config.credential.accessToken,
|
|
36
|
+
messageID: sourceID,
|
|
37
|
+
addLabelIds: params.addLabelIds,
|
|
38
|
+
removeLabelIds: params.removeLabelIds,
|
|
39
|
+
fetch: config.fetch
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
package/lib/api.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { EntityRecord } from '@kubun/connector';
|
|
2
|
+
export declare const MAIL_BASE_URL = "https://gmail.googleapis.com/gmail/v1";
|
|
3
|
+
export declare function parseGoogleError(response: Response): Promise<string>;
|
|
4
|
+
export declare function decodeBase64Url(data: string): string;
|
|
5
|
+
export declare function encodeBase64Url(text: string): string;
|
|
6
|
+
export declare function buildMessagesListURL(params: {
|
|
7
|
+
query: string;
|
|
8
|
+
pageToken?: string;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function buildMessageGetURL(params: {
|
|
11
|
+
messageID: string;
|
|
12
|
+
}): string;
|
|
13
|
+
export declare function buildHistoryListURL(params: {
|
|
14
|
+
startHistoryId: string;
|
|
15
|
+
pageToken?: string;
|
|
16
|
+
}): string;
|
|
17
|
+
export declare function buildProfileURL(): string;
|
|
18
|
+
export declare function buildLabelsListURL(): string;
|
|
19
|
+
export type GmailLabelResource = {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
messageListVisibility?: string;
|
|
24
|
+
labelListVisibility?: string;
|
|
25
|
+
color?: {
|
|
26
|
+
textColor?: string;
|
|
27
|
+
backgroundColor?: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export type GmailLabelsListResponse = {
|
|
31
|
+
labels?: Array<GmailLabelResource>;
|
|
32
|
+
};
|
|
33
|
+
export declare function mapLabelToEntity(label: GmailLabelResource): EntityRecord;
|
|
34
|
+
export type GmailHistoryMessageRef = {
|
|
35
|
+
message: {
|
|
36
|
+
id: string;
|
|
37
|
+
threadId: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export type GmailHistoryRecord = {
|
|
41
|
+
id: string;
|
|
42
|
+
messagesAdded?: Array<GmailHistoryMessageRef>;
|
|
43
|
+
messagesDeleted?: Array<GmailHistoryMessageRef>;
|
|
44
|
+
labelsAdded?: Array<GmailHistoryMessageRef>;
|
|
45
|
+
labelsRemoved?: Array<GmailHistoryMessageRef>;
|
|
46
|
+
};
|
|
47
|
+
export type GmailHistoryListResponse = {
|
|
48
|
+
history?: Array<GmailHistoryRecord>;
|
|
49
|
+
nextPageToken?: string;
|
|
50
|
+
historyId?: string;
|
|
51
|
+
};
|
|
52
|
+
export type GmailMessagePart = {
|
|
53
|
+
mimeType?: string;
|
|
54
|
+
filename?: string;
|
|
55
|
+
headers?: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
value: string;
|
|
58
|
+
}>;
|
|
59
|
+
body?: {
|
|
60
|
+
size?: number;
|
|
61
|
+
data?: string;
|
|
62
|
+
attachmentId?: string;
|
|
63
|
+
};
|
|
64
|
+
parts?: Array<GmailMessagePart>;
|
|
65
|
+
};
|
|
66
|
+
export type GmailMessageResource = {
|
|
67
|
+
id: string;
|
|
68
|
+
threadId: string;
|
|
69
|
+
labelIds?: Array<string>;
|
|
70
|
+
snippet?: string;
|
|
71
|
+
historyId?: string;
|
|
72
|
+
internalDate?: string;
|
|
73
|
+
payload?: GmailMessagePart;
|
|
74
|
+
};
|
|
75
|
+
export type GmailMessagesListResponse = {
|
|
76
|
+
messages?: Array<{
|
|
77
|
+
id: string;
|
|
78
|
+
threadId: string;
|
|
79
|
+
}>;
|
|
80
|
+
nextPageToken?: string;
|
|
81
|
+
resultSizeEstimate?: number;
|
|
82
|
+
};
|
|
83
|
+
export declare function getHeader(headers: Array<{
|
|
84
|
+
name: string;
|
|
85
|
+
value: string;
|
|
86
|
+
}> | undefined, name: string): string | undefined;
|
|
87
|
+
export declare function extractEmail(headerValue: string): string;
|
|
88
|
+
export declare function extractEmails(headerValue: string | undefined): Array<string>;
|
|
89
|
+
export declare function mapMessageToEntity(message: GmailMessageResource): EntityRecord;
|
|
90
|
+
export declare function mapThreadToEntity(threadID: string, subject: string | undefined): EntityRecord;
|
|
91
|
+
export declare function buildRawMessage(params: {
|
|
92
|
+
to: string | Array<string>;
|
|
93
|
+
subject: string;
|
|
94
|
+
body: string;
|
|
95
|
+
cc?: string | Array<string>;
|
|
96
|
+
bcc?: string | Array<string>;
|
|
97
|
+
}): string;
|
|
98
|
+
export declare function sendMessage(config: {
|
|
99
|
+
accessToken: string;
|
|
100
|
+
params: Record<string, unknown>;
|
|
101
|
+
fetch?: typeof globalThis.fetch;
|
|
102
|
+
}): Promise<EntityRecord>;
|
|
103
|
+
export declare function createDraft(config: {
|
|
104
|
+
accessToken: string;
|
|
105
|
+
params: Record<string, unknown>;
|
|
106
|
+
fetch?: typeof globalThis.fetch;
|
|
107
|
+
}): Promise<EntityRecord>;
|
|
108
|
+
export declare function modifyLabels(config: {
|
|
109
|
+
accessToken: string;
|
|
110
|
+
messageID: string;
|
|
111
|
+
addLabelIds?: Array<string>;
|
|
112
|
+
removeLabelIds?: Array<string>;
|
|
113
|
+
fetch?: typeof globalThis.fetch;
|
|
114
|
+
}): Promise<EntityRecord>;
|
|
115
|
+
export declare function createLabel(config: {
|
|
116
|
+
accessToken: string;
|
|
117
|
+
params: Record<string, unknown>;
|
|
118
|
+
fetch?: typeof globalThis.fetch;
|
|
119
|
+
}): Promise<EntityRecord>;
|
|
120
|
+
export declare function updateLabel(config: {
|
|
121
|
+
accessToken: string;
|
|
122
|
+
labelID: string;
|
|
123
|
+
params: Record<string, unknown>;
|
|
124
|
+
fetch?: typeof globalThis.fetch;
|
|
125
|
+
}): Promise<EntityRecord>;
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
export const MAIL_BASE_URL = 'https://gmail.googleapis.com/gmail/v1';
|
|
2
|
+
export async function parseGoogleError(response) {
|
|
3
|
+
try {
|
|
4
|
+
const body = await response.json();
|
|
5
|
+
return body.error?.message ?? `${response.status} ${response.statusText}`;
|
|
6
|
+
} catch {
|
|
7
|
+
return `${response.status} ${response.statusText}`;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function decodeBase64Url(data) {
|
|
11
|
+
return Buffer.from(data, 'base64url').toString('utf8');
|
|
12
|
+
}
|
|
13
|
+
export function encodeBase64Url(text) {
|
|
14
|
+
return Buffer.from(text, 'utf8').toString('base64url');
|
|
15
|
+
}
|
|
16
|
+
export function buildMessagesListURL(params) {
|
|
17
|
+
const url = new URL(`${MAIL_BASE_URL}/users/me/messages`);
|
|
18
|
+
url.searchParams.set('q', params.query);
|
|
19
|
+
if (params.pageToken != null) url.searchParams.set('pageToken', params.pageToken);
|
|
20
|
+
return url.toString();
|
|
21
|
+
}
|
|
22
|
+
export function buildMessageGetURL(params) {
|
|
23
|
+
const url = new URL(`${MAIL_BASE_URL}/users/me/messages/${encodeURIComponent(params.messageID)}`);
|
|
24
|
+
url.searchParams.set('format', 'full');
|
|
25
|
+
return url.toString();
|
|
26
|
+
}
|
|
27
|
+
export function buildHistoryListURL(params) {
|
|
28
|
+
const url = new URL(`${MAIL_BASE_URL}/users/me/history`);
|
|
29
|
+
url.searchParams.set('startHistoryId', params.startHistoryId);
|
|
30
|
+
if (params.pageToken != null) url.searchParams.set('pageToken', params.pageToken);
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
export function buildProfileURL() {
|
|
34
|
+
return `${MAIL_BASE_URL}/users/me/profile`;
|
|
35
|
+
}
|
|
36
|
+
export function buildLabelsListURL() {
|
|
37
|
+
return `${MAIL_BASE_URL}/users/me/labels`;
|
|
38
|
+
}
|
|
39
|
+
export function mapLabelToEntity(label) {
|
|
40
|
+
return {
|
|
41
|
+
sourceService: 'google-mail',
|
|
42
|
+
sourceID: label.id,
|
|
43
|
+
sourceURL: `https://mail.google.com/mail/u/0/#label/${encodeURIComponent(label.name)}`,
|
|
44
|
+
name: label.name,
|
|
45
|
+
type: label.type,
|
|
46
|
+
messageListVisibility: label.messageListVisibility,
|
|
47
|
+
labelListVisibility: label.labelListVisibility,
|
|
48
|
+
textColor: label.color?.textColor,
|
|
49
|
+
backgroundColor: label.color?.backgroundColor
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function getHeader(headers, name) {
|
|
53
|
+
const lower = name.toLowerCase();
|
|
54
|
+
return headers?.find((h)=>h.name.toLowerCase() === lower)?.value;
|
|
55
|
+
}
|
|
56
|
+
export function extractEmail(headerValue) {
|
|
57
|
+
const match = /<([^>]+)>/.exec(headerValue);
|
|
58
|
+
return (match != null ? match[1] : headerValue).trim();
|
|
59
|
+
}
|
|
60
|
+
export function extractEmails(headerValue) {
|
|
61
|
+
if (headerValue == null || headerValue.trim() === '') return [];
|
|
62
|
+
return headerValue.split(',').map((part)=>extractEmail(part));
|
|
63
|
+
}
|
|
64
|
+
function findPartByMime(part, mime) {
|
|
65
|
+
if (part.mimeType === mime && part.body?.data != null) return part;
|
|
66
|
+
for (const child of part.parts ?? []){
|
|
67
|
+
const found = findPartByMime(child, mime);
|
|
68
|
+
if (found != null) return found;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function extractBody(payload) {
|
|
73
|
+
if (payload == null) return '';
|
|
74
|
+
const plain = findPartByMime(payload, 'text/plain');
|
|
75
|
+
if (plain?.body?.data != null) return decodeBase64Url(plain.body.data);
|
|
76
|
+
const html = findPartByMime(payload, 'text/html');
|
|
77
|
+
if (html?.body?.data != null) return decodeBase64Url(html.body.data);
|
|
78
|
+
if (payload.body?.data != null) return decodeBase64Url(payload.body.data);
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
function extractAttachments(payload) {
|
|
82
|
+
const out = [];
|
|
83
|
+
function walk(part) {
|
|
84
|
+
if (part.filename != null && part.filename !== '' && part.body?.attachmentId != null) {
|
|
85
|
+
out.push({
|
|
86
|
+
filename: part.filename,
|
|
87
|
+
mimeType: part.mimeType ?? 'application/octet-stream'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
for (const child of part.parts ?? [])walk(child);
|
|
91
|
+
}
|
|
92
|
+
walk(payload ?? {});
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
export function mapMessageToEntity(message) {
|
|
96
|
+
const headers = message.payload?.headers;
|
|
97
|
+
const labels = message.labelIds ?? [];
|
|
98
|
+
const sentAtMs = message.internalDate != null ? Number.parseInt(message.internalDate, 10) : Number.NaN;
|
|
99
|
+
return {
|
|
100
|
+
sourceService: 'google-mail',
|
|
101
|
+
sourceID: message.id,
|
|
102
|
+
sourceURL: `https://mail.google.com/mail/u/0/#inbox/${message.id}`,
|
|
103
|
+
body: extractBody(message.payload),
|
|
104
|
+
sender: extractEmail(getHeader(headers, 'From') ?? ''),
|
|
105
|
+
subject: getHeader(headers, 'Subject'),
|
|
106
|
+
snippet: message.snippet,
|
|
107
|
+
to: extractEmails(getHeader(headers, 'To')),
|
|
108
|
+
cc: extractEmails(getHeader(headers, 'Cc')),
|
|
109
|
+
labels,
|
|
110
|
+
unread: labels.includes('UNREAD'),
|
|
111
|
+
sentAt: Number.isNaN(sentAtMs) ? undefined : new Date(sentAtMs).toISOString(),
|
|
112
|
+
attachments: extractAttachments(message.payload),
|
|
113
|
+
threadID: message.threadId,
|
|
114
|
+
thread: {
|
|
115
|
+
sourceService: 'google-mail',
|
|
116
|
+
sourceID: message.threadId
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function mapThreadToEntity(threadID, subject) {
|
|
121
|
+
return {
|
|
122
|
+
sourceService: 'google-mail',
|
|
123
|
+
sourceID: threadID,
|
|
124
|
+
sourceURL: `https://mail.google.com/mail/u/0/#inbox/${threadID}`,
|
|
125
|
+
title: subject
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function requireNoNewline(value) {
|
|
129
|
+
if (value.includes('\r') || value.includes('\n')) {
|
|
130
|
+
throw new Error('Google Mail: header field contains illegal newline');
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
// Recipients arrive either as an array (GraphQL JSON) or a comma-separated string.
|
|
135
|
+
// Normalize to a trimmed, non-empty list; `to` must resolve to at least one address.
|
|
136
|
+
function normalizeRecipients(value, key, required) {
|
|
137
|
+
let list;
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
list = value.map((v)=>String(v));
|
|
140
|
+
} else if (typeof value === 'string') {
|
|
141
|
+
list = value.split(',');
|
|
142
|
+
} else if (value == null) {
|
|
143
|
+
list = [];
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error(`Google Mail: "${key}" must be a string or array of strings`);
|
|
146
|
+
}
|
|
147
|
+
const cleaned = list.map((s)=>s.trim()).filter((s)=>s !== '');
|
|
148
|
+
if (required && cleaned.length === 0) {
|
|
149
|
+
throw new Error(`Google Mail: "${key}" is required`);
|
|
150
|
+
}
|
|
151
|
+
return cleaned;
|
|
152
|
+
}
|
|
153
|
+
export function buildRawMessage(params) {
|
|
154
|
+
const to = normalizeRecipients(params.to, 'to', true).map(requireNoNewline);
|
|
155
|
+
const cc = normalizeRecipients(params.cc, 'cc', false).map(requireNoNewline);
|
|
156
|
+
const bcc = normalizeRecipients(params.bcc, 'bcc', false).map(requireNoNewline);
|
|
157
|
+
const lines = [
|
|
158
|
+
`To: ${to.join(', ')}`
|
|
159
|
+
];
|
|
160
|
+
if (cc.length > 0) lines.push(`Cc: ${cc.join(', ')}`);
|
|
161
|
+
if (bcc.length > 0) lines.push(`Bcc: ${bcc.join(', ')}`);
|
|
162
|
+
lines.push(`Subject: ${requireNoNewline(params.subject)}`, 'Content-Type: text/plain; charset="UTF-8"', 'MIME-Version: 1.0', '', params.body);
|
|
163
|
+
return encodeBase64Url(lines.join('\r\n'));
|
|
164
|
+
}
|
|
165
|
+
function requireString(params, key) {
|
|
166
|
+
const value = params[key];
|
|
167
|
+
if (typeof value !== 'string' || value === '') {
|
|
168
|
+
throw new Error(`Google Mail: "${key}" is required`);
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
// Gmail's write endpoints (messages.send, drafts.create, messages.modify) return only
|
|
173
|
+
// { id, threadId, labelIds } -- no payload/headers/body. Re-fetch the full message so the
|
|
174
|
+
// mapped entity carries real sender/body/subject/etc. instead of empty placeholders.
|
|
175
|
+
async function fetchAndMapMessage(config) {
|
|
176
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
177
|
+
const response = await fetchFn(buildMessageGetURL({
|
|
178
|
+
messageID: config.messageID
|
|
179
|
+
}), {
|
|
180
|
+
headers: {
|
|
181
|
+
Authorization: `Bearer ${config.accessToken}`
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
|
|
185
|
+
return mapMessageToEntity(await response.json());
|
|
186
|
+
}
|
|
187
|
+
export async function sendMessage(config) {
|
|
188
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
189
|
+
const raw = buildRawMessage({
|
|
190
|
+
to: config.params.to,
|
|
191
|
+
cc: config.params.cc,
|
|
192
|
+
bcc: config.params.bcc,
|
|
193
|
+
subject: requireString(config.params, 'subject'),
|
|
194
|
+
body: requireString(config.params, 'body')
|
|
195
|
+
});
|
|
196
|
+
const response = await fetchFn(`${MAIL_BASE_URL}/users/me/messages/send`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
200
|
+
'Content-Type': 'application/json'
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
raw
|
|
204
|
+
})
|
|
205
|
+
});
|
|
206
|
+
if (!response.ok) throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
|
|
207
|
+
const sent = await response.json();
|
|
208
|
+
return fetchAndMapMessage({
|
|
209
|
+
accessToken: config.accessToken,
|
|
210
|
+
messageID: sent.id,
|
|
211
|
+
fetch: config.fetch
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
export async function createDraft(config) {
|
|
215
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
216
|
+
const raw = buildRawMessage({
|
|
217
|
+
to: config.params.to,
|
|
218
|
+
cc: config.params.cc,
|
|
219
|
+
bcc: config.params.bcc,
|
|
220
|
+
subject: requireString(config.params, 'subject'),
|
|
221
|
+
body: requireString(config.params, 'body')
|
|
222
|
+
});
|
|
223
|
+
const response = await fetchFn(`${MAIL_BASE_URL}/users/me/drafts`, {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: {
|
|
226
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
227
|
+
'Content-Type': 'application/json'
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
message: {
|
|
231
|
+
raw
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
|
|
236
|
+
const draft = await response.json();
|
|
237
|
+
return fetchAndMapMessage({
|
|
238
|
+
accessToken: config.accessToken,
|
|
239
|
+
messageID: draft.message.id,
|
|
240
|
+
fetch: config.fetch
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
export async function modifyLabels(config) {
|
|
244
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
245
|
+
const url = `${MAIL_BASE_URL}/users/me/messages/${encodeURIComponent(config.messageID)}/modify`;
|
|
246
|
+
const response = await fetchFn(url, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: {
|
|
249
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
250
|
+
'Content-Type': 'application/json'
|
|
251
|
+
},
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
addLabelIds: config.addLabelIds ?? [],
|
|
254
|
+
removeLabelIds: config.removeLabelIds ?? []
|
|
255
|
+
})
|
|
256
|
+
});
|
|
257
|
+
if (!response.ok) throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
|
|
258
|
+
const modified = await response.json();
|
|
259
|
+
return fetchAndMapMessage({
|
|
260
|
+
accessToken: config.accessToken,
|
|
261
|
+
messageID: modified.id,
|
|
262
|
+
fetch: config.fetch
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
// Assemble the Gmail Label write body from an action's params, sending only the
|
|
266
|
+
// fields the API accepts. `color` requires BOTH textColor and backgroundColor and
|
|
267
|
+
// only accepts values from Gmail's fixed palette; send it only when both are given.
|
|
268
|
+
function buildLabelBody(params) {
|
|
269
|
+
const body = {};
|
|
270
|
+
if (typeof params.name === 'string') body.name = requireNoNewline(params.name);
|
|
271
|
+
if (typeof params.messageListVisibility === 'string') {
|
|
272
|
+
body.messageListVisibility = params.messageListVisibility;
|
|
273
|
+
}
|
|
274
|
+
if (typeof params.labelListVisibility === 'string') {
|
|
275
|
+
body.labelListVisibility = params.labelListVisibility;
|
|
276
|
+
}
|
|
277
|
+
if (typeof params.textColor === 'string' && typeof params.backgroundColor === 'string') {
|
|
278
|
+
body.color = {
|
|
279
|
+
textColor: params.textColor,
|
|
280
|
+
backgroundColor: params.backgroundColor
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return body;
|
|
284
|
+
}
|
|
285
|
+
export async function createLabel(config) {
|
|
286
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
287
|
+
const body = buildLabelBody(config.params);
|
|
288
|
+
if (typeof body.name !== 'string') throw new Error('Google Mail: "name" is required');
|
|
289
|
+
const response = await fetchFn(buildLabelsListURL(), {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: {
|
|
292
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
293
|
+
'Content-Type': 'application/json'
|
|
294
|
+
},
|
|
295
|
+
body: JSON.stringify(body)
|
|
296
|
+
});
|
|
297
|
+
if (!response.ok) throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
|
|
298
|
+
return mapLabelToEntity(await response.json());
|
|
299
|
+
}
|
|
300
|
+
export async function updateLabel(config) {
|
|
301
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
302
|
+
const url = `${MAIL_BASE_URL}/users/me/labels/${encodeURIComponent(config.labelID)}`;
|
|
303
|
+
const response = await fetchFn(url, {
|
|
304
|
+
method: 'PATCH',
|
|
305
|
+
headers: {
|
|
306
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
307
|
+
'Content-Type': 'application/json'
|
|
308
|
+
},
|
|
309
|
+
body: JSON.stringify(buildLabelBody(config.params))
|
|
310
|
+
});
|
|
311
|
+
if (!response.ok) throw new Error(`Google Mail API error: ${await parseGoogleError(response)}`);
|
|
312
|
+
return mapLabelToEntity(await response.json());
|
|
313
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ConnectorDefinition } from '@kubun/connector';
|
|
2
|
+
export type CreateGoogleMailConnectorOptions = {
|
|
3
|
+
fetch?: typeof globalThis.fetch;
|
|
4
|
+
query?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function createGoogleMailConnector(options?: CreateGoogleMailConnectorOptions): ConnectorDefinition;
|
package/lib/connector.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createGoogleMailActionHandler } from './actions.js';
|
|
2
|
+
import { googleMailClusters } from './model.js';
|
|
3
|
+
import { createGoogleMailProvider, DEFAULT_QUERY } from './provider.js';
|
|
4
|
+
export function createGoogleMailConnector(options) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'google-mail',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
clusters: googleMailClusters,
|
|
9
|
+
syncStrategies: [
|
|
10
|
+
'on-demand'
|
|
11
|
+
],
|
|
12
|
+
actions: [
|
|
13
|
+
{
|
|
14
|
+
name: 'create',
|
|
15
|
+
description: 'On GoogleMailMessage: send a message (or create a draft when draft is true). On GoogleMailLabel: create a label.',
|
|
16
|
+
input: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
// GoogleMailMessage
|
|
20
|
+
to: {
|
|
21
|
+
type: 'array',
|
|
22
|
+
items: {
|
|
23
|
+
type: 'string'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
cc: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
items: {
|
|
29
|
+
type: 'string'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
bcc: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: {
|
|
35
|
+
type: 'string'
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
subject: {
|
|
39
|
+
type: 'string'
|
|
40
|
+
},
|
|
41
|
+
body: {
|
|
42
|
+
type: 'string'
|
|
43
|
+
},
|
|
44
|
+
draft: {
|
|
45
|
+
type: 'boolean'
|
|
46
|
+
},
|
|
47
|
+
// GoogleMailLabel
|
|
48
|
+
name: {
|
|
49
|
+
type: 'string'
|
|
50
|
+
},
|
|
51
|
+
messageListVisibility: {
|
|
52
|
+
type: 'string'
|
|
53
|
+
},
|
|
54
|
+
labelListVisibility: {
|
|
55
|
+
type: 'string'
|
|
56
|
+
},
|
|
57
|
+
textColor: {
|
|
58
|
+
type: 'string'
|
|
59
|
+
},
|
|
60
|
+
backgroundColor: {
|
|
61
|
+
type: 'string'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
requiresCredential: true
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'update',
|
|
69
|
+
description: 'On GoogleMailMessage: add/remove labels. On GoogleMailLabel: rename or restyle the label.',
|
|
70
|
+
input: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
// GoogleMailMessage
|
|
74
|
+
addLabelIds: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
items: {
|
|
77
|
+
type: 'string'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
removeLabelIds: {
|
|
81
|
+
type: 'array',
|
|
82
|
+
items: {
|
|
83
|
+
type: 'string'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
// GoogleMailLabel
|
|
87
|
+
name: {
|
|
88
|
+
type: 'string'
|
|
89
|
+
},
|
|
90
|
+
messageListVisibility: {
|
|
91
|
+
type: 'string'
|
|
92
|
+
},
|
|
93
|
+
labelListVisibility: {
|
|
94
|
+
type: 'string'
|
|
95
|
+
},
|
|
96
|
+
textColor: {
|
|
97
|
+
type: 'string'
|
|
98
|
+
},
|
|
99
|
+
backgroundColor: {
|
|
100
|
+
type: 'string'
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
requiresCredential: true
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
actionHandler: (config)=>{
|
|
108
|
+
return createGoogleMailActionHandler({
|
|
109
|
+
credential: config.credential,
|
|
110
|
+
fetch: options?.fetch
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
auth: {
|
|
114
|
+
provider: 'google',
|
|
115
|
+
scopes: [
|
|
116
|
+
'https://www.googleapis.com/auth/gmail.readonly'
|
|
117
|
+
],
|
|
118
|
+
writeScopes: [
|
|
119
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
120
|
+
'https://www.googleapis.com/auth/gmail.compose',
|
|
121
|
+
'https://www.googleapis.com/auth/gmail.modify'
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
serverProvider: (config)=>{
|
|
125
|
+
return createGoogleMailProvider({
|
|
126
|
+
credential: config.credential,
|
|
127
|
+
boundary: config.boundary,
|
|
128
|
+
fetch: options?.fetch,
|
|
129
|
+
query: options?.query ?? DEFAULT_QUERY
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|