远程主机运行邮件中的脚本

本来题目想说通过邮件控制远程主机,不过实际实现的情况还达不到完全的交互的控制,所以就有了上面的标题。使用的场景主要是远程主机不能通过ssh端口暴露在互联网中,或者网络连接并不是十分稳定的情况下,缺点就是响应时间慢,可能上午发出的脚本,下午才能执行,不具备时效性,不具备交互性。动工之前未查阅是否已经有相关的实现方式,所以基本上的是闭门造车,方法比较笨。

实现的工具链:fecthmail(收取指定gmail帐号中的邮件) --> 自定义的MDA脚本(将邮件中的附件提取出来,放到指定目录) --> 检查邮件中的附件并做相应处理的后台脚本 --> mutt(将脚本的运行输出邮寄给指定账号) --> exim4(主机上的邮件服务)

邮件发送人的可信任,是通过附件的GPG加密来确认的。发送人发送的邮件附件都必须GPG加密,远程主机在收到邮件后会检查是否能正确解密。

下面是自定义的MDA 的python脚本,fetchmail的配置文件中需要将其做为MDA,邮件头是按gmail的格式来分析的,其它服务商的可能不适用。

1.将此脚本存为MDA.py,保存在/usr/bin/目录,所有人都可执行。

2. 创建 /var/spool/MailControlSpool/attachment 目录,MailControlSpool 及其子目录的ower: fetchmail,  group 可以是mail 或者 root,权限:0755。因为此脚本是fetchmail运行的,所以如果fetchmail无法读写相关目录的话,就会造成投递失败。

#!/usr/bin/python
import os
import fileinput
import time
import base64
import glob
import sys

maildir="/var/spool/MailControlSpool/"
attachmentdir=maildir+"attachment/"

masterid="gmailAccount@gmail.com"

def SaveMail():
    '''
    used to save mail which is pass from fetchmail
    return saved file name
    '''

    dateinfo=time.strftime("%y%m%d-%H%M%S")
    filename=maildir+"Mail"+dateinfo
    f=open(filename,'w')
    if f==None:
        return ""

    longline=""
    # TODO:here maybe need to consider the pretty long line,set a counter?
    for line in fileinput.input():
        #print(line,end="")
        longline+=line

    f.write(longline)
    f.close()
    return filename

