This commit is contained in:
2026-01-27 16:14:48 +03:00
parent 35e7ab6066
commit 2d0e6b95a7
2 changed files with 156 additions and 57 deletions

211
main.py
View File

@@ -2,10 +2,12 @@ import os
import logging import logging
import subprocess import subprocess
import email import email
import tempfile
import html2text
from email import policy from email import policy
from email.parser import BytesParser from email.parser import BytesParser
from telegram import Update from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes
# Enable logging # Enable logging
logging.basicConfig( logging.basicConfig(
@@ -30,32 +32,59 @@ def check_authorization(update: Update) -> bool:
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a message when the command /start is issued.""" """Send a message when the command /start is issued."""
keyboard = [
[InlineKeyboardButton("📬 Check Mail", callback_data="checkmail")],
[InlineKeyboardButton("❓ Help", callback_data="help")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text( await update.message.reply_text(
'Hi! I am Fymious Bot. Send me any message and I will echo it back!\n\n' 'Hi! I am Fymious Bot 🤖\n\n'
'Available commands:\n' 'Use the buttons below to check your emails!',
'/start - Start the bot\n' reply_markup=reply_markup
'/help - Show help message\n'
'/checkmail - List new emails\n'
'/readmail <number> - Read a specific email'
) )
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Send a message when the command /help is issued.""" """Send a message when the command /help is issued."""
await update.message.reply_text( query = update.callback_query
if query:
await query.answer()
message = query.message
else:
message = update.message
keyboard = [[InlineKeyboardButton("📬 Check Mail", callback_data="checkmail")]]
reply_markup = InlineKeyboardMarkup(keyboard)
help_text = (
'Available commands:\n' 'Available commands:\n'
'/start - Start the bot\n' '/start - Start the bot\n'
'/help - Show this help message\n' '/help - Show this help message\n'
'/checkmail - List new emails in your mailbox\n' '/checkmail - List new emails in your mailbox\n'
'/readmail <number> - Read a specific email (e.g., /readmail 1)\n' '/readmail <number> - Read a specific email\n\n'
'\nJust send me any text and I will echo it back!' 'Or just use the buttons below!'
) )
if query:
await query.edit_message_text(help_text, reply_markup=reply_markup)
else:
await message.reply_text(help_text, reply_markup=reply_markup)
async def check_mail(update: Update, context: ContextTypes.DEFAULT_TYPE): async def check_mail(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Check for new emails in the mailserver.""" """Check for new emails in the mailserver."""
query = update.callback_query
if query:
await query.answer()
message = query.message
user = query.from_user
else:
message = update.message
user = update.effective_user
# Check authorization # Check authorization
if not check_authorization(update): if not check_authorization(update):
await update.message.reply_text("❌ You are not authorized to use this command.") await message.reply_text("❌ You are not authorized to use this command.")
logger.warning(f"Unauthorized access attempt by user {update.effective_user.id}") logger.warning(f"Unauthorized access attempt by user {user.id}")
return return
try: try:
@@ -68,7 +97,7 @@ async def check_mail(update: Update, context: ContextTypes.DEFAULT_TYPE):
) )
if result.returncode != 0: if result.returncode != 0:
await update.message.reply_text( await message.reply_text(
f"❌ Error checking mail:\n`{result.stderr}`", f"❌ Error checking mail:\n`{result.stderr}`",
parse_mode='Markdown' parse_mode='Markdown'
) )
@@ -79,71 +108,97 @@ async def check_mail(update: Update, context: ContextTypes.DEFAULT_TYPE):
emails = [e.strip().strip("'") for e in emails if e.strip()] emails = [e.strip().strip("'") for e in emails if e.strip()]
if not emails or emails == ['']: if not emails or emails == ['']:
await update.message.reply_text("📭 No new emails!") keyboard = [[InlineKeyboardButton("🔄 Refresh", callback_data="checkmail")]]
# Clear stored emails reply_markup = InlineKeyboardMarkup(keyboard)
await message.reply_text("📭 No new emails!", reply_markup=reply_markup)
context.user_data['emails'] = [] context.user_data['emails'] = []
return return
# Store email list in user context for readmail command # Store email list in user context
context.user_data['emails'] = emails context.user_data['emails'] = emails
# Format the response # Format the response
response = f"📬 You have {len(emails)} new email(s):\n\n" response = f"📬 You have {len(emails)} new email(s):\n\n"
for i, email_file in enumerate(emails, 1): for i, email_file in enumerate(emails, 1):
# Parse email filename to extract info
parts = email_file.split(',') parts = email_file.split(',')
timestamp_part = parts[0] if parts else email_file
size_part = parts[1] if len(parts) > 1 else "" size_part = parts[1] if len(parts) > 1 else ""
# Extract size in bytes
size_bytes = ""
if size_part.startswith('S='): if size_part.startswith('S='):
size_bytes = size_part[2:]
try: try:
size_kb = int(size_bytes) / 1024 size_kb = int(size_part[2:]) / 1024
size_str = f"{size_kb:.1f} KB" size_str = f"{size_kb:.1f} KB"
except ValueError: except ValueError:
size_str = size_bytes + " bytes" size_str = "?"
else: else:
size_str = "unknown size" size_str = "?"
response += f"{i}. {timestamp_part} ({size_str})\n" response += f"{i}. Email ({size_str})\n"
response += f"\n💡 Use /readmail <number> to read an email" # Create inline keyboard with buttons for each email
await update.message.reply_text(response) keyboard = []
# Add buttons in rows of 2
for i in range(1, len(emails) + 1, 2):
row = [InlineKeyboardButton(f"📧 #{i}", callback_data=f"read_{i}")]
if i + 1 <= len(emails):
row.append(InlineKeyboardButton(f"📧 #{i+1}", callback_data=f"read_{i+1}"))
keyboard.append(row)
# Add refresh button
keyboard.append([InlineKeyboardButton("🔄 Refresh", callback_data="checkmail")])
reply_markup = InlineKeyboardMarkup(keyboard)
if query:
await query.edit_message_text(response, reply_markup=reply_markup)
else:
await message.reply_text(response, reply_markup=reply_markup)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
await update.message.reply_text("❌ Command timed out. Mail server might be slow.") await message.reply_text("❌ Command timed out. Mail server might be slow.")
except FileNotFoundError: except FileNotFoundError:
await update.message.reply_text("❌ Docker command not found. Is Docker installed?") await message.reply_text("❌ Docker command not found. Is Docker installed?")
except Exception as e: except Exception as e:
logger.error(f"Error checking mail: {e}") logger.error(f"Error checking mail: {e}")
await update.message.reply_text(f"❌ An error occurred: {str(e)}") await message.reply_text(f"❌ An error occurred: {str(e)}")
async def read_mail(update: Update, context: ContextTypes.DEFAULT_TYPE): async def read_mail_handler(update: Update, context: ContextTypes.DEFAULT_TYPE, email_num: int = None):
"""Read a specific email by number.""" """Read a specific email by number."""
query = update.callback_query
if query:
await query.answer()
message = query.message
user = query.from_user
else:
message = update.message
user = update.effective_user
# Check authorization # Check authorization
if not check_authorization(update): if not check_authorization(update):
await update.message.reply_text("❌ You are not authorized to use this command.") await message.reply_text("❌ You are not authorized to use this command.")
logger.warning(f"Unauthorized access attempt by user {update.effective_user.id}") logger.warning(f"Unauthorized access attempt by user {user.id}")
return return
# Check if user has checked mail first # Check if user has checked mail first
if 'emails' not in context.user_data or not context.user_data['emails']: if 'emails' not in context.user_data or not context.user_data['emails']:
await update.message.reply_text("📭 Please use /checkmail first to see available emails.") await message.reply_text("📭 Please use /checkmail first to see available emails.")
return return
# Parse email number from command # Get email number from callback or command args
if not context.args: if email_num is None:
await update.message.reply_text("❌ Please specify an email number. Usage: /readmail 1") if not context.args:
return await message.reply_text("❌ Please specify an email number. Usage: /readmail 1")
return
try:
email_num = int(context.args[0])
except ValueError:
await message.reply_text("❌ Invalid email number. Please provide a number.")
return
try: try:
email_num = int(context.args[0])
emails = context.user_data['emails'] emails = context.user_data['emails']
if email_num < 1 or email_num > len(emails): if email_num < 1 or email_num > len(emails):
await update.message.reply_text(f"❌ Invalid email number. Please choose between 1 and {len(emails)}") await message.reply_text(f"❌ Invalid email number. Please choose between 1 and {len(emails)}")
return return
# Get the email filename (array is 0-indexed) # Get the email filename (array is 0-indexed)
@@ -158,7 +213,7 @@ async def read_mail(update: Update, context: ContextTypes.DEFAULT_TYPE):
) )
if result.returncode != 0: if result.returncode != 0:
await update.message.reply_text(f"❌ Error reading email:\n`{result.stderr.decode()}`", parse_mode='Markdown') await message.reply_text(f"❌ Error reading email:\n`{result.stderr.decode()}`", parse_mode='Markdown')
return return
# Parse email # Parse email
@@ -170,46 +225,87 @@ async def read_mail(update: Update, context: ContextTypes.DEFAULT_TYPE):
to_addr = msg.get('To', 'Unknown') to_addr = msg.get('To', 'Unknown')
date = msg.get('Date', 'Unknown') date = msg.get('Date', 'Unknown')
# Get email body # Get email body - handle both plain text and HTML
body = "" body = ""
html_body = ""
if msg.is_multipart(): if msg.is_multipart():
for part in msg.walk(): for part in msg.walk():
content_type = part.get_content_type() content_type = part.get_content_type()
if content_type == "text/plain": if content_type == "text/plain" and not body:
try: try:
body = part.get_content() body = part.get_content()
break except:
pass
elif content_type == "text/html" and not html_body:
try:
html_body = part.get_content()
except: except:
pass pass
else: else:
try: try:
body = msg.get_content() content_type = msg.get_content_type()
if content_type == "text/html":
html_body = msg.get_content()
else:
body = msg.get_content()
except: except:
body = "Could not extract email body" body = "Could not extract email body"
# If we have HTML but no plain text, convert HTML to text
if html_body and not body:
h = html2text.HTML2Text()
h.ignore_links = False
h.ignore_images = True
h.ignore_emphasis = False
body = h.handle(html_body)
# Truncate body if too long (Telegram has message length limits) # Truncate body if too long (Telegram has message length limits)
max_body_length = 3000 max_body_length = 3000
if len(body) > max_body_length: if len(body) > max_body_length:
body = body[:max_body_length] + "\n\n... (truncated)" body = body[:max_body_length] + "\n\n... (truncated)"
# Format response # Format response
response = f"📧 **Email #{email_num}**\n\n" response = f"📧 *Email #{email_num}*\n\n"
response += f"**From:** {from_addr}\n" response += f"*From:* {from_addr}\n"
response += f"**To:** {to_addr}\n" response += f"*To:* {to_addr}\n"
response += f"**Date:** {date}\n" response += f"*Date:* {date}\n"
response += f"**Subject:** {subject}\n" response += f"*Subject:* {subject}\n"
response += f"\n━━━━━━━━━━━━━━━━━━\n\n" response += f"\n━━━━━━━━━━━━━━━━━━\n\n"
response += body response += body
await update.message.reply_text(response, parse_mode='Markdown') # Add back button
keyboard = [[InlineKeyboardButton("⬅️ Back to List", callback_data="checkmail")]]
reply_markup = InlineKeyboardMarkup(keyboard)
if query:
await query.edit_message_text(response, parse_mode='Markdown', reply_markup=reply_markup)
else:
await message.reply_text(response, parse_mode='Markdown', reply_markup=reply_markup)
except ValueError:
await update.message.reply_text("❌ Invalid email number. Please provide a number.")
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
await update.message.reply_text("❌ Command timed out while reading email.") await message.reply_text("❌ Command timed out while reading email.")
except Exception as e: except Exception as e:
logger.error(f"Error reading mail: {e}") logger.error(f"Error reading mail: {e}", exc_info=True)
await update.message.reply_text(f"❌ An error occurred: {str(e)}") await message.reply_text(f"❌ An error occurred: {str(e)}")
async def read_mail(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Read mail command handler."""
await read_mail_handler(update, context)
async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle button callbacks."""
query = update.callback_query
if query.data == "checkmail":
await check_mail(update, context)
elif query.data == "help":
await help_command(update, context)
elif query.data.startswith("read_"):
email_num = int(query.data.split("_")[1])
await read_mail_handler(update, context, email_num)
else:
await query.answer("Unknown action")
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE): async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Echo the user message.""" """Echo the user message."""
@@ -229,6 +325,7 @@ def main():
application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("checkmail", check_mail)) application.add_handler(CommandHandler("checkmail", check_mail))
application.add_handler(CommandHandler("readmail", read_mail)) application.add_handler(CommandHandler("readmail", read_mail))
application.add_handler(CallbackQueryHandler(button_callback))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
# Start the Bot # Start the Bot

View File

@@ -1 +1,3 @@
python-telegram-bot==21.0 python-telegram-bot==21.0
html2text==2024.2.26
playwright==1.49.0