mirror of
https://gitverse.ru/kpa39l/email-assistant.git
synced 2026-09-29 09:15:09 +00:00
Initial commit: Email Assistant project
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sqlite_search.py — FTS5-поиск по архиву писем.
|
||||
|
||||
Использование:
|
||||
python3 sqlite_search.py "запрос"
|
||||
python3 sqlite_search.py "запрос" --limit 20
|
||||
python3 sqlite_search.py "тема:отчёт" --folder "INBOX/!Отчеты"
|
||||
python3 sqlite_search.py "from:example@mail.ru" --body
|
||||
|
||||
Операторы FTS5 (по умолчанию AND):
|
||||
"точная фраза" — точное совпадение
|
||||
OR — любой из терминов
|
||||
-исключить — исключить термин
|
||||
prefix* — префикс (wildcard)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path("/opt/hermes/email/mail_index.db")
|
||||
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def format_date(d):
|
||||
"""Привести дату к читаемому виду."""
|
||||
if not d:
|
||||
return "—"
|
||||
for fmt in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%d %H:%M:%S%z",
|
||||
"%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
|
||||
try:
|
||||
dt = datetime.strptime(d.rstrip("Z"), fmt)
|
||||
return dt.strftime("%d.%m.%Y %H:%M")
|
||||
except ValueError:
|
||||
continue
|
||||
return d[:10]
|
||||
|
||||
|
||||
def search(query, limit=15, folder=None, search_body=False):
|
||||
conn = get_db()
|
||||
c = conn.cursor()
|
||||
|
||||
# Обрабатываем префиксы from:/subject:/body:
|
||||
from_filter = None
|
||||
subject_filter = None
|
||||
body_only = search_body
|
||||
|
||||
# Если запрос содержит from: или subject: — выносим в WHERE
|
||||
import re
|
||||
mf = re.search(r'\bfrom:(\S+)', query)
|
||||
if mf:
|
||||
from_filter = mf.group(1).replace('"', '').replace("'", "")
|
||||
query = query.replace(mf.group(0), '').strip()
|
||||
|
||||
ms = re.search(r'\bsubject:(\S+)', query)
|
||||
if ms:
|
||||
subject_filter = ms.group(1).replace('"', '').replace("'", "")
|
||||
query = query.replace(ms.group(0), '').strip()
|
||||
|
||||
if not query:
|
||||
print("❌ Укажите поисковый запрос")
|
||||
sys.exit(1)
|
||||
|
||||
# Строим SQL
|
||||
if body_only:
|
||||
# Ищем в FTS5 с телом письма
|
||||
sql = """
|
||||
SELECT e.path, e.folder, e.date, e.from_addr, e.subject,
|
||||
e.body_preview, e.contacts_extracted, e.contacts_skipped
|
||||
FROM email_fts f
|
||||
JOIN emails e ON e.rowid = f.rowid
|
||||
WHERE email_fts MATCH ?
|
||||
"""
|
||||
params = [query]
|
||||
else:
|
||||
# Стандартный поиск — FTS5 по subject + from + to
|
||||
sql = """
|
||||
SELECT e.path, e.folder, e.date, e.from_addr, e.subject,
|
||||
e.body_preview, e.contacts_extracted, e.contacts_skipped
|
||||
FROM email_fts f
|
||||
JOIN emails e ON e.rowid = f.rowid
|
||||
WHERE email_fts MATCH ?
|
||||
"""
|
||||
params = [query]
|
||||
|
||||
if folder:
|
||||
sql += " AND e.folder = ?"
|
||||
params.append(folder)
|
||||
|
||||
# Дополнительные фильтры для точного поиска
|
||||
conditions = []
|
||||
if from_filter:
|
||||
conditions.append("e.from_addr LIKE ?")
|
||||
params.append(f"%{from_filter}%")
|
||||
if subject_filter:
|
||||
conditions.append("e.subject LIKE ?")
|
||||
params.append(f"%{subject_filter}%")
|
||||
|
||||
if conditions:
|
||||
sql += " AND " + " AND ".join(conditions)
|
||||
|
||||
sql += " ORDER BY e.date DESC LIMIT ?"
|
||||
params.append(limit)
|
||||
|
||||
try:
|
||||
rows = c.execute(sql, params).fetchall()
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f"❌ Ошибка FTS5: {e}")
|
||||
print()
|
||||
print("Подсказки:")
|
||||
print(" Используйте кавычки для точной фразы: \"Иван Иванов\"")
|
||||
print(" Избегайте спецсимволов: * ? - (их не должно быть в простых словах)")
|
||||
print(" Пример: python3 sqlite_search.py 'битрикс OR контрагент'")
|
||||
conn.close()
|
||||
sys.exit(1)
|
||||
|
||||
if not rows:
|
||||
print(" Ничего не найдено")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
print(f" Найдено: {len(rows)} писем\n")
|
||||
|
||||
for r in rows:
|
||||
extr = "+" if r["contacts_extracted"] else "·"
|
||||
if r["contacts_skipped"]:
|
||||
extr = "-"
|
||||
date_fmt = format_date(r["date"])
|
||||
folder_short = r["folder"].replace("INBOX/", "") if r["folder"].startswith("INBOX") else r["folder"]
|
||||
|
||||
print(f" [{extr}] {date_fmt} | {folder_short:>20}")
|
||||
print(f" От: {r['from_addr'][:60]}")
|
||||
print(f" Тема: {r['subject'][:80]}")
|
||||
print(f" {EMAIL_ROOT / r['path']}")
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
def main():
|
||||
global EMAIL_ROOT
|
||||
from pathlib import Path as P
|
||||
EMAIL_ROOT = P("/opt/hermes/email")
|
||||
|
||||
parser = argparse.ArgumentParser(description="FTS5-поиск по архиву писем")
|
||||
parser.add_argument("query", type=str, nargs="?", help="Поисковый запрос")
|
||||
parser.add_argument("--limit", "-l", type=int, default=15, help="Макс. результатов")
|
||||
parser.add_argument("--folder", "-f", type=str, help="Фильтр по папке")
|
||||
parser.add_argument("--body", "-b", action="store_true", help="Поиск с учётом тела письма")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.query:
|
||||
parser.print_help()
|
||||
print()
|
||||
print("Примеры:")
|
||||
print(f" {sys.argv[0]} 'Стороженко'")
|
||||
print(f" {sys.argv[0]} 'from:example@mail'")
|
||||
print(f" {sys.argv[0]} 'subject:отчёт' --folder 'INBOX/!Отчеты'")
|
||||
print(f" {sys.argv[0]} 'битрикс OR контрагент'")
|
||||
sys.exit(0)
|
||||
|
||||
search(args.query, args.limit, args.folder, args.body)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user