def mailprocess(mailfile):
    '''
    Read mail, if it from master,then save the attachments
    get attachment,and a list of attachment file name
    '''
    print("read file: " + mailfile)
    mail=open(mailfile,'r')

    lastkey=""
    mailheaderEnd=0
    hasboundary=0
    findboundary=0
    usemailheaddict=1
    isheader=1
    isboundary=0
    iscontent=0
    tempDict={'Delivered-To':"",
            'Received':"",
            'Return-Path':"",
            'Received-SPF':"",
            'Authentication-Results':"",
            'DKIM-Signature':"",
            'MIME-Version':"",
            'Date':"",
            'Message-ID':"",
            'Subject':"",
            'From':"",
            'To':"",
            'Content-Type':"",
            'boundary':"",
            'Content':""
            }
    attachmentList=[]
    boundarycount=0

    # start to read the content
    # try to fetch the content and fill them to dictionary
    # TODO: which way will be better:
    #       1. readlines at once then process
    #       2. use if/elif clause to process the keyword
    for line in mail.readlines():
        # if the header is finished,then
        # if this mail is not from master,do nothing
        # lastkey!="": avoid the several header lines are blank and '\n' in content
        #print("line: " + line)
        if line[0]=='\n' and lastkey!="":
            #print("******header finish")
            if mailheaderEnd==0:
                mailheaderEnd=1
                fromwho=tempDict['From']
                pos1=fromwho.find('<')
                pos2=fromwho.find('>')
                fromwho=fromwho[pos1+1:pos2]
                if fromwho != masterid:
                    print("masterid doesn't match: " + fromwho)
                    return 1
                if tempDict['Received-SPF'][0:4] != "pass":
                    print("SPF is not pass: "+ tempDict['Received-SPF'][0:4])
                    return 2

                position=tempDict['Content-Type'].find("boundary")
                if position > 0:
                    tempDict['boundary']=tempDict['Content-Type'][position+9:]
                    tempDict['boundary']="--"+tempDict['boundary']
                    hasboundary=1
                    # :-1 remove the last '\n'
                    boundary=tempDict['boundary']
                    boundaryLength=len(boundary)
                    #print("boundary is: "+tempDict['boundary'])
            lastkey=""
            isheader=0
            continue

        if isheader==1:
            position=line.find(':')
            if position>0:
                keyword=line[:position]
                if keyword in tempDict:
                    # position+2: ignore the space after colon(:)
                    # len(line)-1: ignore the '\n'
                    tempDict[keyword]+=line[position+2:len(line)-1]
                    #print("Get key: " + keyword)
                    lastkey=keyword
                else:
                    if lastkey != "":
                        tempDict[lastkey]+=line
                    else:
                        print("unknow key: "+ keyword)
                continue

            elif lastkey != "":
                tempDict[lastkey]+=line
            else:
                print("Mail last line ")
            continue

        # in the mainbody or attachment mainbody
        if hasboundary==1:
            #print("3line: " +line[:boundaryLength])
            if line[:boundaryLength] == boundary:
                boundarycount+=1
                findboundary=1
                # the next line after boundary will be header
                isheader=1
                if boundarycount==1:
                    # save the mail header content
                    mainbodyDict=tempDict
                if boundarycount==2:
                    # copy the main body content to the mail header dictionay variable
                    mainbodyDict['Content']=tempDict['Content']
                    # save to list,if there one attachment,the count will be to 3
                if boundarycount>2:
                    attachmentList.append(tempDict)
                # mail header is end, change to attachment Dict
                tempDict={'Content-Type':"",
                        'Content-Disposition':"",
                        'Content-Transfer-Encoding':"",
                        'X-Attachment-Id':"",
                        'Content':""
                        }
                continue
            else:
                tempDict['Content']+=line
                continue
        else:
            tempDict['Content']+=line
            continue

        if hasboundary==0:
            mainbodyDict=tempDict

    mail.close()
    #print("boundarycount: " + str(boundarycount))

    '''
    for k,v in mainbodyDict.items():
        print(k+": "+v)

    for l in attachmentList:
        print("-----------------------------")
        for k,v in l.items():
            print(k+": "+v)
            '''
    count=0
    filetype=""
    charset=""
    filename=""
    # use base64 to decode attachments
    for l in attachmentList:
        contentType=l['Content-Type']
        position=contentType.find(';')
        filetype=contentType[:position]
        contentType=contentType[position+1:]
        pos1= contentType.find('charset=')
        if pos1>0:
            pos2= contentType.find(';')
            charset=contentType[pos1+8:pos2]
            #print("charset is: " + charset)
        else:
            #print("no char set found: " + contentType+ ": " + str(pos1))
            pass
        pos1= contentType.find('name=')
        filename=contentType[pos1+5:].strip('"')
        if l['Content-Transfer-Encoding']=="base64": #and filetype=="text/plain":
            #:-1 remove the last '\n' char
            attachmentContent=l['Content'][:-1]
            try:
                # bytes type returned
                attachmentContent=base64.b64decode(attachmentContent.encode('utf-8'))
                filename=attachmentdir+filename
                attachmentFile=open(filename,'wb')
                if attachmentFile!=None:
                    attachmentFile.write(attachmentContent)
                    attachmentFile.close()
                    print("Saved: "+ filename)
                else:
                    print("open file: " + filename + " failed")
                #print(str(attachmentContent)[2:-1].encode(charset))
            except TypeError:
                print("Failed to decrypt attachment use base64")
        else:
            print("This attachment use unkown encoding: "+ l['Content-Transfer-Encoding'])
                

