# 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)}")