init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -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"]
|
||||
165
app.py
Normal file
165
app.py
Normal file
@@ -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)
|
||||
6
compose.yml
Normal file
6
compose.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
email_forwarder:
|
||||
build: .
|
||||
container_name: email_forwarder
|
||||
env_file: .env
|
||||
restart: always
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
python-telegram-bot==13.15
|
||||
imapclient==2.3.1
|
||||
python-dotenv==1.0.1
|
||||
Reference in New Issue
Block a user