166 lines
6.5 KiB
Python
166 lines
6.5 KiB
Python
import os
|
||
import time
|
||
import traceback
|
||
import re
|
||
import email
|
||
import logging
|
||
from io import BytesIO
|
||
from imapclient import IMAPClient
|
||
from telegram import Bot
|
||
from email.header import decode_header
|
||
from email.utils import collapse_rfc2231_value
|
||
from dotenv import load_dotenv
|
||
|
||
# === ЗАГРУЗКА КОНФИГА ===
|
||
load_dotenv()
|
||
|
||
GMAIL_USER = os.getenv("GMAIL_USER")
|
||
GMAIL_PASS = os.getenv("GMAIL_PASS")
|
||
TOKEN = os.getenv("TOKEN")
|
||
CHAT_ID = os.getenv("CHAT_ID")
|
||
ALLOWED_SENDER = os.getenv("ALLOWED_SENDER")
|
||
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", 60))
|
||
|
||
# === НАСТРОЙКА ЛОГГЕРА ===
|
||
os.makedirs("logs", exist_ok=True)
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s | %(levelname)-8s | %(message)s",
|
||
handlers=[
|
||
logging.FileHandler("logs/app.log", encoding="utf-8"),
|
||
logging.StreamHandler()
|
||
]
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# === TELEGRAM BOT ===
|
||
bot = Bot(token=TOKEN)
|
||
|
||
|
||
# === ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ===
|
||
def escape_markdown(text):
|
||
if not text:
|
||
return ""
|
||
escape_chars = r'_*[]()~`>#+-=|{}.!'
|
||
return re.sub(f'([{re.escape(escape_chars)}])', r'\\\1', text)
|
||
|
||
|
||
def decode_mime_words(s):
|
||
if not s:
|
||
return ""
|
||
decoded_fragments = decode_header(s)
|
||
result = ""
|
||
for fragment, encoding in decoded_fragments:
|
||
if isinstance(fragment, bytes):
|
||
encoding = encoding or "utf-8"
|
||
try:
|
||
fragment = fragment.decode(encoding, errors="ignore")
|
||
except Exception as e:
|
||
logger.debug(f"Ошибка декодирования MIME-фрагмента: {e}")
|
||
fragment = fragment.decode("utf-8", errors="ignore")
|
||
result += fragment
|
||
return result
|
||
|
||
|
||
def get_filename(part):
|
||
filename = part.get_filename()
|
||
if filename:
|
||
filename = collapse_rfc2231_value(filename)
|
||
filename = decode_mime_words(filename)
|
||
return filename
|
||
|
||
|
||
# === ОСНОВНАЯ ЛОГИКА ===
|
||
def fetch_new_emails():
|
||
try:
|
||
logger.info("Подключение к Gmail IMAP...")
|
||
with IMAPClient("imap.gmail.com", ssl=True) as server:
|
||
server.login(GMAIL_USER, GMAIL_PASS)
|
||
logger.info("Успешный вход в Gmail")
|
||
|
||
server.select_folder("INBOX")
|
||
messages = server.search(["UNSEEN"])
|
||
logger.info(f"Найдено новых писем: {len(messages)}")
|
||
|
||
for uid, msg_data in server.fetch(messages, ["RFC822"]).items():
|
||
try:
|
||
msg = email.message_from_bytes(msg_data[b"RFC822"])
|
||
from_raw = decode_mime_words(msg.get("from", ""))
|
||
subject = decode_mime_words(msg.get("subject", ""))
|
||
logger.info(f"Обработка письма: '{subject}' от {from_raw}")
|
||
|
||
# Фильтр по адресу
|
||
if ALLOWED_SENDER and ALLOWED_SENDER.lower() not in from_raw.lower():
|
||
logger.info(f"Пропущено письмо от неразрешённого адреса: {from_raw}")
|
||
continue
|
||
|
||
text = ""
|
||
attachments = []
|
||
|
||
if msg.is_multipart():
|
||
for part in msg.walk():
|
||
content_type = part.get_content_type()
|
||
disposition = part.get("Content-Disposition", "")
|
||
if content_type == "text/plain" and "attachment" not in disposition:
|
||
try:
|
||
text += part.get_payload(decode=True).decode(errors="ignore")
|
||
except Exception as e:
|
||
logger.warning(f"Ошибка чтения текстовой части: {e}")
|
||
elif "attachment" in disposition:
|
||
filename = get_filename(part)
|
||
payload = part.get_payload(decode=True)
|
||
if filename and payload:
|
||
attachments.append((filename, payload))
|
||
else:
|
||
text = msg.get_payload(decode=True).decode(errors="ignore")
|
||
|
||
# Формируем сообщение
|
||
message_text = f"📧 *{escape_markdown(subject)}*\nОт: {escape_markdown(from_raw)}\n\n{escape_markdown(text[:3500])}"
|
||
|
||
# Отправка текста
|
||
try:
|
||
bot.send_message(chat_id=CHAT_ID, text=message_text, parse_mode="MarkdownV2")
|
||
logger.info("Сообщение успешно отправлено в Telegram")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при отправке текста письма в Telegram: {e}")
|
||
|
||
# Отправка вложений
|
||
for filename, payload in attachments:
|
||
try:
|
||
bio = BytesIO(payload)
|
||
bio.name = filename
|
||
bot.send_document(chat_id=CHAT_ID, document=bio)
|
||
logger.info(f"Вложение отправлено: {filename}")
|
||
except Exception as e:
|
||
logger.error(f"Не удалось отправить вложение {filename}: {e}")
|
||
|
||
# Отмечаем как прочитанное
|
||
server.add_flags(uid, [b'\\Seen'])
|
||
logger.info("Письмо отмечено как прочитанное")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при обработке письма UID {uid}: {e}")
|
||
traceback.print_exc()
|
||
|
||
server.logout()
|
||
logger.info("Отключено от Gmail")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при подключении к Gmail: {e}")
|
||
traceback.print_exc()
|
||
|
||
|
||
# === ТОЧКА ВХОДА ===
|
||
if __name__ == "__main__":
|
||
logger.info("🚀 Сервис пересылки писем запущен")
|
||
while True:
|
||
try:
|
||
fetch_new_emails()
|
||
except Exception as e:
|
||
logger.error(f"Необработанная ошибка: {e}")
|
||
traceback.print_exc()
|
||
logger.info(f"Ожидание {CHECK_INTERVAL} секунд до следующей проверки...")
|
||
time.sleep(CHECK_INTERVAL)
|