FTPoverEmail Beta v1.00





5.00/5 (1 vote)
FTPoverEmail Beta v1.00
I made a few more edits to FTPoverEmail. I got the SSH functionality working and fixed a couple of bugs for this next version. I’ve started studying for CCDP so my work on it has tailed off a fair amount. Here’s the next version with the bug fixes and SSH functionality. I called this one a beta in that at least it runs fully and that at least it works most of the time now :-D.
# __author__ = 'gecman'
import smtplib
import email
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import mimetypes
import os
import imaplib
import time
import sys
import subprocess
import signal
import urllib3
# TODO: Windows-1252 encoding adds an = sign which the program can't handle.
# Find a way to take care of it.
# TODO: Test more rapidly sent email
# TODO: Find a way to break interactive commands
# TODO: Ensure logging for all error messages
# The bash script checks progOverEmail2_new
# Global Variables
from_name = ''
to_name = ''
subject = ''
log_file = None
user = "your_email_here@gmail.com"
password = "your_password_here"
# sendresponse sends text based responses to the client
# response: The response text to send in the body of the mail
def sendresponse(response):
global log_file
# Create a response email from the response string
response_email = MIMEText(response)
# Configure the message header fields
response_email['Subject'] = subject
response_email['From'] = to_name
response_email['To'] = from_name
try:
# Declare an instance of the SMTP class and connect to the smtp server
smtp = smtplib.SMTP('smtp.gmail.com')
# Start the TLS connection
smtp.starttls()
# Login to the server
smtp.login(user, password)
smtp.sendmail(to_name, from_name, response_email.as_string())
smtp.close()
except smtplib.SMTPResponseException:
# The following lines dump the details of the error to stdout and log any issues
print(
"Server encountered the following error while attempting to
connect to SMTP server: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
log_file.write(time.strftime("%c") + " --------> " +
"Server encountered the following error while attempting to
connect to SMTP server: \nType: {0}\nValue: {1}\nTraceback: {2} \r\n".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# sendfile Instead of just sending text, sends a file. Primarily used for the FTP get functionality.
# file_name: The name of the file to be attached to the email.
def sendfile(file_name, optional_text=None):
global log_file
# Declare the mime class and message header fields
response = MIMEMultipart()
response['Subject'] = subject
response['From'] = to_name
response['To'] = from_name
# Check to see if the caller passed optional text to place in the e-mail,
# otherwise use the default response.
if bool(optional_text):
# Write the optional text into the e-mail instead of the default
response.attach(MIMEText(optional_text))
# Use the default response
else:
# Attach the text portion of the message. Alerts the user of the file name
# and that it is attached.
response.attach(MIMEText("Grabbed file " + file_name + ". See attachment."))
# Define the major and minor mime type
# attachment = MIMEBase('application', "octet-stream")
# This command uses the mimetypes module to attempt to guess the mimetype of the file.
# The strict=False argument
# tells the mimetypes module to include modules not officially registered with IANA
# The file_type variable will be in the format of a tuple #major type#/#sub type# , #encoding#
file_type = mimetypes.guess_type(file_name, strict=False)
main_type = None
# mimetypes.guess_type returns None if it doesn't recognize the file type.
# In this case I chose to use the generic
# application octect-stream type, which should be safe.
if file_type is None:
attachment = MIMEBase('application', 'octet-stream')
else:
main_type = file_type[0].split('/')[0]
sub_type = file_type[0].split('/')[1]
# This pulls the major type and sub type and splits them around the
# '/' character providing the major type in the
# first argument and the sub type in the second argument
attachment = MIMEBase(main_type, sub_type)
# Read the file from disk and set it as the payload for the email
# The with statement ensures the file is properly closed even if an exception is raised.
try:
f = open(file_name, "rb")
attachment.set_payload(f.read())
f.close()
except OSError:
print(
"Server encountered the following error while attempting to open
the file you asked for: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
log_file.write(time.strftime("%c") + " --------> " +
"Server encountered the following error while attempting to open the file
you asked for: \nType: {0}\nValue: {1}\nTraceback: {2}\r\n".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# TODO: Fix the fact that this shows the newlines as \n instead of properly adding newlines
sendresponse(
"Server encountered the following error while attempting to
open the file you asked for: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# Check to see if the attachment requires encoding
if main_type == 'application' or main_type == 'audio' or main_type == 'image':
# Encode the attachment
encoders.encode_base64(attachment)
# This sets the header. The output will be in the following format:
# Content-Disposition: attachment; file_name="the_file_name"
# The os.path.basename(file_name) portion strips the leading part of the
# file path and just gives the actual file name
attachment.add_header('Content-Disposition', 'attachment;
file_name="%s"' % os.path.basename(file_name))
# A ttach the attachment to the response message
response.attach(attachment)
try:
# Declare an instance of the SMTP class and connect to the smtp server
smtp = smtplib.SMTP('smtp.gmail.com')
# Start the TLS connection
smtp.starttls()
# Login to the server
smtp.login(user, password)
# Send the response email
smtp.sendmail(to_name, from_name, response.as_string())
# Close the smtp connection
smtp.close()
except smtplib.SMTPResponseException:
# The following lines dump the details of the error to stdout and log any issues
print(
"Server encountered the following error while attempting to connect to
SMTP server: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
log_file.write(time.strftime("%c") + " --------> " +
"Server encountered the following error while attempting to connect to
SMTP server: \nType: {0}\nValue: {1}\nTraceback: {2} \r\n".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# Responsible for connecting to the server via IMAP and actually grabbing the e-mail.
# It then passes the text or
# content of the e-mail to proccommand for processing.
def getemail():
# Declare as global variables. They are originally defined at the beginning.
# Without these the values won't
# carry over to other functions.
global from_name
global to_name
global subject
global log_file
try:
# Connect to Google's imap server
# TODO: This needs to be generic
mail = imaplib.IMAP4_SSL('imap.gmail.com')
# Login to the imap server
mail.login(user, password)
# List all mailbox (folder) names. In gmail these are called labels.
mail.list()
# Select the inbox folder
mail.select("inbox")
# Grab an ordered list of all mail. The search function returns an ordered list
# from newest to oldest. Only grab new mail.
result, data = mail.search(None, "UNSEEN")
# Check to see if there was new mail or not. If there wasn't return None.
if not data[0].split():
return None
# Data[0] is a list of email IDs in a space separated string.
id_list = data[0].split()
# Grab the latest email
# TODO add support for queuing
latest_email_id = id_list[-1]
# Fetch the email body for the given ID
result, data = mail.fetch(latest_email_id, "(RFC822)")
raw_email = data[0][1]
# Grab the body of the email including the raw text, headers, and payloads
# The following line converts the email from a string into an email message object
email_message = email.message_from_bytes(raw_email)
log_file.write(time.strftime("%c") + " --------> " + "-----Receiving E-mail-----\r\n")
print("-----Receiving E-mail-----\r\n")
if email_message['To'] is not None:
print(email_message['To'] + "\r\n")
log_file.write(time.strftime("%c") + " --------> " + email_message['To'] + "\r\n")
to_name = email_message['To']
else:
print("Received email with an empty To address. Ignoring the message.\r\n")
log_file.write(time.strftime("%c") + " --------> " +
"Received email with an empty To address. Ignoring the message.\r\n")
return
if email_message['Return-Path'] is not None:
print(email_message['Return-Path'])
log_file.write(time.strftime("%c") + " --------> " +
email_message['Return-Path'] + "\r\n")
from_name = email_message['Return-Path']
else:
log_file.write(time.strftime("%c") + " --------> " +
"Received email with an empty Return-Path. Ignoring the message.\r\n")
print("Received email with an empty Return-Path. Ignoring the message.")
return
if email_message['Subject'] is not None:
# If there is already a Re in the subject don't add it again
if email_message['Subject'].find("Re:") == -1:
subject = "Re: " + email_message['Subject']
else:
subject = email_message['Subject']
else:
subject = ''
print(subject + "\r\n")
log_file.write(time.strftime("%c") + " --------> " + subject + "\r\n")
# Check to see if this is a multipart email message. If it isn't just return the text portion.
if email_message.get_content_maintype() == 'multipart':
# Used to determine whether a command was found in the email
found_command = False
# Walk the various sections of the email.
# The enumerate function here returns a number for each email part walked through.
# IE 0 for the first part in
# the email, 1 for the next, and so on.
for index, part in enumerate(email_message.walk()):
# The multipart section is just a container so we can skip it.
if part.get_content_maintype() is 'multipart':
continue
# Check to see if we have reached the text body of the message
if part.get_content_type() == 'text/plain':
found_command = True
message = part.get_payload()
proccommand(message.splitlines(), index, email_message)
# This does not need to continue after this run
break
# If a command was found and successfully processed simply return
if found_command:
return
else:
sendresponse("Error: No command was found in multipart email. Was it there?")
print("Error: Server encountered a multipart email that did not appear
to have a command in it")
# Return the text payload
elif email_message.get_content_maintype() == 'text':
print(email_message.get_payload())
# Pass the message to proccommand
proccommand(email_message.get_payload().splitlines())
# It wasn't text - do not process the email
else:
print("Error processing email in getemail. Encountered unrecognized content type.")
log_file.write(time.strftime("%c") + " --------> " +
"Error processing email in getemail. Encountered unrecognized content type.\r\n")
sendresponse("Error processing email in getemail. Encountered unrecognized content type.")
return
log_file.write(time.strftime("%c") + " --------> " +
"-----End Message Receive-----\r\n\r\n")
except imaplib.IMAP4.error:
print(
"Unable to pull e-mail via IMAP. An error occured: \nType:
{0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# TODO: Fix the fact that this shows the newlines as \n instead of properly adding newlines
log_file.write(time.strftime("%c") + " --------> " +
"Unable to pull e-mail via IMAP. An error occured: \nType:
{0}\nValue: {1}\nTraceback: {2}\r\n".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# Processes the command retrieved from the email server
# message: The message from the email server in the form of [type] [command(s)]
# index: This is an index into a multipart email's parts. getemail may have
# already processed some of them and it is
# unnecessary to reprocess those parts
# email_message: This is an email_message passed from getemail.
# In terms of FTP, this is used for the put command
def proccommand(message, index=None, email_message=None):
global log_file
#found_command = False
# TODO These are temporary
command = message[0].split(' ')
message = message[0]
log_file.write(time.strftime("%c") + " --------> " + "Message: " + message + "\r\n")
# Used for logging to determine whether a command was successful or not
errors_occurred = False
# Process an FTP command
if "FTP" == command[0].upper():
# cd command ---------------------------
# Process the command "cd"
if command[1].lower() == "cd":
# Ensure the correct number of arguments was passed"
if len(command) != 3:
sendresponse("Error: Incorrect number of arguments for cd")
errors_occurred = True
# Make sure the path exists
elif not os.path.exists(command[2]):
sendresponse("Error: The path: \"" + command[2] + "\" does not exist")
errors_occurred = True
# Process the "cd" command
else:
os.chdir(command[2])
sendresponse("CD completed successfully. Directory is now: " + os.getcwd())
# dir command ---------------------------
# Process the command "dir"
elif command[1].lower() == "dir":
# Ensure the correct number of arguments was passed"
if len(command) < 2 or len(command) > 3:
sendresponse("Error: Incorrect number of arguments for dir")
errors_occurred = True
else:
# If no arguments are passed to dir then list the current directory
if len(command) == 2:
response = ""
for file in os.listdir(os.getcwd()):
# Contains newline delimited directory listing at end
response = response + file + "\n"
sendresponse(response)
# Process the dir command with directory argument
else:
# Make sure the path exists
if not os.path.exists(command[2]):
sendresponse("Error: The path: \"" + command[2] + "\" does not exist")
errors_occurred = True
# If the path does exist then list the directory
else:
response = ""
for file in os.listdir(command[2]):
# Contains newline delimited directory listing at end
response = response + file + "\n"
sendresponse(response)
# pwd command ---------------------------
# Process the pwd command
elif command[1].lower() == "pwd":
# Ensure correct number of arguments was passed
if len(command) != 2:
sendresponse("Error: Incorrect number of arguments for pwd")
errors_occurred = True
else:
# Get current directory and respond
sendresponse(os.getcwd())
# get command ---------------------------
# Process the get command
elif command[1].lower() == "get":
# TODO Consider any other downsides to this approach?
# Ensure correct number of arguments was passed. Must be at least 3
if len(command) < 3:
sendresponse("Error: Incorrect number of arguments for get")
errors_occurred = True
else:
# Make sure the file exists. Must include everything after the "FTP get" command
# The syntax " ".join(command[2:]) takes everything from the
# third argument on and joins it as one string
if not os.path.exists(" ".join(command[2:])):
sendresponse("Error: The path: \"" + " ".join(command[2:]) + "\" does not exist")
errors_occurred = True
else:
sendfile(" ".join(command[2:]))
# put command ---------------------------
elif command[1].lower() == "put":
# Ensure correct number of arguments was passed. Must be at least 3
if len(command) < 3:
sendresponse("Error: Incorrect number of arguments for put")
errors_occurred = True
else:
# There may be a better way to do this, but I'm using this to inform
# the functionality below whether it
# should actually execute. This defaults to true, but gets set to False
# if the path is determined to be
# non-valid
valid_path = True
# The line below splits command[2] into two pieces. For example say the path is
# C:\Users\randomUser\Downloads\tryMe.exe. The split command will split this into
# C:\Users\randomUser\Downloads\ and tryMe.exe.
# The [0] index grabs just the path prefix. In this case
# bool will return true IF there is a path prefix and false if there isn't.
# We don't want to check if
# the path exists if the user didn't provide any path
# (because a path of '' will return false. If the
# user didn't provide a path we want to just dump the file in the
# current directory whatever it is.
# The syntax " ".join(command[2:]) takes everything from the third argument
# on and joins it as one string
if bool(os.path.split(" ".join(command[2:]))[0]):
# Make sure the path exists exists
if not os.path.exists(os.path.split(" ".join(command[2:]))[0]):
valid_path = False
sendresponse(
"Error: The path: \"" + os.path.split(" ".join(command[2:]))[0] +
"\" does not exist")
errors_occurred = True
# Only execute this if a valid path is supplied (or no path as the case may be)
if valid_path:
# getemail already parsed some of the sections.
# The index comes from the getemail function and
# allows this section to skip the parts of the email that were already processed.
# email_message.walk() is a generator, but we want to index into it
# so we use the list function.
# This turns it into a list, which we are permitted to index into
for part in list(email_message.walk())[index:]:
# The multipart section is just a container so we can skip it.
if part.get_content_maintype() is 'multipart':
continue
# Grab the file name from the section
file_name = part.get_filename()
# Make sure the file name actually exists and if it does write the file.
if bool(file_name):
try:
file_descriptor = open(" ".join(command[2:]), 'wb')
# Get the payload of the email and write it.
# The decode=True argument basically says if
# the payload is encoded, decode it first otherwise,
# it gets returned as is.
file_descriptor.write(part.get_payload(decode=True))
file_descriptor.close()
sendresponse("Successfully processed the put command: " + message)
except OSError:
errors_occurred = True
print(
"Server encountered the following error while attempting to write
the file you uploaded: \nType: {0}\nValue: {1}\nTraceback:
{2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]),
str(sys.exc_info()[2])))
log_file.write(time.strftime("%c") + " --------> " +
"Server encountered the following error while attempting
to write the file you uploaded: \nType: {0}\nValue:
{1}\nTraceback: {2}\r\n".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]),
str(sys.exc_info()[2])))
# TODO: Fix the fact that this shows the newlines as \n
# instead of properly adding newlines
sendresponse(
"Server encountered the following error while attempting to
write the file you uploaded: \nType: {0}\nValue:
{1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]),
str(sys.exc_info()[2])))
# Break out of the for loop. We should only need to write one file.
break
# Delete a file with the "del" FTP command
# TODO: This requires more thorough testing
elif command[1].lower() == "del":
if len(command) == 3:
try:
os.remove(command[2])
sendresponse("Successfully deleted file: " + command[2])
except OSError:
print(
"Server encountered the following error while attempting to
delete the file: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
log_file.write(time.strftime("%c") + " --------> " +
"Server encountered the following error while attempting to
delete the file: \nType: {0}\nValue: {1}\nTraceback: {2}\r\n".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# TODO: Fix the fact that this shows the newlines as \n instead of
# properly adding newlines
sendresponse(
"Server encountered the following error while attempting to
delete the file: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
else:
sendresponse(
'Error: Incorrect number of arguments sent to the "del" command.
Syntax is "FTP del <filename"')
# TODO May not be necessary. Added for testing purposes.
# Kill the server
elif command[1].lower() == "kill":
log_file.close()
sys.exit(0)
# This is called if the FTP command does not match any correct command.
else:
errors_occurred = True
sendresponse("Error: That command did not match an FTP command.")
print("Error: That command did not match an FTP command.")
log_file.write(time.strftime("%c") + " --------> " +
"Error: That command did not match an FTP command.")
# Perform an operation over SSH
elif command[0].upper() == "SSH":
# TODO THIS MUST BE TESTED
# Ensure the user actually passed an argument to the SSH command
if len(command) < 2:
errors_occurred = True
sendresponse("Error: It does not appeared you supplied an argument for your SSH command.")
print("Error: It does not appear the user supplied an argument with an SSH command.\r\n")
log_file.write(time.strftime("%c") + " --------> " +
"Error: It does not appear the user supplied an argument with an SSH command.")
# This conditional handles the event that the user wants to receive the output
# of their command back in binary
# form.
elif command[1].upper() == "BINARY":
# Make sure the user supplied an argument after the BINARY option
if len(command) > 2:
# Attempt to run the command passed to the shell. Output is saved by .check_output
# as a byte string
try:
command_output = subprocess.check_output(command[2], shell=True)
# Write the output of the command to a file then send the file to the user
# as an attachment.
try:
file_descriptor = open("output.bin", 'wb')
# Get the payload of the email and write it. The decode=True argument
# basically says if
# the payload is encoded, decode it first otherwise, it gets returned as is.
file_descriptor.write(command_output)
file_descriptor.close()
sendfile("output.bin", "The output of your command \"" + command[2] +
"\" is attached.")
# Remove the file now that it is no longer needed
os.remove("output.bin")
except OSError:
errors_occurred = True
print(
"Server encountered the following error while attempting to write
the output to a file: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
log_file.write(time.strftime("%c") + " --------> " +
"Server encountered the following error while attempting to write
the output to a file: \nType: {0}\nValue: {1}\nTraceback: {2}\r\n".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# TODO: Fix the fact that this shows the newlines as \n instead of
# properly adding newlines
sendresponse(
"Server encountered the following error while attempting to write
the output to a file: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
# If the command fails, report an error
except subprocess.CalledProcessError:
sendresponse(
"Server encountered the following error while attempting to process
the command {3}: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]),
str(sys.exc_info()[2]), str(command[1])))
print(
"Server encountered the following error while attempting to process
the command {3}: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2]),
str(command[1])))
log_file.write(time.strftime("%c") + " --------> " +
"Server encountered the following error while attempting to process
the command {3}: \nType: {0}\nValue: {1}\nTraceback: {2}\r\n".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]),
str(sys.exc_info()[2]), str(command[1])))
else:
# Attempt to run the command passed to the shell.
# The below pipes everything into the command_output object.
# The command[1:] slice is necessary because we split everything based on spaces above.
# This combines all of
# the elements in the array after 1.
command_output = subprocess.Popen(" ".join(command[1:]),
shell=True,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
output = ""
# Print the lines from stderr into the response
for line in command_output.stderr:
output = output + str(line.rstrip())[2:-1] + '\n'
command_output.stdout.flush()
output += '\n\n'
# Print the lines from stdout into the response
for line in command_output.stdout:
output = output + str(line.rstrip())[2:-1] + '\n'
command_output.stdout.flush()
sendresponse("Successfully processed command: " +
message + "\r\n\r\n" + output)
# If the protocol or desired action did not match anything...
else:
sendresponse("Error: Bad or nonexistent command: " + message)
print("Server received an email which matched no commands.")
log_file.write(time.strftime("%c") + " --------> " +
"Server received an email which matched no commands.\r\n")
# Used server side for logging purposes
if errors_occurred:
print("Failed to process command \"" + message + "\"")
log_file.write(time.strftime("%c") + " --------> " +
"Failed to process command \"" + message + "\"\r\n")
else:
print("Successfully processed command: " + message)
log_file.write(time.strftime("%c") + " --------> " +
"Successfully processed command: " + message + "\r\n")
# Handler for when someone hits control-c while the server is running.
# We want to perform cleanup before the server
# exits.
def signal_handler(signal, frame):
print("Server exiting...")
log_file.write(time.strftime("%c") + " -------->
" + "Server shutting down...\r\n")
sys.exit(0)
# This is the main section
# Register the signal handler for control-c
signal.signal(signal.SIGINT, signal_handler)
# Open the log file for other functions to use
try:
# This opens the log file
log_file = open("log_file", 'a')
except OSError:
print(
"Server encountered the following error while attempting to open the
log file: \nType: {0}\nValue: {1}\nTraceback: {2}".format(
str(sys.exc_info()[0]), str(sys.exc_info()[1]), str(sys.exc_info()[2])))
sys.exit(1)
# This is the main loop
while 1:
getemail()
time.sleep(5)
log_file.close()