@iola_adm/iola-cli 0.2.29 → 0.2.31
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/package.json
CHANGED
|
@@ -21,21 +21,30 @@ description: Сервисы Яндекса через Yandex Connector: ID, Ди
|
|
|
21
21
|
- `yandex_disk_unshare` - снять публичную ссылку.
|
|
22
22
|
- `yandex_disk_delete` - удалить файл или папку.
|
|
23
23
|
- `yandex_mail_status` - проверить доступ к Яндекс Почте.
|
|
24
|
+
- `yandex_mail_folders` - показать папки Яндекс Почты.
|
|
24
25
|
- `yandex_mail_list` - показать последние письма.
|
|
25
26
|
- `yandex_mail_search` - найти письма.
|
|
26
|
-
- `yandex_mail_read` - прочитать письмо по UID
|
|
27
|
+
- `yandex_mail_read` - прочитать письмо по UID; обычное чтение помечает письмо прочитанным.
|
|
27
28
|
- `yandex_mail_send` - отправить письмо.
|
|
29
|
+
- `yandex_mail_reply` - ответить на письмо по UID.
|
|
30
|
+
- `yandex_mail_delete` - переместить письмо в корзину.
|
|
31
|
+
- `yandex_mail_mark` - пометить письмо прочитанным или непрочитанным.
|
|
32
|
+
- Автоопрос почты включается командой `iola yandex mail-watch on --minutes 5`, выключается `iola yandex mail-watch off`, ручная проверка `iola yandex mail-watch tick`.
|
|
28
33
|
- `yandex_calendar_status` - проверить доступ к Яндекс Календарю.
|
|
29
34
|
- `yandex_calendar_list` - показать события.
|
|
30
35
|
- `yandex_calendar_create_event` - создать событие.
|
|
31
36
|
- `yandex_contacts_status` - проверить доступ к Яндекс Контактам.
|
|
32
37
|
- `yandex_contacts_list` - показать контакты.
|
|
33
38
|
- `yandex_contacts_search` - найти контакт.
|
|
39
|
+
- `yandex_contacts_add_email` - добавить email в существующий контакт.
|
|
34
40
|
- `yandex_telemost_create_event` - создать календарное событие для встречи.
|
|
35
41
|
|
|
36
42
|
Безопасность:
|
|
37
43
|
|
|
38
|
-
- Для отправки письма, удаления файлов, публикации ссылки и создания событий нужен явный запрос пользователя и `confirm=true`.
|
|
39
|
-
- Не отправляй письма, не удаляй
|
|
44
|
+
- Для отправки письма, ответа на письмо, удаления письма, удаления файлов, публикации ссылки и создания событий нужен явный запрос пользователя и `confirm=true`.
|
|
45
|
+
- Не отправляй письма, не отвечай на письма, не удаляй письма/файлы и не публикуй ссылки по косвенному намерению.
|
|
46
|
+
- Список писем и поиск не должны помечать письма прочитанными. Чтение письма по просьбе пользователя помечает письмо прочитанным.
|
|
47
|
+
- Если пользователь просит отправить письмо по имени контакта, сначала ищи контакт. Если контактов несколько, покажи варианты и попроси уточнить. Если контакта нет или у него нет email, скажи прямо.
|
|
48
|
+
- Если пользователь отправил письмо на введенный email и указал имя контакта, можно предложить добавить email в найденный контакт без email.
|
|
40
49
|
- Не выводи OAuth-токены, API-ключи, пароли и секреты.
|
|
41
50
|
- Если сервис не подключен, скажи запустить `iola yandex setup` или открыть `/master`.
|
package/src/cli.js
CHANGED
|
@@ -164,16 +164,21 @@ const YANDEX_TOOLS = [
|
|
|
164
164
|
"yandex_disk_unshare",
|
|
165
165
|
"yandex_disk_delete",
|
|
166
166
|
"yandex_mail_status",
|
|
167
|
+
"yandex_mail_folders",
|
|
167
168
|
"yandex_mail_list",
|
|
168
169
|
"yandex_mail_search",
|
|
169
170
|
"yandex_mail_read",
|
|
170
171
|
"yandex_mail_send",
|
|
172
|
+
"yandex_mail_reply",
|
|
173
|
+
"yandex_mail_delete",
|
|
174
|
+
"yandex_mail_mark",
|
|
171
175
|
"yandex_calendar_status",
|
|
172
176
|
"yandex_calendar_create_event",
|
|
173
177
|
"yandex_calendar_list",
|
|
174
178
|
"yandex_contacts_status",
|
|
175
179
|
"yandex_contacts_list",
|
|
176
180
|
"yandex_contacts_search",
|
|
181
|
+
"yandex_contacts_add_email",
|
|
177
182
|
"yandex_telemost_create_event",
|
|
178
183
|
];
|
|
179
184
|
const ALL_LOCAL_TOOLS = [...LOCAL_TOOLS, ...FILE_TOOLS, ...YANDEX_TOOLS];
|
|
@@ -3304,6 +3309,11 @@ async function handleYandex(args) {
|
|
|
3304
3309
|
return;
|
|
3305
3310
|
}
|
|
3306
3311
|
|
|
3312
|
+
if (action === "mail-watch" || action === "mailwatch" || action === "watch-mail") {
|
|
3313
|
+
await handleYandexMailWatch([target, ...rest].filter(Boolean));
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3307
3317
|
if (action === "enable" || action === "disable") {
|
|
3308
3318
|
const services = [target, ...rest].filter((item) => item && !String(item).startsWith("--"));
|
|
3309
3319
|
if (services.length === 0) throw new Error("Укажите сервисы. Пример: iola yandex enable disk mail calendar");
|
|
@@ -3339,6 +3349,7 @@ async function handleYandex(args) {
|
|
|
3339
3349
|
iola yandex menu
|
|
3340
3350
|
iola yandex status|doctor
|
|
3341
3351
|
iola yandex services
|
|
3352
|
+
iola yandex mail-watch on|off|status|tick [--minutes 5]
|
|
3342
3353
|
iola yandex enable disk mail calendar
|
|
3343
3354
|
iola yandex disable mail
|
|
3344
3355
|
iola yandex oauth-url [disk mail calendar] [--client-id ID] [--open]
|
|
@@ -3368,6 +3379,47 @@ function printYandexServices(options = {}) {
|
|
|
3368
3379
|
]);
|
|
3369
3380
|
}
|
|
3370
3381
|
|
|
3382
|
+
async function handleYandexMailWatch(args = []) {
|
|
3383
|
+
const [action = "status", ...rest] = args;
|
|
3384
|
+
const options = parseOptions(rest);
|
|
3385
|
+
if (action === "on" || action === "enable" || action === "start" || action === "вкл") {
|
|
3386
|
+
const minutes = Math.max(1, Number(options.minutes || options.interval || rest.find((item) => /^\d+$/u.test(String(item))) || 5));
|
|
3387
|
+
const result = await yandexMailWatchEnable(minutes);
|
|
3388
|
+
console.log(`Автопроверка новых писем включена: каждые ${minutes} минут. Текущий последний UID: ${result.lastUid || "-"}.`);
|
|
3389
|
+
console.log("Для работы по расписанию должен запускаться cron tick: вручную, через daemon или Windows Task Scheduler.");
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
if (action === "off" || action === "disable" || action === "stop" || action === "выкл") {
|
|
3393
|
+
await yandexMailWatchDisable();
|
|
3394
|
+
console.log("Автопроверка новых писем выключена.");
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
if (action === "tick" || action === "run" || action === "check") {
|
|
3398
|
+
const result = await yandexMailWatchTick(options);
|
|
3399
|
+
if (!result.enabled) {
|
|
3400
|
+
console.log("Автопроверка новых писем выключена.");
|
|
3401
|
+
} else if (!result.newMessages.length) {
|
|
3402
|
+
console.log("Новых писем нет.");
|
|
3403
|
+
} else {
|
|
3404
|
+
console.log(["Новые письма:", ...result.newMessages.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n"));
|
|
3405
|
+
}
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
if (action === "status" || action === "doctor") {
|
|
3409
|
+
const config = await loadConfig();
|
|
3410
|
+
const watch = config.yandex?.mailWatch || {};
|
|
3411
|
+
printKeyValue({
|
|
3412
|
+
enabled: watch.enabled ? "yes" : "no",
|
|
3413
|
+
minutes: watch.minutes || "-",
|
|
3414
|
+
mailbox: watch.mailbox || "INBOX",
|
|
3415
|
+
lastUid: watch.lastUid || "-",
|
|
3416
|
+
cron: listCronJobs().some((job) => job.command === "yandex mail-watch tick") ? "yes" : "no",
|
|
3417
|
+
});
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
throw new Error("Команды: iola yandex mail-watch on --minutes 5 | off | status | tick");
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3371
3423
|
async function setupYandexConnector(args = []) {
|
|
3372
3424
|
const options = parseOptions(args);
|
|
3373
3425
|
const config = await loadConfig();
|
|
@@ -3949,16 +4001,21 @@ async function executeYandexTool(tool, args = {}) {
|
|
|
3949
4001
|
if (tool === "yandex_disk_unshare") return yandexDiskUnshare(args.remotePath || args.path);
|
|
3950
4002
|
if (tool === "yandex_disk_delete") return yandexDiskDelete(args.remotePath || args.path, args);
|
|
3951
4003
|
if (tool === "yandex_mail_status") return yandexMailStatus();
|
|
3952
|
-
if (tool === "
|
|
3953
|
-
if (tool === "
|
|
3954
|
-
if (tool === "
|
|
4004
|
+
if (tool === "yandex_mail_folders") return yandexMailFolders();
|
|
4005
|
+
if (tool === "yandex_mail_list") return yandexMailList({ mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), limit: args.limit || 10, unread: Boolean(args.unread) });
|
|
4006
|
+
if (tool === "yandex_mail_search") return yandexMailSearch(args.query || "", { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), limit: args.limit || 20 });
|
|
4007
|
+
if (tool === "yandex_mail_read") return yandexMailRead(args.uid || args.id, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX"), markSeen: args.markSeen !== false });
|
|
3955
4008
|
if (tool === "yandex_mail_send") return yandexMailSend(args);
|
|
4009
|
+
if (tool === "yandex_mail_reply") return yandexMailReply(args);
|
|
4010
|
+
if (tool === "yandex_mail_delete") return yandexMailDelete(args.uid || args.id, { ...args, mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX") });
|
|
4011
|
+
if (tool === "yandex_mail_mark") return yandexMailMark(args.uid || args.id, args.seen !== false && args.unread !== true, { mailbox: await resolveYandexMailbox(args.mailbox || args.folder || "INBOX") });
|
|
3956
4012
|
if (tool === "yandex_calendar_status") return yandexCalendarStatus();
|
|
3957
4013
|
if (tool === "yandex_calendar_create_event") return yandexCalendarCreateEvent(args);
|
|
3958
4014
|
if (tool === "yandex_calendar_list") return yandexCalendarList(args);
|
|
3959
4015
|
if (tool === "yandex_contacts_status") return yandexContactsStatus();
|
|
3960
4016
|
if (tool === "yandex_contacts_list") return yandexContactsList(args);
|
|
3961
4017
|
if (tool === "yandex_contacts_search") return yandexContactsSearch(args.query || "", args);
|
|
4018
|
+
if (tool === "yandex_contacts_add_email") return yandexContactsAddEmail(args.query || args.name || "", args.email, args);
|
|
3962
4019
|
if (tool === "yandex_telemost_create_event") return yandexTelemostCreateEvent(args);
|
|
3963
4020
|
throw new Error(`Yandex tool неизвестен: ${tool}`);
|
|
3964
4021
|
}
|
|
@@ -3985,6 +4042,62 @@ async function yandexMailStatus() {
|
|
|
3985
4042
|
}
|
|
3986
4043
|
}
|
|
3987
4044
|
|
|
4045
|
+
async function yandexMailFolders() {
|
|
4046
|
+
const { token, email } = await yandexMailCredentials();
|
|
4047
|
+
const session = await imapConnect();
|
|
4048
|
+
try {
|
|
4049
|
+
await imapAuthenticate(session, email, token);
|
|
4050
|
+
const text = await imapCommand(session, 'LIST "" "*"');
|
|
4051
|
+
return parseImapListMailboxes(text);
|
|
4052
|
+
} finally {
|
|
4053
|
+
await imapClose(session);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
async function resolveYandexMailbox(value = "INBOX") {
|
|
4058
|
+
const requested = String(value || "INBOX").trim();
|
|
4059
|
+
if (!requested || /^inbox$/iu.test(requested)) return "INBOX";
|
|
4060
|
+
if (!isYandexMailboxAlias(requested)) return requested;
|
|
4061
|
+
const folders = await yandexMailFolders();
|
|
4062
|
+
const target = normalizeYandexMailboxAlias(requested);
|
|
4063
|
+
const found = findYandexMailbox(folders, target);
|
|
4064
|
+
if (found?.name) return found.name;
|
|
4065
|
+
throw new Error(`Папка Яндекс Почты не найдена: ${requested}. Проверьте список папок командой: покажи папки почты.`);
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
function isYandexMailboxAlias(value) {
|
|
4069
|
+
return /^(sent|send|sentmail|отправ|исход|draft|drafts|чернов|spam|junk|спам|trash|bin|корзин|удален|удалён)$/iu.test(String(value || "").trim());
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
function normalizeYandexMailboxAlias(value) {
|
|
4073
|
+
const text = String(value || "").toLocaleLowerCase("ru-RU");
|
|
4074
|
+
if (/(sent|send|sentmail|отправ|исход)/iu.test(text)) return "sent";
|
|
4075
|
+
if (/(draft|drafts|чернов)/iu.test(text)) return "drafts";
|
|
4076
|
+
if (/(spam|junk|спам)/iu.test(text)) return "spam";
|
|
4077
|
+
if (/(trash|bin|корзин|удален|удалён)/iu.test(text)) return "trash";
|
|
4078
|
+
return "inbox";
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
function findYandexMailbox(folders, target) {
|
|
4082
|
+
const special = {
|
|
4083
|
+
sent: "\\Sent",
|
|
4084
|
+
drafts: "\\Drafts",
|
|
4085
|
+
spam: "\\Junk",
|
|
4086
|
+
trash: "\\Trash",
|
|
4087
|
+
}[target];
|
|
4088
|
+
if (special) {
|
|
4089
|
+
const bySpecial = folders.find((folder) => folder.flags.some((flag) => flag.toLocaleLowerCase("en-US") === special.toLocaleLowerCase("en-US")));
|
|
4090
|
+
if (bySpecial) return bySpecial;
|
|
4091
|
+
}
|
|
4092
|
+
const patterns = {
|
|
4093
|
+
sent: /(sent|отправ|исход)/iu,
|
|
4094
|
+
drafts: /(draft|чернов)/iu,
|
|
4095
|
+
spam: /(spam|junk|спам)/iu,
|
|
4096
|
+
trash: /(trash|bin|корзин|удален|удалён)/iu,
|
|
4097
|
+
}[target];
|
|
4098
|
+
return folders.find((folder) => patterns?.test(folder.name) || patterns?.test(folder.displayName || ""));
|
|
4099
|
+
}
|
|
4100
|
+
|
|
3988
4101
|
async function yandexMailList(options = {}) {
|
|
3989
4102
|
const { token, email } = await yandexMailCredentials();
|
|
3990
4103
|
const session = await imapConnect();
|
|
@@ -4023,6 +4136,52 @@ async function yandexMailSearch(query, options = {}) {
|
|
|
4023
4136
|
return rows.filter((row) => normalizeGeoText(`${row.from} ${row.subject} ${row.snippet}`).includes(normalized)).slice(0, Number(options.limit || 20));
|
|
4024
4137
|
}
|
|
4025
4138
|
|
|
4139
|
+
async function yandexMailWatchTick(options = {}) {
|
|
4140
|
+
const config = await loadConfig();
|
|
4141
|
+
const watch = config.yandex?.mailWatch || {};
|
|
4142
|
+
if (!watch.enabled && !options.force) return { enabled: false, newMessages: [] };
|
|
4143
|
+
const mailbox = watch.mailbox || "INBOX";
|
|
4144
|
+
const lastUid = Number(watch.lastUid || 0);
|
|
4145
|
+
const rows = await yandexMailList({ mailbox, limit: Number(options.limit || 30), unread: false });
|
|
4146
|
+
const newMessages = rows.filter((row) => Number(row.uid || 0) > lastUid).sort((left, right) => Number(left.uid || 0) - Number(right.uid || 0));
|
|
4147
|
+
const newestUid = Math.max(lastUid, ...rows.map((row) => Number(row.uid || 0)));
|
|
4148
|
+
await saveConfig({
|
|
4149
|
+
yandex: {
|
|
4150
|
+
...(config.yandex || {}),
|
|
4151
|
+
mailWatch: {
|
|
4152
|
+
...watch,
|
|
4153
|
+
enabled: watch.enabled !== false,
|
|
4154
|
+
mailbox,
|
|
4155
|
+
lastUid: newestUid,
|
|
4156
|
+
lastCheckedAt: new Date().toISOString(),
|
|
4157
|
+
},
|
|
4158
|
+
},
|
|
4159
|
+
});
|
|
4160
|
+
return { enabled: true, mailbox, lastUid, newestUid, newMessages };
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
async function yandexMailWatchEnable(minutes = 5) {
|
|
4164
|
+
const config = await loadConfig();
|
|
4165
|
+
const safeMinutes = Math.max(1, Number(minutes || 5));
|
|
4166
|
+
const latest = await yandexMailList({ mailbox: "INBOX", limit: 1, unread: false });
|
|
4167
|
+
const lastUid = latest[0]?.uid || 0;
|
|
4168
|
+
await saveConfig({
|
|
4169
|
+
yandex: {
|
|
4170
|
+
...(config.yandex || {}),
|
|
4171
|
+
mailWatch: { enabled: true, minutes: safeMinutes, mailbox: "INBOX", lastUid, updatedAt: new Date().toISOString() },
|
|
4172
|
+
},
|
|
4173
|
+
});
|
|
4174
|
+
await upsertCronJob(`каждые ${safeMinutes} минут`, "yandex mail-watch tick", { replaceCommand: true });
|
|
4175
|
+
return { enabled: true, minutes: safeMinutes, mailbox: "INBOX", lastUid };
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
async function yandexMailWatchDisable() {
|
|
4179
|
+
const config = await loadConfig();
|
|
4180
|
+
await saveConfig({ yandex: { ...(config.yandex || {}), mailWatch: { ...(config.yandex?.mailWatch || {}), enabled: false, updatedAt: new Date().toISOString() } } });
|
|
4181
|
+
deleteCronJobsByCommand("yandex mail-watch tick");
|
|
4182
|
+
return { enabled: false };
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4026
4185
|
async function yandexMailRead(uid, options = {}) {
|
|
4027
4186
|
if (!uid) throw new Error("UID письма обязателен.");
|
|
4028
4187
|
const { token, email } = await yandexMailCredentials();
|
|
@@ -4031,13 +4190,71 @@ async function yandexMailRead(uid, options = {}) {
|
|
|
4031
4190
|
await imapAuthenticate(session, email, token);
|
|
4032
4191
|
await imapCommand(session, `SELECT ${quoteImapMailbox(options.mailbox || "INBOX")}`);
|
|
4033
4192
|
const bodyAccessor = options.markSeen === false ? "BODY.PEEK[TEXT]" : "BODY[TEXT]";
|
|
4034
|
-
const fetch = await imapCommand(session, `UID FETCH ${Number(uid)} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT)] ${bodyAccessor})`, { timeout: 60000 });
|
|
4193
|
+
const fetch = await imapCommand(session, `UID FETCH ${Number(uid)} (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (DATE FROM SUBJECT MESSAGE-ID REFERENCES)] ${bodyAccessor})`, { timeout: 60000 });
|
|
4035
4194
|
return parseImapFetchSummaries(fetch, { full: true })[0] || { uid, status: "not-found" };
|
|
4036
4195
|
} finally {
|
|
4037
4196
|
await imapClose(session);
|
|
4038
4197
|
}
|
|
4039
4198
|
}
|
|
4040
4199
|
|
|
4200
|
+
async function yandexMailMark(uid, seen = true, options = {}) {
|
|
4201
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4202
|
+
const { token, email } = await yandexMailCredentials();
|
|
4203
|
+
const session = await imapConnect();
|
|
4204
|
+
try {
|
|
4205
|
+
await imapAuthenticate(session, email, token);
|
|
4206
|
+
const mailbox = options.mailbox || "INBOX";
|
|
4207
|
+
await imapCommand(session, `SELECT ${quoteImapMailbox(mailbox)}`);
|
|
4208
|
+
const command = seen ? "+FLAGS.SILENT" : "-FLAGS.SILENT";
|
|
4209
|
+
await imapCommand(session, `UID STORE ${Number(uid)} ${command} (\\Seen)`);
|
|
4210
|
+
return { uid: Number(uid), mailbox, status: seen ? "seen" : "unseen" };
|
|
4211
|
+
} finally {
|
|
4212
|
+
await imapClose(session);
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
async function yandexMailDelete(uid, options = {}) {
|
|
4217
|
+
if (!options.confirm) throw new Error("Для удаления письма нужен аргумент confirm=true.");
|
|
4218
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4219
|
+
const folders = await yandexMailFolders();
|
|
4220
|
+
const trash = options.trashMailbox || findYandexMailbox(folders, "trash")?.name;
|
|
4221
|
+
if (!trash) throw new Error("Не нашел папку корзины в Яндекс Почте. Безопасное удаление невозможно.");
|
|
4222
|
+
const { token, email } = await yandexMailCredentials();
|
|
4223
|
+
const session = await imapConnect();
|
|
4224
|
+
try {
|
|
4225
|
+
await imapAuthenticate(session, email, token);
|
|
4226
|
+
const mailbox = options.mailbox || "INBOX";
|
|
4227
|
+
await imapCommand(session, `SELECT ${quoteImapMailbox(mailbox)}`);
|
|
4228
|
+
await imapCommand(session, `UID MOVE ${Number(uid)} ${quoteImapMailbox(trash)}`, { timeout: 60000 });
|
|
4229
|
+
return { uid: Number(uid), from: mailbox, to: trash, status: "moved-to-trash" };
|
|
4230
|
+
} finally {
|
|
4231
|
+
await imapClose(session);
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
async function yandexMailReply(args = {}) {
|
|
4236
|
+
if (!args.confirm) throw new Error("Для ответа на письмо нужен аргумент confirm=true.");
|
|
4237
|
+
const uid = args.uid || args.id;
|
|
4238
|
+
const text = args.text || args.body || args.message || "";
|
|
4239
|
+
if (!uid) throw new Error("UID письма обязателен.");
|
|
4240
|
+
if (!String(text).trim()) throw new Error("Текст ответа пустой.");
|
|
4241
|
+
const mailbox = await resolveYandexMailbox(args.mailbox || args.folder || "INBOX");
|
|
4242
|
+
const original = await yandexMailRead(uid, { mailbox, markSeen: true, includeMessageId: true });
|
|
4243
|
+
if (!original || original.status === "not-found") throw new Error(`Письмо #${uid} не найдено.`);
|
|
4244
|
+
const to = [extractEmailAddress(original.from)].filter(Boolean);
|
|
4245
|
+
if (!to.length) throw new Error("Не удалось определить получателя ответа из поля From.");
|
|
4246
|
+
const subject = /^re:/iu.test(original.subject || "") ? original.subject : `Re: ${original.subject || "(без темы)"}`;
|
|
4247
|
+
const result = await yandexMailSend({
|
|
4248
|
+
to,
|
|
4249
|
+
subject,
|
|
4250
|
+
text,
|
|
4251
|
+
confirm: true,
|
|
4252
|
+
inReplyTo: original.messageId || "",
|
|
4253
|
+
references: original.references || original.messageId || "",
|
|
4254
|
+
});
|
|
4255
|
+
return { ...result, replyToUid: Number(uid), originalFrom: original.from };
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4041
4258
|
async function yandexMailSend(args = {}) {
|
|
4042
4259
|
if (!args.confirm) throw new Error("Для отправки письма нужен аргумент confirm=true.");
|
|
4043
4260
|
const to = Array.isArray(args.to) ? args.to : String(args.to || "").split(/[;,]/).map((item) => item.trim()).filter(Boolean);
|
|
@@ -4053,7 +4270,7 @@ async function yandexMailSend(args = {}) {
|
|
|
4053
4270
|
await smtpCommand(session, `MAIL FROM:<${email}>`);
|
|
4054
4271
|
for (const recipient of to) await smtpCommand(session, `RCPT TO:<${recipient}>`);
|
|
4055
4272
|
await smtpCommand(session, "DATA", { expect: /^354/u });
|
|
4056
|
-
await smtpCommand(session, `${dotStuffSmtpData(buildMimeMessage({ from: email, to, subject, text }))}\r\n.`);
|
|
4273
|
+
await smtpCommand(session, `${dotStuffSmtpData(buildMimeMessage({ from: email, to, subject, text, inReplyTo: args.inReplyTo, references: args.references }))}\r\n.`);
|
|
4057
4274
|
await smtpCommand(session, "QUIT").catch(() => {});
|
|
4058
4275
|
return { from: email, to, subject, status: "sent" };
|
|
4059
4276
|
} finally {
|
|
@@ -4065,7 +4282,7 @@ function buildXoauth2(email, token) {
|
|
|
4065
4282
|
return Buffer.from(`user=${email}\x01auth=Bearer ${token}\x01\x01`).toString("base64");
|
|
4066
4283
|
}
|
|
4067
4284
|
|
|
4068
|
-
function buildMimeMessage({ from, to, subject, text }) {
|
|
4285
|
+
function buildMimeMessage({ from, to, subject, text, inReplyTo = "", references = "" }) {
|
|
4069
4286
|
const encodedSubject = Buffer.from(subject, "utf8").toString("base64");
|
|
4070
4287
|
const messageIdDomain = String(from || "").split("@")[1] || "localhost";
|
|
4071
4288
|
const messageId = `${randomUUID()}@${messageIdDomain}`;
|
|
@@ -4076,13 +4293,15 @@ function buildMimeMessage({ from, to, subject, text }) {
|
|
|
4076
4293
|
`Subject: =?UTF-8?B?${encodedSubject}?=`,
|
|
4077
4294
|
`Date: ${new Date().toUTCString()}`,
|
|
4078
4295
|
`Message-ID: <${messageId}>`,
|
|
4296
|
+
inReplyTo ? `In-Reply-To: ${inReplyTo}` : "",
|
|
4297
|
+
references ? `References: ${references}` : "",
|
|
4079
4298
|
"MIME-Version: 1.0",
|
|
4080
4299
|
"Content-Type: text/plain; charset=utf-8",
|
|
4081
4300
|
"Content-Transfer-Encoding: base64",
|
|
4082
4301
|
"X-Mailer: IOLA CLI",
|
|
4083
4302
|
"",
|
|
4084
4303
|
Buffer.from(text.replace(/\r?\n/g, "\r\n"), "utf8").toString("base64").replace(/.{1,76}/g, "$&\r\n").trim(),
|
|
4085
|
-
].join("\r\n");
|
|
4304
|
+
].filter((line) => line !== "").join("\r\n");
|
|
4086
4305
|
}
|
|
4087
4306
|
|
|
4088
4307
|
function dotStuffSmtpData(message) {
|
|
@@ -4172,7 +4391,7 @@ async function imapClose(session) {
|
|
|
4172
4391
|
}
|
|
4173
4392
|
|
|
4174
4393
|
function quoteImapMailbox(value) {
|
|
4175
|
-
return `"${String(value || "INBOX").replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
4394
|
+
return `"${encodeImapModifiedUtf7(String(value || "INBOX")).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
4176
4395
|
}
|
|
4177
4396
|
|
|
4178
4397
|
function parseImapSelect(text) {
|
|
@@ -4188,6 +4407,55 @@ function parseImapSearchUids(text) {
|
|
|
4188
4407
|
.flatMap((match) => match[1].trim().split(/\s+/).map(Number).filter(Boolean));
|
|
4189
4408
|
}
|
|
4190
4409
|
|
|
4410
|
+
function parseImapListMailboxes(text) {
|
|
4411
|
+
return String(text || "")
|
|
4412
|
+
.split(/\r?\n/u)
|
|
4413
|
+
.map((line) => {
|
|
4414
|
+
const match = line.match(/^\* LIST \(([^)]*)\) (?:"([^"]*)"|NIL) (?:"((?:\\"|[^"])*)"|(.+))$/iu);
|
|
4415
|
+
if (!match) return null;
|
|
4416
|
+
const flags = [...match[1].matchAll(/\\[^\s)]+/gu)].map((item) => item[0]);
|
|
4417
|
+
const delimiter = match[2] || "";
|
|
4418
|
+
const rawName = (match[3] || match[4] || "").trim();
|
|
4419
|
+
const name = decodeImapModifiedUtf7(rawName.replace(/\\"/g, "\""));
|
|
4420
|
+
return {
|
|
4421
|
+
name,
|
|
4422
|
+
displayName: name,
|
|
4423
|
+
delimiter,
|
|
4424
|
+
flags,
|
|
4425
|
+
special: flags.find((flag) => /\\(?:Inbox|Sent|Drafts|Junk|Trash)/iu.test(flag)) || "",
|
|
4426
|
+
};
|
|
4427
|
+
})
|
|
4428
|
+
.filter(Boolean);
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
function decodeImapModifiedUtf7(value) {
|
|
4432
|
+
return String(value || "").replace(/&([^-]*)-/gu, (_, data) => {
|
|
4433
|
+
if (!data) return "&";
|
|
4434
|
+
const base64 = data.replace(/,/g, "/");
|
|
4435
|
+
try {
|
|
4436
|
+
const buffer = Buffer.from(base64, "base64");
|
|
4437
|
+
let decoded = "";
|
|
4438
|
+
for (let index = 0; index + 1 < buffer.length; index += 2) {
|
|
4439
|
+
decoded += String.fromCharCode(buffer.readUInt16BE(index));
|
|
4440
|
+
}
|
|
4441
|
+
return decoded;
|
|
4442
|
+
} catch {
|
|
4443
|
+
return `&${data}-`;
|
|
4444
|
+
}
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
function encodeImapModifiedUtf7(value) {
|
|
4449
|
+
return String(value || "").replace(/&/gu, "&-").replace(/[^\x20-\x7e]+/gu, (chunk) => {
|
|
4450
|
+
const bytes = [];
|
|
4451
|
+
for (const char of chunk) {
|
|
4452
|
+
const code = char.charCodeAt(0);
|
|
4453
|
+
bytes.push((code >> 8) & 0xff, code & 0xff);
|
|
4454
|
+
}
|
|
4455
|
+
return `&${Buffer.from(bytes).toString("base64").replace(/\//g, ",").replace(/=+$/u, "")}-`;
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4191
4459
|
function parseImapFetchSummaries(text, options = {}) {
|
|
4192
4460
|
const rows = [];
|
|
4193
4461
|
const chunks = String(text || "").split(/\n(?=\* \d+ FETCH)/u);
|
|
@@ -4199,7 +4467,15 @@ function parseImapFetchSummaries(text, options = {}) {
|
|
|
4199
4467
|
const from = headers.from || "";
|
|
4200
4468
|
const date = headers.date || "";
|
|
4201
4469
|
const body = options.full ? stripMailBody(chunk) : stripMailBody(chunk).slice(0, 800);
|
|
4202
|
-
rows.push({
|
|
4470
|
+
rows.push({
|
|
4471
|
+
uid,
|
|
4472
|
+
date,
|
|
4473
|
+
from,
|
|
4474
|
+
subject,
|
|
4475
|
+
messageId: headers.messageId || "",
|
|
4476
|
+
references: headers.references || "",
|
|
4477
|
+
snippet: body.replace(/\s+/g, " ").trim().slice(0, options.full ? 12000 : 500),
|
|
4478
|
+
});
|
|
4203
4479
|
}
|
|
4204
4480
|
return rows;
|
|
4205
4481
|
}
|
|
@@ -4214,9 +4490,18 @@ function parseMailHeaders(value) {
|
|
|
4214
4490
|
date: headerValue("Date"),
|
|
4215
4491
|
from: headerValue("From"),
|
|
4216
4492
|
subject: headerValue("Subject"),
|
|
4493
|
+
messageId: headerValue("Message-ID"),
|
|
4494
|
+
references: headerValue("References"),
|
|
4217
4495
|
};
|
|
4218
4496
|
}
|
|
4219
4497
|
|
|
4498
|
+
function extractEmailAddress(value) {
|
|
4499
|
+
const text = String(value || "").trim();
|
|
4500
|
+
return text.match(/<([^<>@\s]+@[^<>@\s]+)>/u)?.[1]
|
|
4501
|
+
|| text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0]
|
|
4502
|
+
|| "";
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4220
4505
|
function decodeMimeHeader(value) {
|
|
4221
4506
|
return String(value || "")
|
|
4222
4507
|
.replace(/\?=\s+=\?/gu, "?==?")
|
|
@@ -4383,7 +4668,7 @@ async function yandexContactsList(args = {}) {
|
|
|
4383
4668
|
const rows = [];
|
|
4384
4669
|
for (const href of hrefs) {
|
|
4385
4670
|
const card = await yandexDavRequest(new URL(href, "https://carddav.yandex.ru/").toString(), token, { method: "GET", timeout: 30000 }).catch(() => "");
|
|
4386
|
-
rows.push(...parseVCards(card));
|
|
4671
|
+
rows.push(...parseVCards(card).map((row) => args.full ? { ...row, href, card } : row));
|
|
4387
4672
|
}
|
|
4388
4673
|
return rows.slice(0, Number(args.limit || 50));
|
|
4389
4674
|
}
|
|
@@ -4395,6 +4680,64 @@ async function yandexContactsSearch(query, args = {}) {
|
|
|
4395
4680
|
return rows.filter((row) => normalizeGeoText(`${row.name} ${row.email} ${row.phone}`).includes(normalized)).slice(0, Number(args.limit || 20));
|
|
4396
4681
|
}
|
|
4397
4682
|
|
|
4683
|
+
async function resolveYandexMailRecipientFromContacts(query) {
|
|
4684
|
+
const normalized = normalizeContactLookupText(query);
|
|
4685
|
+
if (!normalized) return { status: "not-found", contacts: [] };
|
|
4686
|
+
const rows = await yandexContactsList({ limit: 300 });
|
|
4687
|
+
const matches = rows.filter((row) => contactMatchesQuery(row, normalized)).slice(0, 10);
|
|
4688
|
+
if (!matches.length) return { status: "not-found", contacts: [] };
|
|
4689
|
+
const withEmail = matches.filter((row) => row.email);
|
|
4690
|
+
if (withEmail.length === 1) return { status: "ok", contact: withEmail[0] };
|
|
4691
|
+
if (withEmail.length > 1) return { status: "ambiguous", contacts: withEmail };
|
|
4692
|
+
return matches.length === 1
|
|
4693
|
+
? { status: "no-email", contact: matches[0] }
|
|
4694
|
+
: { status: "ambiguous", contacts: matches };
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
async function yandexContactsAddEmail(query, email, args = {}) {
|
|
4698
|
+
if (!args.confirm) throw new Error("Для изменения контакта нужен аргумент confirm=true.");
|
|
4699
|
+
if (!query) throw new Error("Укажите имя или часть имени контакта.");
|
|
4700
|
+
if (!email || !/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu.test(String(email))) throw new Error("Укажите корректный email.");
|
|
4701
|
+
const rows = await yandexContactsList({ limit: 500, full: true });
|
|
4702
|
+
const matches = rows.filter((row) => contactMatchesQuery(row, normalizeContactLookupText(query))).slice(0, 10);
|
|
4703
|
+
if (!matches.length) return { status: "not-found", query };
|
|
4704
|
+
if (matches.length > 1 && !args.selectFirst) return { status: "ambiguous", query, contacts: matches.map(({ card, ...row }) => row) };
|
|
4705
|
+
const contact = matches[0];
|
|
4706
|
+
if (contact.email && !args.overwrite) return { status: "has-email", contact: { name: contact.name, email: contact.email } };
|
|
4707
|
+
const updatedCard = upsertVcardEmail(contact.card, email, { overwrite: Boolean(args.overwrite) });
|
|
4708
|
+
const token = await requireYandexOAuthToken("organizer", "Яндекс Контакты");
|
|
4709
|
+
await yandexDavRequest(new URL(contact.href, "https://carddav.yandex.ru/").toString(), token, {
|
|
4710
|
+
method: "PUT",
|
|
4711
|
+
headers: { "content-type": "text/vcard; charset=utf-8" },
|
|
4712
|
+
body: updatedCard,
|
|
4713
|
+
timeout: 45000,
|
|
4714
|
+
});
|
|
4715
|
+
return { status: "updated", name: contact.name, email };
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
function upsertVcardEmail(card, email, options = {}) {
|
|
4719
|
+
const text = String(card || "").replace(/\r/g, "").trim();
|
|
4720
|
+
if (!text.includes("BEGIN:VCARD")) throw new Error("Контакт не похож на vCard.");
|
|
4721
|
+
if (/^EMAIL[^:]*:/imu.test(text)) {
|
|
4722
|
+
if (!options.overwrite) return text;
|
|
4723
|
+
return text.replace(/^EMAIL[^:]*:[^\n]*/imu, `EMAIL;TYPE=INTERNET:${email}`);
|
|
4724
|
+
}
|
|
4725
|
+
return text.replace(/\nEND:VCARD/iu, `\nEMAIL;TYPE=INTERNET:${email}\nEND:VCARD`);
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
function contactMatchesQuery(contact, normalizedQuery) {
|
|
4729
|
+
const contactText = normalizeContactLookupText(`${contact.name || ""} ${contact.email || ""}`);
|
|
4730
|
+
if (!contactText || !normalizedQuery) return false;
|
|
4731
|
+
if (contactText.includes(normalizedQuery)) return true;
|
|
4732
|
+
const queryTokens = normalizedQuery.split(/\s+/u).filter(Boolean);
|
|
4733
|
+
const contactTokens = contactText.split(/\s+/u).filter(Boolean);
|
|
4734
|
+
return queryTokens.every((queryToken) => contactTokens.some((contactToken) => contactToken.startsWith(queryToken.slice(0, Math.max(4, Math.min(queryToken.length, 6)))) || queryToken.startsWith(contactToken.slice(0, Math.max(4, Math.min(contactToken.length, 6))))));
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
function normalizeContactLookupText(value) {
|
|
4738
|
+
return normalizeGeoText(String(value || "").replace(/\b(?:кому|контакт|письмо|сообщение)\b/giu, " ")).trim();
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4398
4741
|
async function yandexContactsBaseUrl(token) {
|
|
4399
4742
|
const root = "https://carddav.yandex.ru/";
|
|
4400
4743
|
const principalXml = await yandexDavRequest(root, token, {
|
|
@@ -9083,6 +9426,25 @@ function addCronJob(scheduleText, command) {
|
|
|
9083
9426
|
}
|
|
9084
9427
|
}
|
|
9085
9428
|
|
|
9429
|
+
async function upsertCronJob(scheduleText, command, options = {}) {
|
|
9430
|
+
initDatabase();
|
|
9431
|
+
const db = openDatabase();
|
|
9432
|
+
try {
|
|
9433
|
+
const existing = db.prepare("SELECT id FROM cron_jobs WHERE command = ? ORDER BY id DESC LIMIT 1").get(command);
|
|
9434
|
+
if (existing?.id) {
|
|
9435
|
+
db.prepare("UPDATE cron_jobs SET schedule_text = ?, enabled = 1 WHERE id = ?").run(scheduleText, existing.id);
|
|
9436
|
+
return Number(existing.id);
|
|
9437
|
+
}
|
|
9438
|
+
if (options.replaceCommand) {
|
|
9439
|
+
db.prepare("DELETE FROM cron_jobs WHERE command = ?").run(command);
|
|
9440
|
+
}
|
|
9441
|
+
const result = db.prepare("INSERT INTO cron_jobs(schedule_text, command) VALUES (?, ?)").run(scheduleText, command);
|
|
9442
|
+
return Number(result.lastInsertRowid);
|
|
9443
|
+
} finally {
|
|
9444
|
+
db.close();
|
|
9445
|
+
}
|
|
9446
|
+
}
|
|
9447
|
+
|
|
9086
9448
|
function deleteCronJob(id) {
|
|
9087
9449
|
initDatabase();
|
|
9088
9450
|
const db = openDatabase();
|
|
@@ -9093,6 +9455,16 @@ function deleteCronJob(id) {
|
|
|
9093
9455
|
}
|
|
9094
9456
|
}
|
|
9095
9457
|
|
|
9458
|
+
function deleteCronJobsByCommand(command) {
|
|
9459
|
+
initDatabase();
|
|
9460
|
+
const db = openDatabase();
|
|
9461
|
+
try {
|
|
9462
|
+
db.prepare("DELETE FROM cron_jobs WHERE command = ?").run(command);
|
|
9463
|
+
} finally {
|
|
9464
|
+
db.close();
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9467
|
+
|
|
9096
9468
|
function dueCronJobs() {
|
|
9097
9469
|
initDatabase();
|
|
9098
9470
|
const db = openDatabase();
|
|
@@ -9638,23 +10010,87 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9638
10010
|
].join("\n");
|
|
9639
10011
|
}
|
|
9640
10012
|
|
|
9641
|
-
if (/(почт|письм|email|e-mail)/iu.test(normalized)) {
|
|
10013
|
+
if (mailFollowup || /(почт|письм|email|e-mail|спам|чернов|отправлен|исходящ|корзин)/iu.test(normalized)) {
|
|
10014
|
+
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(включ|запусти|начни|поставь|создай)/iu.test(normalized)) {
|
|
10015
|
+
const minutes = Number(String(question || "").match(/(\d+)\s*(?:мин|минут)/iu)?.[1] || 5);
|
|
10016
|
+
await yandexMailWatchEnable(minutes);
|
|
10017
|
+
return `Автопроверка новых писем включена: каждые ${minutes} минут.`;
|
|
10018
|
+
}
|
|
10019
|
+
if (/(авто|автомат|кажд|период|монитор|следи|проверяй|проверку|режим)/iu.test(normalized) && /(выключ|отключ|останов|убери)/iu.test(normalized)) {
|
|
10020
|
+
await yandexMailWatchDisable();
|
|
10021
|
+
return "Автопроверка новых писем выключена.";
|
|
10022
|
+
}
|
|
10023
|
+
if (/(проверь|получи|есть).{0,40}(нов|свеж)/iu.test(normalized)) {
|
|
10024
|
+
const result = await yandexMailWatchTick({ force: true });
|
|
10025
|
+
if (!result.newMessages.length) return "Новых писем нет.";
|
|
10026
|
+
return ["Новые письма:", ...result.newMessages.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
10027
|
+
}
|
|
10028
|
+
if (/(папк|ящик|mailbox|folder)/iu.test(normalized) && /(покажи|список|какие|есть)/iu.test(normalized)) {
|
|
10029
|
+
const folders = await yandexMailFolders();
|
|
10030
|
+
if (!folders.length) return "Папки Яндекс Почты не найдены.";
|
|
10031
|
+
return ["Папки Яндекс Почты:", ...folders.map((folder, index) => `${index + 1}. ${folder.name}${folder.special ? ` (${folder.special})` : ""}`)].join("\n");
|
|
10032
|
+
}
|
|
9642
10033
|
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
9643
10034
|
const result = await yandexMailStatus();
|
|
9644
10035
|
return `Яндекс Почта подключена: ${result.email}. Входящие: ${result.inbox?.exists ?? "-"}.`;
|
|
9645
10036
|
}
|
|
9646
|
-
if (/(
|
|
10037
|
+
if (/(ответь|ответить|напиши\s+ответ)/iu.test(normalized)) {
|
|
10038
|
+
const reply = parseYandexMailReplyRequest(question, previousAssistantText);
|
|
10039
|
+
if (!reply.uid || !reply.text) return "Для ответа укажите письмо и текст. Пример: ответь на письмо #2382 текст: спасибо, получил.";
|
|
10040
|
+
const result = await yandexMailReply({ ...reply, confirm: true });
|
|
10041
|
+
return `Ответ отправлен на письмо #${result.replyToUid}: ${result.to.join(", ")}. Тема: ${result.subject}.`;
|
|
10042
|
+
}
|
|
10043
|
+
if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) {
|
|
10044
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10045
|
+
if (!uid) return "Какое письмо удалить? Укажите номер из списка или UID, например: удали письмо #2382.";
|
|
10046
|
+
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
10047
|
+
const result = await yandexMailDelete(uid, { mailbox, confirm: true });
|
|
10048
|
+
return `Письмо #${result.uid} перемещено в корзину: ${result.to}.`;
|
|
10049
|
+
}
|
|
10050
|
+
if (/(пометь|отметь|сделай)/iu.test(normalized) && /(прочитан|непрочитан)/iu.test(normalized)) {
|
|
10051
|
+
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText);
|
|
10052
|
+
if (!uid) return "Какое письмо пометить? Укажите номер из списка или UID.";
|
|
10053
|
+
const seen = !/непрочитан/iu.test(normalized);
|
|
10054
|
+
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
10055
|
+
const result = await yandexMailMark(uid, seen, { mailbox });
|
|
10056
|
+
return `Письмо #${result.uid} помечено как ${seen ? "прочитанное" : "непрочитанное"}.`;
|
|
10057
|
+
}
|
|
10058
|
+
if (/(отправь|отправить|напиши|пошли)/iu.test(normalized) && !/(отправлен|исходящ)/iu.test(normalized)) {
|
|
9647
10059
|
const draft = parseYandexMailSendRequest(question);
|
|
10060
|
+
if (!draft.to.length && draft.contactQuery) {
|
|
10061
|
+
const contactResult = await resolveYandexMailRecipientFromContacts(draft.contactQuery);
|
|
10062
|
+
if (contactResult.status === "not-found") return `В Яндекс Контактах не нашел: ${draft.contactQuery}. Укажите email вручную.`;
|
|
10063
|
+
if (contactResult.status === "no-email") return `Контакт найден, но email не указан: ${contactResult.contact.name}. Укажите email вручную.`;
|
|
10064
|
+
if (contactResult.status === "ambiguous") {
|
|
10065
|
+
return [
|
|
10066
|
+
`Нашел несколько контактов для "${draft.contactQuery}". Уточните, кому отправить:`,
|
|
10067
|
+
...contactResult.contacts.map((contact, index) => `${index + 1}. ${contact.name || contact.email || "Контакт"}${contact.email ? `, ${contact.email}` : ", email не указан"}`),
|
|
10068
|
+
].join("\n");
|
|
10069
|
+
}
|
|
10070
|
+
draft.to = [contactResult.contact.email];
|
|
10071
|
+
}
|
|
9648
10072
|
if (!draft.to.length || !draft.text) {
|
|
9649
10073
|
return "Для отправки письма укажите получателя и текст. Пример: отправь письмо user@example.com тема: Привет текст: Проверка.";
|
|
9650
10074
|
}
|
|
9651
10075
|
const result = await yandexMailSend({ ...draft, confirm: true });
|
|
9652
|
-
|
|
10076
|
+
let contactNote = "";
|
|
10077
|
+
if (draft.contactQuery && result.to[0] && process.stdin.isTTY) {
|
|
10078
|
+
const contactResult = await resolveYandexMailRecipientFromContacts(draft.contactQuery).catch(() => null);
|
|
10079
|
+
if (contactResult?.status === "no-email") {
|
|
10080
|
+
const shouldAdd = await askYesNo(`Добавить ${result.to[0]} в контакт "${contactResult.contact.name}"? [y/N] `, false);
|
|
10081
|
+
if (shouldAdd) {
|
|
10082
|
+
const update = await yandexContactsAddEmail(draft.contactQuery, result.to[0], { confirm: true, selectFirst: true });
|
|
10083
|
+
contactNote = update.status === "updated" ? `\nEmail добавлен в контакт: ${update.name || draft.contactQuery}.` : "";
|
|
10084
|
+
}
|
|
10085
|
+
}
|
|
10086
|
+
}
|
|
10087
|
+
return `Письмо отправлено: ${result.to.join(", ")}. Тема: ${result.subject}.${contactNote}`;
|
|
9653
10088
|
}
|
|
9654
10089
|
if (/(сколько|количеств|есть\s+ли)/iu.test(normalized) && /непрочитан/iu.test(normalized)) {
|
|
9655
|
-
const
|
|
10090
|
+
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
10091
|
+
const count = await yandexMailCount({ mailbox, unread: true });
|
|
9656
10092
|
if (!count) return "Непрочитанных писем нет.";
|
|
9657
|
-
const latest = await yandexMailList({ limit: 1, unread: true });
|
|
10093
|
+
const latest = await yandexMailList({ mailbox, limit: 1, unread: true });
|
|
9658
10094
|
return [
|
|
9659
10095
|
`Непрочитанных писем: ${count}.`,
|
|
9660
10096
|
latest[0] ? `Самое свежее: ${formatYandexMailSummary(latest[0])}` : "",
|
|
@@ -9664,13 +10100,14 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9664
10100
|
const uid = resolveYandexMailUidFromQuestion(question, previousAssistantText)
|
|
9665
10101
|
|| await getLatestYandexMailUid({ unread: /непрочитан/iu.test(normalized) });
|
|
9666
10102
|
if (!uid) return "Писем для чтения не найдено.";
|
|
9667
|
-
const row = await yandexMailRead(uid);
|
|
10103
|
+
const row = await yandexMailRead(uid, { mailbox: await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX") });
|
|
9668
10104
|
if (!row || row.status === "not-found") return `Письмо #${uid} не найдено.`;
|
|
9669
10105
|
return formatYandexMailRead(row);
|
|
9670
10106
|
}
|
|
10107
|
+
const mailbox = await resolveYandexMailbox(extractYandexMailboxName(question) || "INBOX");
|
|
9671
10108
|
const rows = /(найди|поиск)/iu.test(normalized)
|
|
9672
|
-
? await yandexMailSearch(cleanupYandexQuery(question), { limit: 10 })
|
|
9673
|
-
: await yandexMailList({ limit: 10, unread: /непрочитан/iu.test(normalized) });
|
|
10109
|
+
? await yandexMailSearch(cleanupYandexQuery(question), { mailbox, limit: 10 })
|
|
10110
|
+
: await yandexMailList({ mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) });
|
|
9674
10111
|
if (!rows.length) return "Писем по запросу не найдено.";
|
|
9675
10112
|
return ["Яндекс Почта:", ...rows.map((row, index) => `${index + 1}. ${formatYandexMailSummary(row)}`)].join("\n");
|
|
9676
10113
|
}
|
|
@@ -9682,6 +10119,21 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9682
10119
|
}
|
|
9683
10120
|
|
|
9684
10121
|
if (/(контакт|адресн)/iu.test(normalized)) {
|
|
10122
|
+
if (/(добав|запиши|сохрани).{0,40}(email|e-mail|почт)/iu.test(normalized)) {
|
|
10123
|
+
const email = String(question || "").match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/iu)?.[0] || "";
|
|
10124
|
+
const query = cleanupYandexContactEmailTarget(question, email);
|
|
10125
|
+
if (!email || !query) return "Укажите контакт и email. Пример: добавь email petrov@example.com к контакту Петров.";
|
|
10126
|
+
const result = await yandexContactsAddEmail(query, email, { confirm: true });
|
|
10127
|
+
if (result.status === "not-found") return `Контакт не найден: ${query}.`;
|
|
10128
|
+
if (result.status === "has-email") return `У контакта уже есть email: ${result.contact.name}, ${result.contact.email}.`;
|
|
10129
|
+
if (result.status === "ambiguous") {
|
|
10130
|
+
return [
|
|
10131
|
+
`Нашел несколько контактов для "${query}". Уточните контакт:`,
|
|
10132
|
+
...result.contacts.map((contact, index) => `${index + 1}. ${contact.name || contact.email || "Контакт"}${contact.email ? `, ${contact.email}` : ""}`),
|
|
10133
|
+
].join("\n");
|
|
10134
|
+
}
|
|
10135
|
+
return `Email добавлен в контакт: ${result.name || query}, ${result.email}.`;
|
|
10136
|
+
}
|
|
9685
10137
|
if (/(статус|проверь|работает|доступ)/iu.test(normalized)) {
|
|
9686
10138
|
const result = await yandexContactsStatus();
|
|
9687
10139
|
return `Яндекс Контакты подключены: ${result.displayName || result.url}.`;
|
|
@@ -9700,7 +10152,7 @@ async function buildYandexDirectAnswer(question, history = []) {
|
|
|
9700
10152
|
}
|
|
9701
10153
|
|
|
9702
10154
|
function isYandexServiceQuestion(normalized) {
|
|
9703
|
-
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex
|
|
10155
|
+
return /(яндекс|яндес|язндекс|язндекс|яндкс|yandex|почт|письм|календар|контакт|телемост|спам|чернов|отправлен|исходящ|корзин)/iu.test(String(normalized || ""));
|
|
9704
10156
|
}
|
|
9705
10157
|
|
|
9706
10158
|
function isYandexIdentityQuestion(normalized) {
|
|
@@ -9712,7 +10164,8 @@ function isYandexIdentityQuestion(normalized) {
|
|
|
9712
10164
|
function isYandexMailFollowupQuestion(normalized, question) {
|
|
9713
10165
|
return isYandexMailReadRequest(normalized)
|
|
9714
10166
|
|| isYandexMailSelectionQuestion(question)
|
|
9715
|
-
|| /(
|
|
10167
|
+
|| /(ответь|ответить|напиши\s+ответ|удали|удалить|перемести\s+в\s+корзин|пометь|отметь|сделай)/iu.test(String(normalized || ""))
|
|
10168
|
+
|| /(самое\s+свеж|последн|получи|получить|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(String(normalized || ""));
|
|
9716
10169
|
}
|
|
9717
10170
|
|
|
9718
10171
|
function cleanupYandexQuery(question) {
|
|
@@ -9725,6 +10178,16 @@ function cleanupYandexQuery(question) {
|
|
|
9725
10178
|
.trim();
|
|
9726
10179
|
}
|
|
9727
10180
|
|
|
10181
|
+
function extractYandexMailboxName(question) {
|
|
10182
|
+
const text = String(question || "").toLocaleLowerCase("ru-RU");
|
|
10183
|
+
if (/(спам|spam|junk)/iu.test(text)) return "spam";
|
|
10184
|
+
if (/(чернов|draft)/iu.test(text)) return "drafts";
|
|
10185
|
+
if (/(отправлен|исходящ|sent)/iu.test(text)) return "sent";
|
|
10186
|
+
if (/(корзин|удален|удалён|trash|bin)/iu.test(text)) return "trash";
|
|
10187
|
+
const explicit = String(question || "").match(/(?:папк[аиуы]?|mailbox|folder)\s+["'«]?([^"'».,!?]+)["'»]?/iu)?.[1];
|
|
10188
|
+
return explicit ? explicit.trim() : "";
|
|
10189
|
+
}
|
|
10190
|
+
|
|
9728
10191
|
function extractYandexMailUid(question) {
|
|
9729
10192
|
const text = String(question || "");
|
|
9730
10193
|
const explicit = text.match(/(?:#|uid\s*|письм[оа]?\s*)?(\d{3,})/iu)?.[1];
|
|
@@ -9743,7 +10206,12 @@ function resolveYandexMailUidFromQuestion(question, previousAssistantText = "")
|
|
|
9743
10206
|
const uid = extractYandexMailUidByOrdinal(previousAssistantText, Number(ordinal));
|
|
9744
10207
|
if (uid) return uid;
|
|
9745
10208
|
}
|
|
9746
|
-
|
|
10209
|
+
const actionOrdinal = String(question || "").match(/(?:удали|удалить|пометь|отметь|сделай|ответь|ответить)\s+#?(\d{1,3})\.?(?!\d)/iu)?.[1];
|
|
10210
|
+
if (actionOrdinal) {
|
|
10211
|
+
const uid = extractYandexMailUidByOrdinal(previousAssistantText, Number(actionOrdinal));
|
|
10212
|
+
if (uid) return uid;
|
|
10213
|
+
}
|
|
10214
|
+
if (/(самое\s+свеж|последн|получи|получить|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(String(question || ""))) {
|
|
9747
10215
|
return extractFirstYandexMailUid(previousAssistantText);
|
|
9748
10216
|
}
|
|
9749
10217
|
return 0;
|
|
@@ -9764,10 +10232,22 @@ function extractFirstYandexMailUid(text) {
|
|
|
9764
10232
|
}
|
|
9765
10233
|
|
|
9766
10234
|
function isYandexMailReadRequest(normalizedQuestion) {
|
|
9767
|
-
return /(^|\s)(
|
|
10235
|
+
return /(^|\s)(прочитай|прочти|открой|раскрой|прочитаем|получи|получить)(\s|$)/iu.test(normalizedQuestion)
|
|
9768
10236
|
|| /(покажи\s+содерж|о чем|о чём|текст\s+(?:то\s+)?(?:письм|где)|содержим)/iu.test(normalizedQuestion);
|
|
9769
10237
|
}
|
|
9770
10238
|
|
|
10239
|
+
function parseYandexMailReplyRequest(question, previousAssistantText = "") {
|
|
10240
|
+
const text = String(question || "").replace(/\s+/g, " ").trim();
|
|
10241
|
+
const uid = resolveYandexMailUidFromQuestion(text, previousAssistantText);
|
|
10242
|
+
const bodyMatch = text.match(/(?:текст|body|сообщение)\s*:\s*(.*)$/iu)
|
|
10243
|
+
|| text.match(/(?:ответь|ответить|напиши\s+ответ)(?:\s+на\s+письмо\s+#?\d+|\s+#?\d+)?\s*:?\s*(.*)$/iu);
|
|
10244
|
+
return {
|
|
10245
|
+
uid,
|
|
10246
|
+
text: (bodyMatch?.[1] || "").trim(),
|
|
10247
|
+
mailbox: extractYandexMailboxName(question) || "INBOX",
|
|
10248
|
+
};
|
|
10249
|
+
}
|
|
10250
|
+
|
|
9771
10251
|
async function getLatestYandexMailUid(options = {}) {
|
|
9772
10252
|
const rows = await yandexMailList({ limit: 1, unread: Boolean(options.unread) });
|
|
9773
10253
|
return rows[0]?.uid || 0;
|
|
@@ -9778,6 +10258,7 @@ function parseYandexMailSendRequest(question) {
|
|
|
9778
10258
|
const emails = [...text.matchAll(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu)].map((match) => match[0]);
|
|
9779
10259
|
const subjectMatch = text.match(/(?:тема|subject)\s*:\s*(.*?)(?=\s+(?:текст|body|сообщение)\s*:|$)/iu);
|
|
9780
10260
|
const bodyMatch = text.match(/(?:текст|body|сообщение)\s*:\s*(.*)$/iu);
|
|
10261
|
+
const contactMatch = text.match(/^(?:отправь|отправить|напиши|пошли)\s+(.+?)(?:\s+письмо|\s+сообщение|\s+текст\s*:|\s+напомни|\s+скажи|$)/iu);
|
|
9781
10262
|
const withoutCommand = text
|
|
9782
10263
|
.replace(/^(?:отправь|отправить|напиши|пошли)\s+(?:письмо\s+)?/iu, "")
|
|
9783
10264
|
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "")
|
|
@@ -9785,10 +10266,33 @@ function parseYandexMailSendRequest(question) {
|
|
|
9785
10266
|
return {
|
|
9786
10267
|
to: emails,
|
|
9787
10268
|
subject: (subjectMatch?.[1] || "Сообщение от IOLA CLI").trim(),
|
|
9788
|
-
text: (bodyMatch?.[1] || (!subjectMatch ? withoutCommand : "")).trim(),
|
|
10269
|
+
text: (bodyMatch?.[1] || cleanupYandexMailBodyText(!subjectMatch ? withoutCommand : "")).trim(),
|
|
10270
|
+
contactQuery: cleanupYandexContactQuery((contactMatch?.[1] || "").replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/giu, "")),
|
|
9789
10271
|
};
|
|
9790
10272
|
}
|
|
9791
10273
|
|
|
10274
|
+
function cleanupYandexMailBodyText(value) {
|
|
10275
|
+
return String(value || "")
|
|
10276
|
+
.replace(/^.+?\s+(?:письмо|сообщение)\s+/iu, "")
|
|
10277
|
+
.trim();
|
|
10278
|
+
}
|
|
10279
|
+
|
|
10280
|
+
function cleanupYandexContactQuery(value) {
|
|
10281
|
+
return String(value || "")
|
|
10282
|
+
.replace(/\b(?:письмо|сообщение|текст|напомни|скажи|ему|ей)\b/giu, " ")
|
|
10283
|
+
.replace(/\s+/g, " ")
|
|
10284
|
+
.trim();
|
|
10285
|
+
}
|
|
10286
|
+
|
|
10287
|
+
function cleanupYandexContactEmailTarget(question, email) {
|
|
10288
|
+
return String(question || "")
|
|
10289
|
+
.replace(email || "", " ")
|
|
10290
|
+
.replace(/\b(?:добавь|добавить|запиши|сохрани|email|e-mail|почту|почта|к|ко|контакту|контакт)\b/giu, " ")
|
|
10291
|
+
.replace(/[,:;.!?]+/gu, " ")
|
|
10292
|
+
.replace(/\s+/g, " ")
|
|
10293
|
+
.trim();
|
|
10294
|
+
}
|
|
10295
|
+
|
|
9792
10296
|
function formatYandexMailSummary(row) {
|
|
9793
10297
|
return `#${row.uid} ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}${row.date ? `, ${row.date}` : ""}`;
|
|
9794
10298
|
}
|
|
@@ -10414,8 +10918,8 @@ async function buildLocalToolPlan(question, providerConfig, options) {
|
|
|
10414
10918
|
`Доступные tools: ${availableToolNames(options).join(", ")}.`,
|
|
10415
10919
|
"Схема: {\"steps\":[{\"tool\":\"search_data\",\"args\":{\"dataset\":\"schools|kindergartens|all\",\"query\":\"text\",\"limit\":10}}]}",
|
|
10416
10920
|
"Минимальные tools: search_data {dataset,query,limit}, get_card {query}, export_report {name,format,output}, file_read {path}, browser_open {url}.",
|
|
10417
|
-
"Yandex tools: yandex_identity_me {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_save_text {path,text}, yandex_mail_list {limit,unread}, yandex_mail_search {query}, yandex_mail_read {uid}, yandex_calendar_list {start,end}, yandex_contacts_search {query}.",
|
|
10418
|
-
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_delete, yandex_mail_send, yandex_calendar_create_event, yandex_telemost_create_event.",
|
|
10921
|
+
"Yandex tools: yandex_identity_me {}, yandex_disk_ls {path}, yandex_disk_mkdir {path}, yandex_disk_find {query,path}, yandex_disk_save_text {path,text}, yandex_mail_folders {}, yandex_mail_list {mailbox,limit,unread}, yandex_mail_search {mailbox,query}, yandex_mail_read {mailbox,uid}, yandex_mail_mark {mailbox,uid,seen}, yandex_calendar_list {start,end}, yandex_contacts_search {query}.",
|
|
10922
|
+
"Опасные Yandex tools используй только при явной просьбе пользователя и с confirm=true: yandex_disk_share, yandex_disk_delete, yandex_mail_send, yandex_mail_reply, yandex_mail_delete, yandex_calendar_create_event, yandex_telemost_create_event.",
|
|
10419
10923
|
"MCP tools доступны как mcp:SERVER:TOOL, например mcp:iola-local:search.",
|
|
10420
10924
|
"Для выгрузки CSV добавь export_report с format=csv и output, если пользователь назвал файл.",
|
|
10421
10925
|
`Вопрос: ${question}`,
|
|
@@ -10484,9 +10988,16 @@ function inferToolPlan(question, options = {}) {
|
|
|
10484
10988
|
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_disk_find", args: { query: question, path: CLOUD_DEFAULT_REMOTE_DIR, limit: 20 } }] };
|
|
10485
10989
|
return { steps: [{ tool: "yandex_disk_ls", args: { path: CLOUD_DEFAULT_REMOTE_DIR } }] };
|
|
10486
10990
|
}
|
|
10487
|
-
if (/(почт|письм|email|e-mail)/iu.test(normalized)) {
|
|
10488
|
-
|
|
10489
|
-
|
|
10991
|
+
if (/(почт|письм|email|e-mail|спам|чернов|отправлен|исходящ|корзин)/iu.test(normalized)) {
|
|
10992
|
+
const mailbox = extractYandexMailboxName(question) || "INBOX";
|
|
10993
|
+
const uid = extractYandexMailUid(question);
|
|
10994
|
+
if (/(папк|ящик|mailbox|folder)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_folders", args: {} }] };
|
|
10995
|
+
if (/(ответь|ответить|напиши\s+ответ)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_reply", args: { uid, mailbox, text: parseYandexMailReplyRequest(question).text, confirm: true } }] };
|
|
10996
|
+
if (/(удали|удалить|перемести\s+в\s+корзин)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_delete", args: { uid, mailbox, confirm: true } }] };
|
|
10997
|
+
if (/(пометь|отметь|сделай)/iu.test(normalized) && /(прочитан|непрочитан)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_mark", args: { uid, mailbox, seen: !/непрочитан/iu.test(normalized) } }] };
|
|
10998
|
+
if (/(прочитай|прочти|открой|раскрой|получи|получить)/iu.test(normalized) && uid) return { steps: [{ tool: "yandex_mail_read", args: { uid, mailbox } }] };
|
|
10999
|
+
if (/(найди|поиск)/iu.test(normalized)) return { steps: [{ tool: "yandex_mail_search", args: { mailbox, query: question, limit: 20 } }] };
|
|
11000
|
+
return { steps: [{ tool: "yandex_mail_list", args: { mailbox, limit: 10, unread: /непрочитан/iu.test(normalized) } }] };
|
|
10490
11001
|
}
|
|
10491
11002
|
if (/(календар|событи|встреч|телемост)/iu.test(normalized)) {
|
|
10492
11003
|
return { steps: [{ tool: normalized.includes("телемост") ? "yandex_telemost_create_event" : "yandex_calendar_list", args: { limit: 20 } }] };
|
|
@@ -11050,6 +11561,10 @@ function formatToolResult(result, options) {
|
|
|
11050
11561
|
if (row.provider === "yandex-disk" && row.publicUrl) return `Публичная ссылка: ${row.publicUrl}`;
|
|
11051
11562
|
if (row.provider === "yandex-disk" && row.remote) return `Яндекс Диск: ${row.status || "ok"} ${row.remote}`;
|
|
11052
11563
|
if (row.uid && (row.subject || row.from)) return `Письмо #${row.uid}: ${row.subject || "(без темы)"}${row.from ? `, от ${row.from}` : ""}`;
|
|
11564
|
+
if (row.status === "moved-to-trash") return `Письмо #${row.uid} перемещено в корзину: ${row.to}`;
|
|
11565
|
+
if (row.status === "seen" || row.status === "unseen") return `Письмо #${row.uid}: ${row.status === "seen" ? "прочитано" : "непрочитано"}`;
|
|
11566
|
+
if (row.status === "sent" && row.to) return `Письмо отправлено: ${Array.isArray(row.to) ? row.to.join(", ") : row.to}. Тема: ${row.subject || "-"}`;
|
|
11567
|
+
if (row.special || row.delimiter) return `Папка почты: ${row.name}${row.special ? ` (${row.special})` : ""}`;
|
|
11053
11568
|
if (row.login || row.defaultEmail) return `Yandex ID: ${row.displayName || row.login || "-"}${row.defaultEmail ? `, ${row.defaultEmail}` : ""}`;
|
|
11054
11569
|
if (row.title && (row.start || row.end)) return `${row.title}: ${row.start || "-"}${row.end ? ` - ${row.end}` : ""}`;
|
|
11055
11570
|
if (row.email || row.phone) return formatYandexContact(row);
|
package/test/smoke-test.js
CHANGED
|
@@ -83,6 +83,17 @@ assertIncludes(cliSource, "resolveYandexMailUidFromQuestion", "Yandex mail follo
|
|
|
83
83
|
assertIncludes(cliSource, "extractYandexMailUidByOrdinal", "Yandex mail follow-ups should support numbered selections");
|
|
84
84
|
assertIncludes(cliSource, "BODY[TEXT]", "Yandex mail read should mark opened messages as seen");
|
|
85
85
|
assertIncludes(cliSource, "markSeen === false", "Yandex mail read should keep an explicit no-mark fallback");
|
|
86
|
+
assertIncludes(cliSource, "yandex_mail_folders", "Yandex mail should expose folders as a tool");
|
|
87
|
+
assertIncludes(cliSource, "yandex_mail_reply", "Yandex mail should expose reply as a tool");
|
|
88
|
+
assertIncludes(cliSource, "yandex_mail_delete", "Yandex mail should expose safe delete as a tool");
|
|
89
|
+
assertIncludes(cliSource, "yandex_mail_mark", "Yandex mail should expose read/unread mark as a tool");
|
|
90
|
+
assertIncludes(cliSource, "decodeImapModifiedUtf7", "Yandex mail folders should decode IMAP modified UTF-7 names");
|
|
91
|
+
assertIncludes(cliSource, "encodeImapModifiedUtf7", "Yandex mail should select non-ASCII folders via IMAP modified UTF-7");
|
|
92
|
+
assertIncludes(cliSource, "yandexMailWatchEnable", "Yandex mail should support scheduled new-mail polling");
|
|
93
|
+
assertIncludes(cliSource, "yandexMailWatchTick", "Yandex mail should support manual new-mail polling");
|
|
94
|
+
assertIncludes(cliSource, "resolveYandexMailRecipientFromContacts", "Yandex mail should resolve recipients from contacts");
|
|
95
|
+
assertIncludes(cliSource, "yandexContactsAddEmail", "Yandex contacts should support adding email to a contact");
|
|
96
|
+
assertIncludes(cliSource, "yandex_contacts_add_email", "Yandex contacts add email should be exposed as a tool");
|
|
86
97
|
assertIncludes(cliSource, "buildCasualDirectAnswer(question)", "Casual greetings should bypass external AI providers");
|
|
87
98
|
assertNotIncludes(cliSource, "Сервисы через запятую [identity,disk]", "Yandex setup should not ask for services during connector setup");
|
|
88
99
|
if (!packageJson.files.includes("docs/assets/iola-oauth-icon.png")) {
|
|
@@ -172,9 +172,26 @@ iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
|
172
172
|
|
|
173
173
|
- проверить доступ к почте;
|
|
174
174
|
- показать последние или непрочитанные письма;
|
|
175
|
+
- показать папки почты: входящие, отправленные, черновики, спам, корзина и пользовательские папки;
|
|
176
|
+
- показать письма из конкретной папки, например из спама, черновиков или отправленных;
|
|
175
177
|
- найти письмо;
|
|
176
|
-
- прочитать письмо по UID
|
|
177
|
-
-
|
|
178
|
+
- прочитать письмо по UID или номеру из последнего списка;
|
|
179
|
+
- при чтении по просьбе пользователя пометить письмо прочитанным;
|
|
180
|
+
- пометить письмо прочитанным или непрочитанным;
|
|
181
|
+
- отправить письмо только после явного запроса;
|
|
182
|
+
- ответить на письмо по UID или номеру из последнего списка;
|
|
183
|
+
- удалить письмо безопасно: переместить его в корзину, а не стирать безвозвратно.
|
|
184
|
+
- включить автоопрос новых писем, например каждые 5 минут;
|
|
185
|
+
- выключить автоопрос новых писем;
|
|
186
|
+
- вручную проверить новые письма через mail-watch tick;
|
|
187
|
+
- отправить письмо по имени контакта из Яндекс Контактов;
|
|
188
|
+
- если найдено несколько однофамильцев, попросить уточнить получателя;
|
|
189
|
+
- если контакт найден, но email не указан, прямо сообщить об этом;
|
|
190
|
+
- добавить email в существующий контакт по явной просьбе пользователя.
|
|
191
|
+
|
|
192
|
+
Список писем и поиск не меняют статус письма. Статус меняется только при чтении письма, явной пометке или явном удалении.
|
|
193
|
+
|
|
194
|
+
Автоопрос почты использует локальный cron CLI. Он не передает письма на сервер IOLA: токены, состояние последнего UID и расписание хранятся на компьютере пользователя.
|
|
178
195
|
|
|
179
196
|
### yandex-calendar
|
|
180
197
|
|