if __name__ == '__main__':
    # if there are files in the $attachmentdir,then quit with 1
    filecount=0
    for file in glob.glob(attachmentdir+'*'):
        filecount+=1
    if filecount>0:
        print("There are " + str(filecount) + " files in attachment dir")
        sys.exit(9)
    mailfile= SaveMail()
    print("save mail as "+mailfile)
    mailprocess(mailfile)


下面这个是runmailscript.sh,会每隔1分钟检查/var/spool/MailControlSpool/attachment目录,如果有文件的话,就会验证是否可以正常解密,如果可以并且是shell脚本,则将其移到/root/mailscript/目录(需要创建)运行,并将其输出寄回以master账号。这种方法对于所要执行的脚本是些限制的,比如不能是有要求输入的,否则就会停在那了,如果可以预见输入提示的话,可以用expect来操作。脚本执行完后会被删除。这里需要有gpg的key事先生成,gpghomedir是存放key的目录,gpgid是对应key的id.

如果把runmailscript.sh 加在/etc/rc.local中,一开机就运行,那么Mutt的配置文件需要放在/etc中,文件名为Muttrc。 因为在不登录的情况下将会使用系统的配置文件,而非个人的,包括fetchmail的配置文件fetchmailrc也要放在/etc中。

#!/bin/bash

mailattachmentdir="/var/spool/MailControlSpool/attachment/"
scriptdir="/root/mailscript/"
scriptoutput="/root/mailscript/scriptoutput.txt"

pidfile="/var/run/runmailscript.pid"
master="gmailAccount@gmail.com"
gpgid="mygpgid"
gpghomedir="/root/.gnupg/"

mailmsg=""

echo $$ > $pidfile
cd $scriptdir

mutt -s "service start" $master <<EOF
`date`
EOF


while : 
do
	while :
	do
		filecount=`ls $mailattachmentdir | wc -l`
		if [ $filecount -gt 0 ];then
			mv $mailattachmentdir/* $scriptdir
			break
		else
			echo "no new file found"
			sleep 60
		fi
	done

	gpgfilecount=0
	gpgerror=0
	haserror=0
	mailmsg=""

	for file in `ls`
	do
		length=${#file}
		position=$(($length-3))
		postfix=${file:$position}
		echo $postfix
		# check the postfix of attachment,if gpg,then decrypt
		if [ "$postfix" == "asc" ] || [ "$postfix" == "gpg" ];then
			((gpgfilecount++))
			gpg --homedir=${gpghomedir} $file 2> $scriptoutput
			result=$?
			if [ $result -eq 0 ];then
				rm $file
				continue
			else
				# if gpg failed,then do nothing
				mailmsg="Error: gpg failed to decrypt file: "$file"!\n"
				while read line;do
					mailmsg=$mailmsg$line
				done < $scriptoutput
				gpgerror=1
				haserror=1
				break
			fi
		else
			mailmsg="No asc/gpg file found: "$file"!"
			haserror=1
			break
		fi
	done

	scriptfilecount=`ls *.sh 2>/dev/null | wc -l`

	if [ $scriptfilecount -ne 1 ];then
		mailmsg=$mailmsg" no or more than one .sh files found"
		haserror=1
	fi

	if [ $haserror -gt 0 ];then
		echo $mailmsg > $scriptoutput
		echo "runs mutt: has error found"
		if [ $gpgerror -eq 1 ];then
			mutt -s "Gpg failed" -a $file -- $master < $scriptoutput
		else
			mutt -s "Run error" $master < $scriptoutput
		fi
	else
		scriptname=`ls *.sh 2>/dev/null`
		bash $scriptname 1> $scriptoutput 2>&1
		result=$?
		echo "Here run script: "$scriptname
		# use trust-mode,or gpg will ask yes/no to use this public key
		gpg --homedir=${gpghomedir} --trust-model direct -ea -r $gpgid $scriptoutput
		if [ -f $scriptoutput.asc ];then
			echo $mailmsg > $scriptoutput
			echo "runs mutt: script is executed"
			mutt -s "script return: "$result $master -a $scriptoutput.asc -- $master < $scriptoutput
		else
			echo "gpg failed"
		fi
	fi

	rm $scriptdir/* 2>/dev/null
done

下面是fetchmailrc的参考:

set daemon	300


set no bouncemail

defaults:
  antispam -1 
  batchlimit 100


poll imap.gmail.com with protocol imap
user gmailAccount@gmail.com
pass passwd
mda "/usr/bin/MDA.py"

options
keep
idle
ssl

下面是Muttrc的参考:

set realname = "realname"
set from = "gmailAccount@gmail.com"
set use_from = yes
set envelope_from ="yes"


set sendmail="/usr/sbin/exim4"   

set spoolfile = /var/spool/mail/root

set folder="$HOME/Mail/mbox"      # Local mailboxes stored here
set record="/root/sent"           # Where to store sent messages
set postponed="+postponed"   # Where to store draft messages
set mbox_type=mbox           # Mailbox type
set move=no                  # Don't move mail from spool

mailboxes ! +slrn +fetchmail +mutt
set sort_browser=alpha    # Sort mailboxes by alpha(bet)

ignore *
unignore Date: From: User-Agent: X-Mailer X-Operating-System To: \
         Cc: Reply-To: Subject: Mail-Followup-To:
hdr_order Date: From: User-Agent: X-Mailer X-Operating-System To: \
        Cc: Reply-To: Subject: Mail-Followup-To:
                       
set editor="vim -c 'set tw=70 et' '+/^$' " 
set edit_headers=yes      # See the headers when editing

下面是exim4的配置,在Debian下是这两个文件,其它系统的配置方法可能不一样:

/etc/exim4/passwd.client:

*.google.com:gmailAccount@gmail.com:passwd

/etc/exim4/update-exim4.conf.conf
dc_eximconfig_configtype='smarthost'
dc_other_hostnames='localhost.localdomain'
dc_local_interfaces='127.0.0.1 ; ::1'
dc_readhost=''
dc_relay_domains=''
dc_minimaldns='false'
dc_relay_nets='192.168.0.101/24'
dc_smarthost='smtp.gmail.com'
CFILEMODE='640'
dc_use_split_config='false'
dc_hide_mailname='true'
dc_mailname_in_oh='true'
dc_localdelivery='mail_spool'



基于SpringBoot网上超市,系统包含两种角色:用户、管理员,系统分为前台和后台两大模块,主要功能如下: 1 管理员功能实现 商品信息管理 管理员可以通过提交商品名称查询商品,并查看该商品的用户评论信息。 用户管理 管理员通过提交用户名来获取用户资料,对有异常情况的用户信息进行修改,并可以详细查看用户资料。 商品评价管理 管理员审核用户对商品的评价,经过审核的评价才会显示,并可以统计商品评价信息。 已支付订单 管理员查看已支付的订单,并逐个进行订单发货。 2 用户功能实现 商品信息 用户可以收藏、立即购买商品,或对商品进行评价,同时将商品添加到购物车。 购物车 用户可以直接下单购买购物车的商品,或删除购物车的商品。 确认下单 用户选择地址,查看支付金额信息,以确认订单之前的所有细节。 已支付订单 用户查看已支付的订单,若对购买商品产生后悔,可以申请退款。 二、项目技术 开发语言:Java 数据库:MySQL 项目管理工具:Maven Web应用服务器:Tomcat 前端技术:Vue、 后端技术:SpringBoot框架 三、运行环境 操作系统:Windows、macOS都可以 JDK版本:JDK1.8以上版本都可以 开发工具:IDEA、Ecplise都可以 数据库: MySQL 5.7/8.0版本均可 Tomcat:7.x、8.x、9.x版本均可 Maven:任意版本都可以
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值