Библиотека для прослушивания и обработки электронных писем через IMAP.
Project description
!!! Для личного использования в проекте vo0ov, но вы можете использовать библиотеку в своих проектах. !!!
Документация к библиотеке EmailListener
Содержание
- Введение
- Установка
- Использование
- Описание классов и методов
- Пример кода
- Дополнительно о настройке IMAP
Введение
Библиотека EmailListener предназначена для отслеживания новых писем в почтовом ящике через протокол IMAP. Она предоставляет удобный интерфейс для:
- Авторизации на почтовом сервере.
- Выбора почтового ящика (по умолчанию INBOX).
- Использования любого произвольного критерия поиска писем (например, UNSEEN, ALL, FROM "someone" и т.д.).
- Скачивания вложений с ограничением по типу файлов.
- Регистрации и вызова обработчиков (колбэков) при появлении новых писем.
Главная особенность — библиотека не содержит прямых вызовов print и exit, что позволяет гибко использовать её в любых проектах и контролировать поток вывода и логику завершения самостоятельно. Все ошибки, с которыми библиотека сталкивается, обрабатываются путём генерации исключений класса EmailListenerException.
Установка
pip install IMAP-EmailListener
Использование
- Импортируйте
EmailListenerи нужные классы из файла email_listener.py. - Создайте объект
EmailListener, передав необходимые настройки (логин, пароль, сервер, порт и т.д.). - Используйте декоратор
@mail_listener.on_new_email(...)для регистрации функций-обработчиков писем. - Вызовите метод
mail_listener.start(...)для запуска прослушивания. - Чтобы остановить прослушивание, примените
mail_listener.stop()из любого места кода (или ждитеKeyboardInterrupt, если вы запускаете прослушивание в основном потоке).
Описание классов и методов
Исключение EmailListenerException
- Наследуется от
Exception. - Предназначение: использоваться для всех ошибок, возникающих в процессе работы библиотеки (ошибки подключения, авторизации, чтения писем, декодирования, сохранения вложений и т.д.).
- Поведение: любая нештатная ситуация внутри
EmailListenerвызываетraise EmailListenerException(...)с описанием проблемы.
Класс EmailMessage
- Атрибуты:
title: str— тема письма.body: str— текстовое содержимое письма (извлекается в приоритете:text/plain, если нет, тоtext/html).sender: str— адрес (и возможное имя) отправителя.file_paths: List[str]— список путей к сохранённым вложениям на диске.
Данный класс является простым контейнером (Dataclass), хранящим информацию о конкретном письме, которое передаётся в каждый обработчик.
Класс EmailListener
Основной класс, обеспечивающий:
- Подключение и авторизацию на почтовом сервере (через IMAP).
- Периодический (с помощью цикла) опрос новых писем.
- Скачивание вложений (при необходимости).
- Вызов всех зарегистрированных обработчиков.
Конструктор
def __init__(
self,
email: str,
password: str,
server: str = 'imap.mail.ru',
port: int = 993,
download_folder: Optional[str] = None,
accepted_extensions: Optional[List[str]] = None,
mailbox: str = 'INBOX',
search_criteria: str = 'UNSEEN'
):
...
- email: Ваш адрес почты (логин для IMAP).
- password: Пароль от почты (зачастую требуются специальные пароли приложений).
- server: Адрес IMAP-сервера (по умолчанию
imap.mail.ru). - port: Порт IMAP-сервера (по умолчанию
993). - download_folder: Папка для скачивания вложений. Если не указано, создаётся
downloadsв директории рядом с файлом email_listener.py. - accepted_extensions: Список разрешённых расширений файлов (например,
['.pdf', '.zip', '.jpg']). Если не указано, по умолчанию('.pdf', '.zip'). - mailbox: Почтовый ящик для прослушивания (по умолчанию
INBOX). - search_criteria: Критерий поиска писем в формате IMAP (по умолчанию
UNSEEN— не прочитанные письма). Примеры:'ALL'— все письма.'FROM "someone@example.com"'— письма от конкретного адреса.'SUBJECT "hello"'— письма с темой, содержащей "hello".
При инициализации создаются:
- Список обработчиков (handlers).
- Внутренняя переменная
_stop_flagдля плавной остановки.
on_new_email
def on_new_email(self, interval: int = 5) -> Callable[[Callable[[EmailMessage], Any]], Callable[[EmailMessage], Any]]:
...
- Описание: Декоратор, регистрирующий обработчики новых писем.
- Параметр
interval: int = 5служит лишь для наглядности — описывает, что обработчик будет вызываться в цикле, который проверяется каждые5секунд (по умолчанию). Технически этот параметр не используется внутриstart(), но он позволяет иметь несколько декораторов с разным интервалом, если вы захотите модифицировать логику. - Возвращает: функцию-декоратор, которая добавляет саму обёрнутую функцию в список
handlers. - Пример:
@mail_listener.on_new_email(interval=10) def my_handler(msg: EmailMessage): print("У меня есть письмо!", msg)
_decode_str
def _decode_str(self, value: Optional[str]) -> str:
...
- Описание: Метод, декодирующий заголовки (например, тему и отправителя) из MIME-формата (base64, квотированный-printable и т.д.).
- На вход: строка
value, которая может быть в любом виде илиNone. - На выход: обычная Python-строка в UTF-8 с заменой непредвиденных символов (
errors='replace'). - Генерирует
EmailListenerException, если что-то пошло не так в процессе декодирования.
_get_email_body
def _get_email_body(self, msg: Message) -> str:
...
- Описание: Извлекает тело письма (body) с приоритетом
text/plain. Если нет, берётtext/htmlи чистит его от HTML-тэгов черезBeautifulSoup. - На вход: объект
Messageиз модуляemail. - На выход: декодированная строка.
_save_attachment
def _save_attachment(self, part: Message) -> Optional[str]:
...
- Описание: Проверяет, является ли часть письма вложением (нужен
Content-Disposition) и неmultipart. Если расширение вложения подходит подaccepted_extensions, оно сохраняется на диск. - Возвращает: Путь к сохранённому файлу или
None, если нет файла или расширение не подходит. - Генерирует
EmailListenerExceptionпри ошибках записи на диск.
start
def start(self, check_interval: int = 5) -> None:
...
-
Описание: Запускает основной цикл прослушивания.
-
Параметр
check_interval(по умолчанию 5 сек): частота опроса IMAP-сервера. Внутри цикла:- Выбирается папка
mailbox. - Поисковые критерии:
search_criteria. - Для каждого найденного письма вызывается
email.message_from_bytes(...)и формируетсяEmailMessage. - Запускаются все обработчики из
handlers. - Пауза
time.sleep(check_interval).
- Выбирается папка
-
Остановка:
- Если пользователь нажмёт
Ctrl+C, сгенерируетсяKeyboardInterrupt, обёрнутый вEmailListenerException('Прослушивание почты остановлено пользователем'). - Вызов
stop()(см. ниже) установит_stop_flag = True, и цикл завершится без генерирования исключения.
- Если пользователь нажмёт
stop
def stop(self) -> None:
...
- Описание: Устанавливает флаг
_stop_flag = True, благодаря чему основной цикл вstart()завершится в ближайшем циклеwhile. - Где использовать: Можно вызывать из любого места, если у вас, например, есть внешний управляющий поток или логика, при которой нужно завершить прослушивание писем без прерывания клавиатурой.
Пример кода
Ниже приведён полный код файла email_listener.py со встроенным примером использования в блоке if __name__ == '__main__':.
import os
import time
import email
import imaplib
from typing import Callable, Any, List, Optional
from functools import wraps
from dataclasses import dataclass
from email.header import decode_header
from email.message import Message
from bs4 import BeautifulSoup
class EmailListenerException(Exception):
"""Базовое исключение для EmailListener."""
pass
@dataclass
class EmailMessage:
title: str
body: str
sender: str
file_paths: List[str]
class EmailListener:
def __init__(
self,
email: str,
password: str,
server: str = 'imap.mail.ru',
port: int = 993,
download_folder: Optional[str] = None,
accepted_extensions: Optional[List[str]] = None,
mailbox: str = 'INBOX',
search_criteria: str = 'UNSEEN'
):
self.email = email
self.password = password
self.server = server
self.port = port
self.handlers: List[Callable[[EmailMessage], Any]] = []
self.download_folder = (
download_folder
if download_folder
else os.path.join(os.path.dirname(__file__), 'downloads')
)
if not os.path.exists(self.download_folder):
try:
os.makedirs(self.download_folder, exist_ok=True)
except OSError as e:
raise EmailListenerException(
f'Не удалось создать папку для загрузки: {e}'
) from e
self.accepted_extensions = (
tuple(ext.lower() for ext in accepted_extensions)
if accepted_extensions
else ('.pdf', '.zip')
)
self.mailbox = mailbox
self.search_criteria = search_criteria
self._stop_flag = False
def on_new_email(
self, interval: int = 5
) -> Callable[[Callable[[EmailMessage], Any]], Callable[[EmailMessage], Any]]:
def decorator(func: Callable[[EmailMessage], Any]) -> Callable[[EmailMessage], Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
return func(*args, **kwargs)
self.handlers.append(wrapper)
return wrapper
return decorator
def _decode_str(self, value: Optional[str]) -> str:
if not value:
return ''
parts = []
for decoded, charset in decode_header(value):
if isinstance(decoded, bytes):
try:
parts.append(decoded.decode(charset or 'utf-8', errors='replace'))
except LookupError as e:
raise EmailListenerException(f'Ошибка декодирования заголовка: {e}') from e
else:
parts.append(decoded)
return ''.join(parts)
def _get_email_body(self, msg: Message) -> str:
if msg.is_multipart():
for part in msg.walk():
ctype = part.get_content_type()
if ctype == 'text/plain':
return part.get_payload(decode=True).decode(errors='replace')
elif ctype == 'text/html':
html = part.get_payload(decode=True).decode(errors='replace')
return BeautifulSoup(html, 'html.parser').get_text('\n', strip=True)
return msg.get_payload(decode=True).decode(errors='replace')
def _save_attachment(self, part: Message) -> Optional[str]:
filename = part.get_filename()
if filename:
decoded_filename = self._decode_str(filename)
if any(decoded_filename.lower().endswith(ext) for ext in self.accepted_extensions):
path = os.path.join(self.download_folder, decoded_filename)
try:
with open(path, 'wb') as f:
f.write(part.get_payload(decode=True))
except OSError as e:
raise EmailListenerException(
f'Не удалось сохранить вложение {decoded_filename}: {e}'
) from e
return path
return None
def start(self, check_interval: int = 5) -> None:
self._stop_flag = False
try:
mail = imaplib.IMAP4_SSL(self.server, self.port)
except Exception as e:
raise EmailListenerException(f'Ошибка подключения к серверу: {e}') from e
try:
mail.login(self.email, self.password)
except Exception as e:
raise EmailListenerException(f'Ошибка авторизации: {e}') from e
try:
while not self._stop_flag:
try:
mail.select(self.mailbox)
except Exception as e:
raise EmailListenerException(f'Ошибка выбора почтового ящика: {e}') from e
try:
_, email_ids = mail.search(None, self.search_criteria)
except Exception as e:
raise EmailListenerException(f'Ошибка поиска писем: {e}') from e
for eid in email_ids[0].split():
try:
_, email_data = mail.fetch(eid, '(RFC822)')
except Exception as e:
raise EmailListenerException(f'Ошибка чтения письма: {e}') from e
try:
msg = email.message_from_bytes(email_data[0][1])
except Exception as e:
raise EmailListenerException(f'Ошибка формирования сообщения: {e}') from e
file_paths = []
for part in msg.walk():
if part.get_content_maintype() != 'multipart' and part.get('Content-Disposition'):
saved_path = self._save_attachment(part)
if saved_path:
file_paths.append(saved_path)
email_message = EmailMessage(
title=self._decode_str(msg.get('Subject')),
sender=self._decode_str(msg.get('From')),
body=self._get_email_body(msg),
file_paths=file_paths
)
for handler in self.handlers:
handler(email_message)
time.sleep(check_interval)
except KeyboardInterrupt:
raise EmailListenerException('Прослушивание почты остановлено пользователем')
finally:
try:
mail.logout()
except Exception:
pass
def stop(self) -> None:
self._stop_flag = True
if __name__ == '__main__':
def main():
mail_listener = EmailListener(
email='EMAIL',
password='PASSWORD',
server='imap.mail.ru',
port=993,
download_folder='/path/to/custom/folder',
accepted_extensions=['.jpg', '.pdf', '.zip'],
mailbox='INBOX',
search_criteria='UNSEEN'
)
@mail_listener.on_new_email(interval=5)
def print_all_emails(message: EmailMessage):
print('\n' + '=' * 50)
print(f'Тема: {message.title}')
print(f'От: {message.sender}')
print('\nТекст письма:')
print('-' * 20)
print(message.body)
if message.file_paths:
print('\nВложения:')
for path in message.file_paths:
print(f'- {path}')
print('=' * 50)
@mail_listener.on_new_email()
def handle_important_emails(message: EmailMessage):
if 'important@example.com' in message.sender.lower():
print(f'\nПолучено важное письмо: {message.title}')
attachments_count = 0
@mail_listener.on_new_email()
def count_attachments(message: EmailMessage):
nonlocal attachments_count
if message.file_paths:
attachments_count += len(message.file_paths)
print(f'\nВсего получено вложений: {attachments_count}')
print('Запуск прослушивания почты... Нажмите Ctrl+C или вызовите mail_listener.stop() для остановки.')
try:
mail_listener.start()
except EmailListenerException as exc:
print(f'\nОшибка в работе EmailListener: {exc}')
finally:
mail_listener.stop()
main()
Дополнительно о настройке IMAP
- На большинстве почтовых сервисов для IMAP может потребоваться включить IMAP-доступ в настройках аккаунта.
- Часто требуется пароль приложений (application password), а не основной пароль, особенно для сервисов, поддерживающих двухфакторную аутентификацию.
- Если вы используете Gmail, IMAP-сервер обычно
imap.gmail.com, порт993, и обязательно включенный IMAP в настройках Gmail.
Спасибо за использование EmailListener! Если возникнут вопросы или проблемы, вы можете:
- Создать issue (если используете репозиторий на GitHub).
- Написать автору напрямую.
- Сделать pull request с улучшениями.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file imap_emaillistener-1.0.0.tar.gz.
File metadata
- Download URL: imap_emaillistener-1.0.0.tar.gz
- Upload date:
- Size: 15.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.0.1 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b203b8cff1f54725ea15145867355d90387ac4cd7703508263e35ae897303973
|
|
| MD5 |
29f5e8daeed57749636e3cea934c0af6
|
|
| BLAKE2b-256 |
303a1622693131c3295d312f951bbcb607c596cd120059fc72e609e953bc09b7
|
File details
Details for the file IMAP_EmailListener-1.0.0-py3-none-any.whl.
File metadata
- Download URL: IMAP_EmailListener-1.0.0-py3-none-any.whl
- Upload date:
- Size: 12.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.0.1 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bb8c92c60533506a52aa7c004cef08436f1588cef7f36340d582876cd9234912
|
|
| MD5 |
de1d32bc2f79b9bc06b6c9e5f5ec35a1
|
|
| BLAKE2b-256 |
6f67e39880fce1b5c26aa914f294ba7f2459600d8dec36c7b60965b4db516cdb
|