Python实现邮件控制Linux

本文介绍了一个使用Python编写的脚本,该脚本可以通过邮件接收命令并在Linux系统上执行,同时将执行结果回邮给发送者。脚本包括邮件收发模块,能过滤白名单之外的邮件,并且支持命令执行历史记录,防止重复执行。通过邮件控制Linux,可以在远程执行一些管理任务,例如获取主机IP等。
摘要由CSDN通过智能技术生成

前言

几年前买了个树莓派板子,一直都只是运行一个爬虫程序定期发邮件给自己,总感觉没有物尽其用。大多时间都不在板子旁边,有时候爬虫没运行也来也不知道。即使记住了外网IP重启路由IP也有可能会变(房东提供的公共路由器有点烂经常要重启)。
于是乎,有了这个脚本。
功能:通过接收邮件内容来执行命令。
原理:定期查看邮箱内的邮件,如果邮件主题为"cmd",并且邮件发件人在白名单里的,则把邮件主体内容当作命令在本机执行,并且将执行后的标准输出流/标准错误流以及状态码回复给邮件发送者,执行过的邮件保存在字典内,第二次查看邮件时不再执行(也可以直接调用dele方法删除已经执行过的邮件)。

标题是邮件控制Linux,其实Windows也可以的(有点标题党了),其它可以运行Python的平台都是可以的,这是语言跨平台的好处之一

源码

usage

usage: run.py [-h] --username USERNAME --password PASSWORD --whitelist
              WHITELIST [WHITELIST ...] [--refresh_rate REFRESH_RATE]

optional arguments:
  -h, --help            show this help message and exit
  --username USERNAME, -u USERNAME
                        sender email address.
  --password PASSWORD, -p PASSWORD
                        sender email password.
  --whitelist WHITELIST [WHITELIST ...], -w WHITELIST [WHITELIST ...]
                        email addresses:
  --refresh_rate REFRESH_RATE, -r REFRESH_RATE
                        refresh email time(minute)

如:

./run.py -u xxx@163.com -p 123456789 -w xxx@163.com xxx@qq.com 

-u xxx@163.com:用于接收命令的邮箱地址
-p 123456789: xxx@163.com邮箱的密码
-w xxx@163.com xxx@qq.com : 出于安全性考虑只有白名单内的邮件可以执行命令,多个地址用空格分隔(垃圾邮件盛行的年代,不加个白名单限制怎能行)

其它参数:
–refresh_rate: 刷新频率,即多少分钟查一次邮箱内容,默认30分钟查看一次

1.lib/mail.py

# -*- coding:utf-8 -*-
"""
1. smtp send email
2. pop3 receive email

author: zbc
"""
__author__: str = 'zbc'

import re
import os
import sys
import smtplib
import poplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.parser import Parser
from email.header import decode_header
# from email.header import Header

# from logger.logger import Logger

# logger = Logger.get_logger(__name__)


