Категории

Как добавить full-text поиск в Roundcube (с ограничением по пользователю и iframe)

19.07.2025 13:45 | коды из категории: Создание сайтов

Кто ставил бесплатный софт. тот знает на сколько это больно )) так и с раундкубом, я так и не нашел рабочий плагин для индексирования писем для быстрого поиска по телу письма. вот такой костыль сделал.

про Как добавить full-text поиск в Roundcube (с ограничением по пользователю и iframe)

Добавляем поиск по телу писем в Roundcube

Без плагинов, своими руками, с ограничением по пользователю и iframe сверху

💡 Цель:
  • Поиск по тексту писем
  • Только для текущего пользователя
  • Встроенный интерфейс в Roundcube как iframe сверху

установим необходимые библиотеки

Python и зависимости Скрипт использует Python 3 и несколько библиотек:


# Установите Python 3 и pip (если ещё не установлены)
sudo apt update
sudo apt install python3 python3-pip python3-venv

# Установите необходимые Python-библиотеки
pip3 install mysql-connector-python chardet


MySQL/MariaDB и клиентская библиотека Скрипт подключается к базе данных Roundcube, поэтому нужны:


# Установите MySQL/MariaDB (если ещё не установлен)
sudo apt install mariadb-server mariadb-client

# Установите клиентскую библиотеку Python для MySQL
sudo apt install python3-dev libmysqlclient-dev # Для Debian/Ubuntu
pip3 install mysqlclient # Альтернативный драйвер (может работать лучше)


Права доступа к почтовым файлам
Скрипт читает файлы из /var/mail/..., поэтому:

Убедитесь, что пользователь, от которого запускается скрипт, имеет права на чтение почтовых файлов (обычно mail или vmail).

Если используется Dovecot, проверьте, что структура папок соответствует ожидаемой (cur, new и т. д.).

Настройка Roundcube
Убедитесь, что база данных Roundcube (roundcubemail) существует и доступна.

Проверьте, что в таблице users есть записи с почтовыми ящиками, соответствующими папкам в /var/mail/....
Виртуальное окружение (опционально) Если скрипт использует #!/opt/maildir_venv/bin/python, создайте виртуальное окружение:

python3 -m venv /opt/maildir_venv
/opt/maildir_venv/bin/pip install mysql-connector-python chardet
Проверка зависимостей

python3 -c "import mysql.connector; import chardet; print('OK')"


Проверить доступ к MySQL
Убедитесь, что:
База Roundcube (roundcubemail) доступна.
Пользователь roundcube имеет права на запись в таблицу email_index.
Если MySQL на другом сервере — проверить подключение (можно через mysql -u roundcube -p -h localhost).

📦 Шаг 1: Подготовка базы данных

Создай таблицу для хранения индексированных писем:

CREATE TABLE IF NOT EXISTS email_index (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT UNSIGNED NOT NULL,
    path VARCHAR(512) NOT NULL,
    subject TEXT,
    from_email TEXT,
    body LONGTEXT,
    message_id VARCHAR(255),
    file_name VARCHAR(255),
    uid INT UNSIGNED DEFAULT NULL,
    FULLTEXT (body)
);

⚙️ Шаг 2: Парсер Maildir + MySQL

Сохраните этот скрипт как /usr/local/bin/maildir_multiuser_indexer.py:

#!/opt/maildir_venv/bin/python
import os
import email
from email.parser import BytesParser
from email import policy
import mysql.connector
import chardet

MAILDIR_DOMAIN_PATH = "/var/mail/..."
DB_CONFIG = {
    "host": "localhost",
    "user": "roundcube",
    "password": "roundcube12345",
    "database": "roundcubemail"
}

def get_decoded_body(msg):
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_type() == "text/plain":
                payload = part.get_payload(decode=True)
                if payload:
                    encoding = chardet.detect(payload)['encoding'] or 'utf-8'
                    return payload.decode(encoding, errors='replace')
    else:
        payload = msg.get_payload(decode=True)
        if payload:
            encoding = chardet.detect(payload)['encoding'] or 'utf-8'
            return payload.decode(encoding, errors='replace')
    return ""

def connect_db():
    return mysql.connector.connect(**DB_CONFIG)

def already_indexed(cursor, file_path):
    cursor.execute("SELECT 1 FROM email_index WHERE path = %s", (file_path,))
    return cursor.fetchone() is not None

def load_uid_map(user_maildir):
    """Читаем dovecot-uidlist из корневой папки пользователя"""
    uidlist_path = os.path.join(user_maildir, "dovecot-uidlist")

    if not os.path.exists(uidlist_path):
        print(f"❌ Файл dovecot-uidlist не найден: {uidlist_path}")
        return {}

    uid_map = {}
    with open(uidlist_path, 'r') as f:
        lines = f.readlines()

    for line in lines:
        if line.startswith('1 '):  # формат: 1 <uid> <filename>
            parts = line.strip().split()
            if len(parts) >= 3:
                try:
                    uid = int(parts[1])
                    filename = parts[2]
                    uid_map[filename] = uid
                except ValueError:
                    continue  # игнорируем некорректные строки
    return uid_map

def index_mail_file(cursor, file_path, user_id, uid_map):
    try:
        filename = os.path.basename(file_path)
        uid = uid_map.get(filename)

        with open(file_path, 'rb') as f:
            raw_email = f.read()
        msg = BytesParser(policy=policy.default).parsebytes(raw_email)
        subject = msg['subject']
        from_email = msg['from']
        body = get_decoded_body(msg)
        message_id = msg['message-id'] or ''

        if not body:
            print(f"⚠️ Не удалось извлечь текст из {file_path}")
            return

        cursor.execute("""
            INSERT INTO email_index (user_id, path, subject, from_email, body, message_id, file_name, uid)
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
        """, (user_id, file_path, subject, from_email, body, message_id, filename, uid))

        print(f"✅ Проиндексировано: {file_path} | UID: {uid}")

    except Exception as e:
        print(f"❌ Ошибка при обработке {file_path}: {e}")

