Что получится: модуль helpdesk на Python. Он подключает ящик поддержки к каналу «Почта». Каждое новое письмо становится заявкой, а письма одной цепочки собираются в одну заявку. Оператор отвечает из helpdesk, ответ уходит в ту же цепочку, обработанное письмо переносится в папку «Обработано». Если ответ не дошёл, заявка помечается.
письмо клиента ──► incomingMessageReceived ──► заявка (по threadId)
emailMessageData │
▼
оператор ──► replyEmail (в цепочку) ──► moveMessages «Обработано»
│
отчёт о недоставке ──► outgoingMessageStatus: failed ──► пометка в заявке
Что понадобится
- канал «Почта» с ключом (без ящика) и переменные
GRATIO_URL,GRATIO_ID,GRATIO_TOKEN; - вебхук канала — Шаг 4. Уведомления на свой сервер;
- Python 3.10+,
pip install fastapi uvicorn requests.
Шаг 1. Подключите ящик
Для Gmail, Outlook, Яндекса и Mail Gratio вернёт ссылку на страницу входа сервиса — её открывает человек, который знает ящик:
curl
curl -X POST "$GRATIO_URL/waInstance$GRATIO_ID/connectMailbox/$GRATIO_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"email": "support@school.ru"}'
Python
import os
import requests
BASE = f"{os.environ['GRATIO_URL']}/waInstance{os.environ['GRATIO_ID']}"
TOKEN = os.environ["GRATIO_TOKEN"]
url = f"{BASE}/connectMailbox/{TOKEN}"
payload = {"email": "support@school.ru"}
response = requests.post(url, json=payload, timeout=60)
print(response.status_code, response.json())
Node.js
const { GRATIO_URL, GRATIO_ID, GRATIO_TOKEN } = process.env;
const base = `${GRATIO_URL}/waInstance${GRATIO_ID}`;
const url = `${base}/connectMailbox/${GRATIO_TOKEN}`;
const payload = { email: "support@school.ru" };
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
console.log(response.status, await response.json());
PHP
<?php
$base = getenv('GRATIO_URL') . '/waInstance' . getenv('GRATIO_ID');
$url = $base . '/connectMailbox/' . getenv('GRATIO_TOKEN');
$payload = ['email' => 'support@school.ru'];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$response = json_decode(curl_exec($ch), true);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), PHP_EOL;
print_r($response);
Вы увидите:
{"connected": false, "auth": "oauth", "email": "support@school.ru", "provider": "yandex",
"providerTitle": "Яндекс", "authUrl": "https://oauth.yandex.ru/authorize?…", "expiresAt": 1792221600}
Откройте authUrl, войдите и разрешите доступ. Ссылка живёт 20 минут. Когда вход закончен, getStateInstance ответит authorized.
Для почты на хостинге или своём сервере передайте пароль — вход проверится сразу и ответ будет "connected": true. Все варианты и отказы — connectMailbox.
Шаг 2. Настройте ящик
Уведомления — только о «Входящих» и без рассылок; подпись — для всех ответов. Папка «Обработано» — для разобранных писем:
curl -X POST "$GRATIO_URL/waInstance$GRATIO_ID/setSettings/$GRATIO_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"emailFolders": "inbox",
"emailSkipBulk": "yes",
"emailSignature": "Служба поддержки\nШкола робототехники"
}'
curl -X POST "$GRATIO_URL/waInstance$GRATIO_ID/createFolder/$GRATIO_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name": "Обработано"}'
Шаг 3. Письма — в заявки
Файл helpdesk.py. Заявка — цепочка писем (threadId). Если клиент отвечает в ту же цепочку, письмо добавляется к существующей заявке.
import os
import sqlite3
import uuid
import requests
from fastapi import FastAPI, Header, HTTPException
BASE = f"{os.environ['GRATIO_URL']}/waInstance{os.environ['GRATIO_ID']}"
TOKEN = os.environ["GRATIO_TOKEN"]
HOOK_TOKEN = os.environ["GRATIO_HOOK_TOKEN"]
db = sqlite3.connect("helpdesk.db", check_same_thread=False)
db.executescript("""
create table if not exists tickets (thread_id text primary key, email text, subject text,
status text default 'new', problem text);
create table if not exists letters (id_message text primary key, thread_id text, direction text,
text text, mailbox text, uid int, at int);
""")
app = FastAPI()
def call(method: str, payload: dict) -> dict:
r = requests.post(f"{BASE}/{method}/{TOKEN}", json=payload, timeout=90)
r.raise_for_status()
return r.json()
def text_of(data: dict) -> str:
return ((data.get("textMessageData") or {}).get("textMessage")
or (data.get("extendedTextMessageData") or {}).get("text") or "")
def on_letter(body: dict) -> None:
data = body["messageData"]
mail = data["emailMessageData"]
thread = mail["threadId"] or body["idMessage"]
db.execute("insert or ignore into tickets (thread_id, email, subject) values (?, ?, ?)",
(thread, body["senderData"]["chatId"], mail["subject"]))
db.execute("update tickets set status = 'new' where thread_id = ? and status = 'answered'",
(thread,))
db.execute("insert or ignore into letters values (?, ?, 'in', ?, ?, ?, ?)",
(body["idMessage"], thread, text_of(data), mail["mailbox"], mail["uid"], body["timestamp"]))
mailbox и uid — где письмо лежит в ящике: по ним его надёжно найдут методы почты. Текст в уведомлении — без цитаты и подписи. Письмо целиком с html и вложениями — getEmail.
Шаг 4. Ответ оператора
replyEmail отвечает в цепочку письма: на адрес Reply-To или отправителя, с цитатой и подписью из настроек. После ответа письмо клиента переносится в «Обработано». clientMessageId защищает от второго письма при повторе запроса.
def answer(thread_id: str, text: str) -> dict:
mailbox, uid = db.execute(
"""select mailbox, uid from letters
where thread_id = ? and direction = 'in' order by at desc limit 1""",
(thread_id,)).fetchone()
sent = call("replyEmail", {"folder": mailbox, "uid": uid, "message": text,
"clientMessageId": f"ticket-{thread_id}-{uuid.uuid4()}"})
db.execute("insert or ignore into letters values (?, ?, 'out', ?, null, null, strftime('%s','now'))",
(sent["idMessage"], thread_id, text))
db.execute("update tickets set status = 'answered' where thread_id = ?", (thread_id,))
db.commit()
call("moveMessages", {"folder": mailbox, "uids": [uid], "to": "Обработано"})
return sent
Шаг 5. Недоставка
Если почтовый сервер не принял ответ, придёт отчёт о недоставке — уведомление outgoingMessageStatus со статусом failed и причиной в description:
def on_status(body: dict) -> None:
if body.get("status") != "failed":
return
row = db.execute("select thread_id from letters where id_message = ?", (body["idMessage"],)).fetchone()
if row:
db.execute("update tickets set status = 'problem', problem = ? where thread_id = ?",
(body.get("description", "не доставлено"), row[0]))
Шаг 6. Вебхук и запуск
@app.post("/gratio/hook")
def hook(body: dict, authorization: str = Header("")):
if authorization != f"Bearer {HOOK_TOKEN}":
raise HTTPException(status_code=401)
if body["typeWebhook"] == "incomingMessageReceived" and "emailMessageData" in body["messageData"]:
on_letter(body)
elif body["typeWebhook"] == "outgoingMessageStatus":
on_status(body)
db.commit()
return {"ok": True}
@app.get("/tickets")
def tickets():
rows = db.execute("select thread_id, email, subject, status, problem from tickets").fetchall()
return [dict(zip(("thread", "email", "subject", "status", "problem"), r)) for r in rows]
@app.post("/tickets/{thread_id}/answer")
def answer_endpoint(thread_id: str, payload: dict):
return answer(thread_id, payload["text"])
Сложите фрагменты шагов 3–6 в helpdesk.py по порядку и запустите:
uvicorn helpdesk:app --port 8080
- Напишите на ящик поддержки с личной почты. Вы увидите заявку:
curl localhost:8080/tickets. - Ответьте:
curl -X POST localhost:8080/tickets/<thread>/answer -H 'Content-Type: application/json' -d '{"text": "Здравствуйте! Смотрим."}'. Вы увидите письмо в личной почте — в той же цепочке, с подписью, а письмо клиента в ящике поддержки — в папке «Обработано». - Ответьте из личной почты. Вы увидите у заявки снова статус
new.
Что ещё добавить
- Заявки с форм сайта — правило
emailFormsразберёт поля «Имя», «Телефон» и другие: Заявки с форм сайта. - Вложения — ссылки на каждый файл письма в getEmail; файл в ответ —
filesв replyEmail. - Старые письма до подключения — searchMessages по папкам ящика.
Что дальше
- Все поля письма в уведомлении — Письмо.
- Методы ящика: папки, флаги, перенос — Почта через API.