class MailManager:

    def __init__(self, username: str, password: str, **kwargs):
        self.username = username
        self.password = password

        # pop.163.com
        self.pop_host = kwargs.get('pop_host', "pop.{}".format(username.split("@")[-1]))
        # smtp.163.com
        self.smtp_host = kwargs.get('smtp_host', "smtp.{}".format(username.split("@")[-1]))
        self.port = kwargs.get('port', 25)

        self.mail_box = None
        self.mail_link = None

        self.login()
        self.config_mail_box()

    # login email
    def login(self):
        try:
            self.mail_link = poplib.POP3_SSL(self.pop_host)
            self.mail_link.set_debuglevel(0)
            self.mail_link.user(self.username)
            self.mail_link.pass_(self.password)
            self.mail_link.list()
            print(u'login success!')

        except Exception as e:
            print(u'login fail! ' + str(e))
            # quit()
            sys.exit(1)

    def config_mail_box(self):
        try:
            self.mail_box = smtplib.SMTP(self.smtp_host, self.port)
            self.mail_box.login(self.username, self.password)
            print(u'config mailbox success!')

        except Exception as e:
            print(u'config mailbox fail! ' + str(e))
            sys.exit(1)

    @staticmethod
    def guess_charset(msg):
        # get charset from msg
        charset = msg.get_charset()
        if charset is None:
            # get charset from Content-Type field if charset if None
            content_type = msg.get('Content-Type', '').lower()
            pos = content_type.find('charset=')
            if pos >= 0:
                charset = content_type[pos + 8:].strip()
        return charset

    def parser_mail(self, msg):
        kv = {}
        for header in ['From', 'To', 'Cc', 'Subject', 'Date']:
            # continue if header not exists
            if not msg.get(header):
                continue

            for i in decode_header(msg.get(header)):
                (last_word, last_charset) = i
                if last_charset is not None:
                    last_word = last_word.decode(last_charset)
                elif isinstance(last_word, bytes):
                    last_word = last_word.decode()

                kv[header] = last_word
                pattern = r"[0-9a-zA-Z_.]{0,19}@[0-9a-zA-Z]{1,13}(?:\.[A-Za-z]+)+"
                if header in ["From", 'To', 'Cc'] and re.search(pattern, last_word, re.IGNORECASE):
                    kv[header] = list(set(re.findall("({})".format(pattern), last_word, re.IGNORECASE)))

        # MIMEMultipart obj
        if msg.is_multipart():
            parts = msg.get_payload()
            for n, part in enumerate(parts):
                print('part {}'.format(n))
                # print(part)
                # TODO: parse MIMEMultipart
                kv.update(self.parser_mail(part))
        # not MIMEMultipart obj
        else:
            content_type = msg.get_content_type()
            # if content_type == 'text/plain' or content_type == 'text/html':
            if content_type in ['text/plain', 'text/html']:
                # text or html
                content = msg.get_payload(decode=True)
                charset = self.guess_charset(msg)
                if charset:
                    content = content.decode(charset)
                    # kv["Content"] = content
                    # filter html
                    # kv["Content"] = re.sub(r'</?\w+[^>]*>', '', content)
                    kv["Content"] = re.sub(r'<.+?>', '', content)
                # print('Text: {}'.format(content))
            else:
                # TODO: Attachment
                print('Attachment: {}'.format(content_type))
        # print(kv)
        return kv

    # get mail
    def retr_mail(self):
        mails = []
        # source = []
        try:
            # ['response', ['mesg_num octets', ...], octets].
            # (b'+OK 8 57364', [b'1 5727', b'2 5728', b'3 1885', b'4 695', b'5 734', b'6 35906', b'7 941', b'8 5748'], 62)
            response, mail_list, octets = self.mail_link.list()
            if len(mail_list) == 0:
                return None

            # Iterate email
            for seq_octets in mail_list:
                number = seq_octets.decode().split(' ')[0]
                lines = self.mail_link.retr(number)[1]
                lines = [i.decode() for i in lines]  # byte to str
                msg_content = '\r\n'.join(lines)
                msg = Parser().parsestr(msg_content)
                # source.append(msg)
                print("-----------------email seq {}-----------------".format(number))
                mails.append(self.parser_mail(msg))
                print("sender: {}".format(mails[-1]['From'][0]))
                print("Subject: {}".format(mails[-1]['Subject']))
                print("Date: {}".format(mails[-1]['Date']))
            return mails
        except Exception as e:
            print(e)
            return None

    # send email
    def send_msg(self, to: list, subject: str, mail_body: str, **kwargs):
        """
        :param to: A list of addresses to send this mail to.
        :param subject: email subject
        :param mail_body: Mail Body
        :param kwargs: cc=[]: cc list addresses, attachments=[]: file(s) list
        :return:
        """
        try:
            # msg = MIMEText(mail_body, 'plain', 'utf-8')
            msg = MIMEMultipart('related')
            msg['from'] = self.username
            msg['to'] = ','.join(to)
            msg['Subject'] = subject
            cc = kwargs.get("cc", None)
            if cc:
                msg['cc'] = ','.join(cc)

            # mail body
            msg.attach(MIMEText(mail_body))

            # add attachment(s)
            files: list = kwargs.get("attachments", None)
            if files:
                for f in files:
                    if not os.path.isfile(f):
                        print("[error] no such file: {}".format(f))
                        continue
                    att = MIMEText(open(f, 'rb').read(), 'base64', 'utf-8')
                    att["Content-Type"] = 'application/octet-stream'
                    att["Content-Disposition"] = 'attachment; filename="%s"' % f
                    msg.attach(att)
            self.mail_box.sendmail(self.username, to, msg.as_string())
            print(u'send mail success!')
            return 0

        except Exception as e:
            print(u'send mail fail! ' + str(e))
            return 1

2.run.py

#!/usr/bin/env python3
# coding: utf-8
import os
import time
import pickle
import argparse
import subprocess

from lib.mail import MailManager


def exec_cmd(cmd: str):
    """
    execute system command
    :param cmd: Windows/Linux command
    :return: status, stdoutput, erroutput
    """
    print("[info] exec: {}".format(cmd))
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (stdoutput, erroutput) = p.communicate()
    result_code = p.poll()
    # return result_code, stdoutput.decode('utf-8', "ignore"), erroutput.decode('utf-8', "ignore")
    return result_code, stdoutput.decode('gbk', "ignore"), erroutput.decode('gbk', "ignore")