def get_user_id(cursor, email_address):
    cursor.execute("SELECT user_id FROM users WHERE username = %s LIMIT 1", (email_address,))
    result = cursor.fetchone()
    return result[0] if result else None

def main():
    db = connect_db()
    cursor = db.cursor()

    indexed_count = 0

    if not os.path.exists(MAILDIR_DOMAIN_PATH):
        print(f"❌ Папка домена не найдена: {MAILDIR_DOMAIN_PATH}")
        return

    print("\n📬 Обработка всех пользователей домена logo-academia.ru")

    for user_email_dir in os.listdir(MAILDIR_DOMAIN_PATH):
        user_email_path = os.path.join(MAILDIR_DOMAIN_PATH, user_email_dir)
        if not os.path.isdir(user_email_path):
            continue

        print(f"\n📧 Обработка пользователя: {user_email_dir}")

        user_id = get_user_id(cursor, user_email_dir)
        if not user_id:
            print(f"❌ Пользователь {user_email_dir} не найден в базе Roundcube")
            continue

        # Загружаем UID map из корневой папки пользователя
        uid_map = load_uid_map(user_email_path)

        for folder in ["cur", "new"]:
            folder_path = os.path.join(user_email_path, folder)
            if not os.path.exists(folder_path):
                print(f"📁 Папка '{folder}' не найдена для {user_email_dir}")
                continue

            print(f"📂 Читаю папку: {folder_path}")

            for filename in os.listdir(folder_path):
                file_path = os.path.join(folder_path, filename)
                if not os.path.isfile(file_path):
                    continue

                if already_indexed(cursor, file_path):
                    print(f"🟨 Уже проиндексировано: {file_path}")
                    continue

                index_mail_file(cursor, file_path, user_id, uid_map)
                indexed_count += 1

        print(f"📨 Найдено новых писем для {user_email_dir}: {indexed_count}")

    db.commit()
    cursor.close()
    db.close()

    print(f"\n📬 Всего проиндексировано новых писем: {indexed_count}")
    print("✅ Индексация завершена.")

if __name__ == "__main__":
    main()

🛠 Запуск парсера

sudo -u www-data /opt/maildir_venv/bin/python /usr/local/bin/maildir_multiuser_indexer.py

🔐 Шаг 3: Сохраняем логин пользователя при входе в Roundcube

Правим файл:

После строки:

$_SESSION['user_id'] = $user_id;

Добавляем:

file_put_contents("/tmp/roundcube_login_{$_SERVER['REMOTE_ADDR']}.txt", $this->get_user_name());

🔎 Шаг 4: PHP-скрипт поиска — `search_email_best.php`

<?php
$ip = $_SERVER['REMOTE_ADDR'];
$login_file = "/tmp/roundcube_login_{$ip}.txt";

if (!file_exists($login_file)) {
    die("❌ Вы не авторизованы");
}
$email = trim(file_get_contents($login_file));

try {
    $pdo = new PDO("mysql:host=localhost;dbname=roundcubemail;charset=utf8mb4", "roundcube", "roundcube123");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (\PDOException $e) {
    die('Ошибка подключения к БД: ' . $e->getMessage());
}

$search = $_GET['q'] ?? '';
$results = [];

if ($search && strlen($search) >= 3) {
    $stmt = $pdo->prepare("
        SELECT
            ei.id,
            u.username AS email,
            ei.subject,
            ei.from_email,
            ei.body,
            MATCH(ei.body) AGAINST(:search IN NATURAL LANGUAGE MODE) AS relevance
        FROM email_index ei
        JOIN users u ON ei.user_id = u.user_id
        WHERE MATCH(ei.body) AGAINST(:search IN NATURAL LANGUAGE MODE)
          AND u.username = :email
        ORDER BY relevance DESC
        LIMIT 50
    ");
    $stmt->execute([':search' => $search, ':email' => $email]);
    $results = $stmt->fetchAll();
}

🧩 Шаг 5: Встраиваем поиск как iframe в Roundcube

Редактируем тему Roundcube:

/var/www/html/webmail/skins/elastic/templates/mail.html

Вставляем перед

:

&lt;iframe src="/webmail/search_email_best.php"
         style="width:100%; height:150px; border:none;"
         scrolling="yes"&gt;
&lt;/iframe&gt;

🔄 Шаг 6: (Опционально) Автоматизация индексации

Добавьте в crontab -e:

0 2 * * * sudo -u www-data /opt/maildir_venv/bin/python /usr/local/bin/maildir_multiuser_indexer.py

🎉 Готово!

Теперь у вас:

  • 🔍 Full-text поиск по письмам
  • 🔒 Только для авторизованного пользователя
  • 🖼️ iframe сверху Roundcube
  • 🧠 Работает без плагинов

Комментарии

Пока нет комментариев. Будьте первым!

Оставить комментарий

← Назад к списку

Важно: Блог-эксперимент

Блог только запустил, все статьи генерирую через нейросеть т.к. лень, возможны ошибки. Просто чтобы вы знали и не запускали ядерный реактор по моим статьям ))
Если у вас есть вопросы, или Нашли неточность? пишите в коментах — вместе поправим и сделаем статью более качественной. Я лично объясню нюансы из практики.

Посетителей сегодня: 0


кто я | книга | контакты без контактов

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA