EmailBot/email_sender.py

148 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# email_sender.py
import os
import pickle
import base64
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.application import MIMEApplication
from typing import List
from queue_manager import QueuedFile
from timezone_utils import format_time
import logging
import mimetypes
logger = logging.getLogger(__name__)
# Пути к файлам аутентификации
TOKEN_FILE = 'token.pickle'
CREDENTIALS_FILE = 'credentials.json' # нужен для обновления токена
SCOPES = ['https://www.googleapis.com/auth/gmail.send']
def get_gmail_service():
"""
Загружает сохранённый токен и возвращает сервис Gmail API
Если токен протух и есть credentials.json — обновляет его
"""
creds = None
# Проверяем наличие token.pickle
if not os.path.exists(TOKEN_FILE):
raise Exception(
f"Файл {TOKEN_FILE} не найден! "
"Загрузи его на сервер или получи заново через get_token.py"
)
# Загружаем токен
with open(TOKEN_FILE, 'rb') as token:
creds = pickle.load(token)
# Если токен протух — обновляем
from google.auth.transport.requests import Request
if creds and creds.expired and creds.refresh_token:
logger.info("Токен протух, обновляем...")
creds.refresh(Request())
# Сохраняем обновлённый токен
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
logger.info("Токен обновлён и сохранён")
elif creds and creds.expired and not creds.refresh_token:
# Нет refresh_token — нужна повторная авторизация
if os.path.exists(CREDENTIALS_FILE):
logger.warning("Нет refresh_token, выполняем повторную авторизацию...")
from google_auth_oauthlib.flow import InstalledAppFlow
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
creds = flow.run_local_server(port=0)
with open(TOKEN_FILE, 'wb') as token:
pickle.dump(creds, token)
logger.info("Новый токен получен и сохранён")
else:
raise Exception(
f"Токен протух, а {CREDENTIALS_FILE} не найден. "
"Загрузи credentials.json для автообновления или получи новый token.pickle"
)
from googleapiclient.discovery import build
return build('gmail', 'v1', credentials=creds)
async def send_batch_to_email(target_email: str, files: List[QueuedFile]):
"""
Отправляет письмо с вложениями через Gmail API
Работает без SMTP портов (использует HTTPS 443)
Args:
target_email: Кому отправить
files: Список файлов из очереди
"""
if not target_email:
raise ValueError("Целевая почта не настроена")
if not files:
logger.warning("Нет файлов для отправки")
return
logger.info(f"Начинаем отправку {len(files)} файлов через Gmail API")
# Создаём письмо
msg = MIMEMultipart()
msg['To'] = target_email
msg['From'] = 'me' # 'me' = аккаунт, который авторизован в Gmail API
msg['Subject'] = f"📎 {len(files)} новых файлов"
# Формируем текст письма со списком файлов
total_size = sum(len(f.file_bytes) for f in files)
total_size_mb = total_size / (1024 * 1024)
body = f"Получено {len(files)} файлов.\n"
body += f"Общий размер: {total_size_mb:.2f} MB\n\n"
body += "Список вложений:\n"
body += "-" * 30 + "\n"
for idx, file in enumerate(files, 1):
file_size_kb = len(file.file_bytes) / 1024
if file_size_kb > 1024:
size_str = f"{file_size_kb / 1024:.2f} MB"
else:
size_str = f"{file_size_kb:.2f} KB"
body += f"{idx}. {file.filename} ({size_str})\n"
body += "\n---\n"
body += f"Время отправки: {format_time(files[0].timestamp)}\n"
body += f"Отправлено через Gmail API"
msg.attach(MIMEText(body, 'plain'))
# Добавляем вложения
for file in files:
try:
mime_type, encoding = mimetypes.guess_type(file.filename)
if mime_type and mime_type.startswith('image/'):
img_type = mime_type.split('/')[1]
mime_file = MIMEImage(file.file_bytes, _subtype=img_type)
else:
mime_file = MIMEApplication(file.file_bytes, _subtype='octet-stream')
mime_file.add_header('Content-Disposition', 'attachment', filename=file.filename)
msg.attach(mime_file)
logger.debug(f"Добавлено вложение: {file.filename}")
except Exception as e:
logger.error(f"Ошибка при добавлении вложения {file.filename}: {e}")
# Кодируем письмо в base64 для Gmail API
try:
raw_message = base64.urlsafe_b64encode(msg.as_bytes()).decode('utf-8')
except Exception as e:
raise Exception(f"Ошибка кодирования письма: {e}")
body_message = {'raw': raw_message}
# Отправляем через Gmail API
try:
service = get_gmail_service()
result = service.users().messages().send(userId='me', body=body_message).execute()
logger.info(f"✅ Письмо отправлено через Gmail API, ID: {result['id']}, файлов: {len(files)}")
except Exception as e:
raise Exception(f"Gmail API ошибка: {str(e)}")