if __name__ == '__main__':
    PARSER = argparse.ArgumentParser()
    PARSER.add_argument("--username", "-u", type=str, required=True, help="sender email address.")
    PARSER.add_argument("--password", "-p", type=str, required=True, help="sender email password.")
    # parser.add_argument("--to", "-t", type=str, required=False, help="email addresses")
    # parser.add_argument("--cc", "-c", type=str, required=False, help="email addresses")
    PARSER.add_argument("--whitelist", "-w", nargs='+', type=str, required=True, help="email addresses:")
    PARSER.add_argument("--refresh_rate", "-r", type=int, default=30, help="refresh email time(minute)")

    args = PARSER.parse_args()
    sleep_time = args.refresh_rate * 60
    WHITELIST = args.whitelist
    # WHITE_LIST = ['xxxx@163.com', 'xxxx@qq.com']
    #db_file = 'task_list.pkl'
    db_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),'task_list.pkl')
    while True:
        mail_manager = MailManager(args.username, args.password)
        # mail_manager.send_msg(['xxxxx@163.com', "xxxx@qq.com"], "test_subject", "test_body",
        #                      cc=["xxxxx@163.com"], attachments=["test.docx", "test.txt"])
        completed_task = {}
        # create empty file if not exits
        if not os.path.isfile(db_file):
            with open(db_file, 'wb') as f:
                pass
        # load pickle file
        with open(db_file, 'rb') as f1:
            try:
                completed_task = pickle.load(f1)
            except EOFError:
                completed_task = {}

        mail = mail_manager.retr_mail()
        print("--" * 30)
        if mail is not None:
            # for i, m in enumerate(mail):
            for m in mail:
                cur_sender = m['From'][0]
                cur_subject = m['Subject']
                date = m['Date']
                if cur_sender in WHITELIST and cur_subject.lower() == "cmd":
                    if (cur_sender, date) in completed_task.keys():
                        continue
                    to = m['To']
                    ret, stdout, stderr = exec_cmd(m['Content'])
                    # ret, stdout, stderr = exec_cmd('ipconfig')
                    print("exec: {}".format(m['Content']))
                    print(stdout, stderr)
                    text = "result code: {} \r\n ".format(ret)
                    text = "{} \r\nstdout: {} \r\n ".format(text, stdout)
                    text = "{} \r\nstderr: {} \r\n".format(text, stderr)
                    text = "{}\r\n\r\n\r\n------------ The original email ------------\r\n".format(text)
                    text = "{}\r\nFrom: {}".format(text, cur_sender)
                    text = "{}\r\nto: {}: ".format(text, to)
                    text = "{}\r\nSubject: {}".format(text, cur_subject)
                    if 'Cc' in m.keys():
                        text = "{}\r\ncc: {}".format(text, m['Cc'])
                    text = "{}\r\nDate: {}".format(text, date)
                    text = "{}\r\n{}".format(text, m['Content'])

                    to.append(cur_sender)
                    subject = "Re: {} ".format(cur_subject)
                    d = {}
                    d.keys()
                    if 'Cc' in m.keys():
                        cc = m['Cc']
                        ret = mail_manager.send_msg(to, subject, text, cc=cc)
                    else:
                        ret = mail_manager.send_msg(to, subject, text)
                    completed_task[cur_sender, date] = "success" if ret == 0 else "fail"
                    with open(db_file, 'wb') as f:
                        pickle.dump(completed_task, f, pickle.HIGHEST_PROTOCOL)
        time.sleep(sleep_time)

使用

这里是建议把./run.py脚本添加到开机启动中去

发送邮件获取主机IP地址

注意: ifconfig命令,拿到的是局域网IP,我需要的是外网IP,所以使用curl icanhazip.com获取

发邮件

收到脚本的回复结果

脚本回复的邮件,如下图。
result code:0 执行命令返回码为0表示命令执行成功(非0表示失败)
stdout: 执行命令时打印的标准输出流。
stderr: 执行命令时打印的标准错误流。
邮件底部是原请求邮件的内容。
邮件回复结果
从邮件主返回的内容中我们拿到了Linux主机对应的外网IP:36.xxx.xx4.167

通过外网访问Linux

  1. 首先需要设置路由,将地址和端口映射到你的Linux机器上。
    如下图,
    端口映射
    3389端口:远程桌面默认端口
    5901端口:主要是VNC默认端口
    22端口: SSH连接的默认端口
    主要用的还是22端口,其它的只是测试用了一下,平常几乎没用到

  2. 为了用外网登录Linux,我把手机Wifi关了用4G网络,在手机上SSH登录Linux,如下图登录成功了。
    SSH登录成功
    在Windows平台也是可行的:
    命令: ipconfig
    ipconfig

宕机的情况

再搭配这个4年前买的智能插座,即使死机了还能断电重启机器。
这里顺便安利一下智能插座,四年前买手机平台送了一个代金券,当时没地方用买了两个智能插座,一个安在热水器每天定时烧水下班回来就有热水洗澡(酒店既视感有没有?),一个安在手机充电头上下班就自动上电,睡觉自动断电,省电不伤手机。。。(各路牌子智能插座的厂商,看到请打钱!)

智能插座

总的来说功能还是挻简单的,脚本也不完善,这里为了节省篇幅,已经写完了的Logger日志也移除了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值