前言
几年前买了个树莓派板子,一直都只是运行一个爬虫程序定期发邮件给自己,总感觉没有物尽其用。大多时间都不在板子旁边,有时候爬虫没运行也来也不知道。即使记住了外网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
-
首先需要设置路由,将地址和端口映射到你的Linux机器上。
如下图,
3389端口:远程桌面默认端口
5901端口:主要是VNC默认端口
22端口: SSH连接的默认端口
主要用的还是22端口,其它的只是测试用了一下,平常几乎没用到 -
为了用外网登录Linux,我把手机Wifi关了用4G网络,在手机上SSH登录Linux,如下图登录成功了。
在Windows平台也是可行的:
命令: ipconfig
宕机的情况
再搭配这个4年前买的智能插座,即使死机了还能断电重启机器。
这里顺便安利一下智能插座,四年前买手机平台送了一个代金券,当时没地方用买了两个智能插座,一个安在热水器每天定时烧水下班回来就有热水洗澡(酒店既视感有没有?),一个安在手机充电头上下班就自动上电,睡觉自动断电,省电不伤手机。。。(各路牌子智能插座的厂商,看到请打钱!)
结
总的来说功能还是挻简单的,脚本也不完善,这里为了节省篇幅,已经写完了的Logger日志也移除了。