diff --git a/main.py b/main.py index 4ccf422..37758f4 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,9 @@ import os import logging import subprocess +import email +from email import policy +from email.parser import BytesParser from telegram import Update from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes @@ -32,7 +35,8 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): 'Available commands:\n' '/start - Start the bot\n' '/help - Show help message\n' - '/checkmail - Check new emails' + '/checkmail - List new emails\n' + '/readmail - Read a specific email' ) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -41,7 +45,8 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): 'Available commands:\n' '/start - Start the bot\n' '/help - Show this help message\n' - '/checkmail - Check new emails in your mailbox\n' + '/checkmail - List new emails in your mailbox\n' + '/readmail - Read a specific email (e.g., /readmail 1)\n' '\nJust send me any text and I will echo it back!' ) @@ -75,15 +80,19 @@ async def check_mail(update: Update, context: ContextTypes.DEFAULT_TYPE): if not emails or emails == ['']: await update.message.reply_text("šŸ“­ No new emails!") + # Clear stored emails + context.user_data['emails'] = [] return + # Store email list in user context for readmail command + context.user_data['emails'] = emails + # Format the response response = f"šŸ“¬ You have {len(emails)} new email(s):\n\n" - for i, email in enumerate(emails, 1): + for i, email_file in enumerate(emails, 1): # Parse email filename to extract info - # Format: timestamp.M###P###.mail,S=size,W=size - parts = email.split(',') - timestamp_part = parts[0] if parts else email + parts = email_file.split(',') + timestamp_part = parts[0] if parts else email_file size_part = parts[1] if len(parts) > 1 else "" # Extract size in bytes @@ -100,6 +109,7 @@ async def check_mail(update: Update, context: ContextTypes.DEFAULT_TYPE): response += f"{i}. {timestamp_part} ({size_str})\n" + response += f"\nšŸ’” Use /readmail to read an email" await update.message.reply_text(response) except subprocess.TimeoutExpired: @@ -110,6 +120,97 @@ async def check_mail(update: Update, context: ContextTypes.DEFAULT_TYPE): logger.error(f"Error checking mail: {e}") await update.message.reply_text(f"āŒ An error occurred: {str(e)}") +async def read_mail(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Read a specific email by number.""" + # Check authorization + if not check_authorization(update): + await update.message.reply_text("āŒ You are not authorized to use this command.") + logger.warning(f"Unauthorized access attempt by user {update.effective_user.id}") + return + + # Check if user has checked mail first + 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.") + return + + # Parse email number from command + if not context.args: + await update.message.reply_text("āŒ Please specify an email number. Usage: /readmail 1") + return + + try: + email_num = int(context.args[0]) + emails = context.user_data['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)}") + return + + # Get the email filename (array is 0-indexed) + email_filename = emails[email_num - 1] + email_path = f"/var/mail/fymio.us/me/new/{email_filename}" + + # Read email content using docker exec + result = subprocess.run( + ['docker', 'exec', 'mailserver', 'cat', email_path], + capture_output=True, + timeout=10 + ) + + if result.returncode != 0: + await update.message.reply_text(f"āŒ Error reading email:\n`{result.stderr.decode()}`", parse_mode='Markdown') + return + + # Parse email + msg = BytesParser(policy=policy.default).parsebytes(result.stdout) + + # Extract email details + subject = msg.get('Subject', 'No Subject') + from_addr = msg.get('From', 'Unknown') + to_addr = msg.get('To', 'Unknown') + date = msg.get('Date', 'Unknown') + + # Get email body + body = "" + if msg.is_multipart(): + for part in msg.walk(): + content_type = part.get_content_type() + if content_type == "text/plain": + try: + body = part.get_content() + break + except: + pass + else: + try: + body = msg.get_content() + except: + body = "Could not extract email body" + + # Truncate body if too long (Telegram has message length limits) + max_body_length = 3000 + if len(body) > max_body_length: + body = body[:max_body_length] + "\n\n... (truncated)" + + # Format response + response = f"šŸ“§ **Email #{email_num}**\n\n" + response += f"**From:** {from_addr}\n" + response += f"**To:** {to_addr}\n" + response += f"**Date:** {date}\n" + response += f"**Subject:** {subject}\n" + response += f"\n━━━━━━━━━━━━━━━━━━\n\n" + response += body + + await update.message.reply_text(response, parse_mode='Markdown') + + except ValueError: + await update.message.reply_text("āŒ Invalid email number. Please provide a number.") + except subprocess.TimeoutExpired: + await update.message.reply_text("āŒ Command timed out while reading email.") + except Exception as e: + logger.error(f"Error reading mail: {e}") + await update.message.reply_text(f"āŒ An error occurred: {str(e)}") + async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE): """Echo the user message.""" await update.message.reply_text(f"You said: {update.message.text}") @@ -127,6 +228,7 @@ def main(): application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("checkmail", check_mail)) + application.add_handler(CommandHandler("readmail", read_mail)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) # Start the Bot