commit d242a0928199c420cc3c1f0de0148cc6514fac7c Author: Serafim Date: Sat Nov 1 13:22:58 2025 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..449b450 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["python", "app.py"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..7341fbe --- /dev/null +++ b/app.py @@ -0,0 +1,165 @@ +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) diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..1a5ae17 --- /dev/null +++ b/compose.yml @@ -0,0 +1,6 @@ +services: + email_forwarder: + build: . + container_name: email_forwarder + env_file: .env + restart: always diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..31c2b38 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +python-telegram-bot==13.15 +imapclient==2.3.1 +python-dotenv==1.0.1