Files

175 lines
5.8 KiB
Python

#!/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()