20121228

用邮件分割和传送大文件,python实现 III

用邮件分割和传送大文件,python实现 III

4. 接收端baoyu

4.1 导言和import

这部分,是从zhumao.py中抄过来的。

1 #! /usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # baoyu, 邮件接收者
5 __usage__ = "usage: %baoyu.py [--help]"
6 __version__ = "baoyu by Young 2012-12-21"
7
8 from optparse import OptionParser
9 import base64
10 import time
11 import sys
12 import poplib
13 import time
14 from email.Parser import Parser
15 from email.header import decode_header
16

4.2 helper函数们

这里是一些在后面的业务逻辑实现中要调用的一些函数。它们在此声明和定义,定义的方式一般应依据动机、目的,而不是根据实现手段。也就是说,使用的词汇应该是分析阶段的词汇。函数被从"后面的业务逻辑"中抽取到这里,可能因为被调用很多次,也可能只调用一次,但是在业务上具有较为鲜明的特征。如果调用很多次,抽取出来的原因之一就是重用,这样可以避免后续维护的时候一旦有修改需求,这个功能相关的很多地方都要修改;如果只调用一次,但是业务特征明显,就是为了信息隐藏,以后或别人读代码的时候要容易一些。

根据场合和组织方式的差异,这些函数有不同的称呼。在C++/java中,私有函数基本上实现了这样的功能;在flex/bison及我一时没想起来的很多领域中,它们被称为helper函数。

17 ######
18 # helpers
19 ######
20 def getheader(header_text, default="ascii"):
21 """Decode the specified header"""
22 try:
23 headers = decode_header(header_text)
24 header_sections = [unicode(text, charset or default)
25 for text, charset in headers]
26 return u"".join(header_sections)
27 except:
28 return u"".join("invalidated encode")
29

上面的代码是从某位貌似日本人的站点上抄来的,它的名字其实不应该叫做getheader,而是"根据charset解码"邮件头或正文。

30 def get_msg(which):
31 return "\n".join(server.top(which, 1)[1])

这是根据 which指定的邮件id号,从邮件服务器上取得对应的邮件,但是不删除。server.top()。详见手册[http://docs.python.org/2/library/poplib.html],"POP3.top(which,
howmuch)"。

33 def get_from(msg):
34 email = parser.parsestr(msg)
35 return email.get("From")

上述的 pserser 就是 第14行 "from email.Parser import Parser"
中的Parser的实例,在下面的第82行。手册[http://docs.python.org/2/library/email.parser.html]。这个parser是专门用来从邮件中析出邮件头部等各个部分的。

第35行,解析出From部分,也就是邮件发送者地址。baoyu.py根据这个来判断某封邮件符合"zhumao-baoyu"协议的发送者部分。

这个粗糙的协议包括:发送者、邮件主题、第几封邮件、共几封。其中,发送者和邮件主题作为下载和删除邮件的过滤条件。

37 def get_body(which):
38 msg = "\n".join(server.retr(which)[1])
39 email = parser.parsestr(msg)
40 return email.get("BODY_START")

与第33行开始的函数类似,get_body用于取得which指定的id对应的邮件的body。不同的是,get_body把这封邮件标记为已读。

此外,上文提到,我们为了解析方便,在body的开头标记了"BODY_START:",所以此处,body可以视为"BODY_START"部分的值。

42 def get_subject(msg):
43 return getheader(parser.parsestr(msg).get("Subject"))

还是与第33行开始的函数类似,取主题部分。

45 # e.g. [base64-2] 1/2
46 def get_cur_num(subject, subject_prefix):
47 start = subject.find(opt.subject_prefix)+len(opt.subject_prefix)
48 slash = subject.find('/', start)
49 return int(subject[start:slash])

51 def get_all_num(subject, subject_prefix):
52 start = subject.find(opt.subject_prefix)+len(opt.subject_prefix)
53 slash = subject.find('/', start)
54 return int(subject[slash+1:])

以上两个函数,是从邮件头里取得这是第几个包、一共几个包这两个数据,使用的方法是字符串处理,以"/"分割类似"[base64-2] 1/2"的邮件头部。

4.3 命令行解析

与发送端zhumao.py的命令行解析类似,解释从略。

57 #--------------------------------------------------------------------
58 # 命令行解析
59 p = OptionParser(usage=__usage__, version=__version__, description=__doc__)
60 p.add_option("-F", "--file", dest="filename",
61 help="file need to be saved", metavar="FILE",
62 default="test.out")
63 p.add_option("-P", "--pop", dest="pop",
64 help="pop3 server", metavar="POP3_SERVER",
65 default="pop3.nenu.edu.cn")
66 p.add_option("-u", "--user", dest="user",
67 help="user name", metavar="USER")
68 p.add_option("-p", "--password", dest="password",
69 help="password", metavar="PASSWORD")
70 p.add_option("-f", "--from", dest="fromaddr",
71 help="from whom to be filtered", metavar="FROM",
72 default='gift.young.1@gmail.com' )
73 p.add_option("-L", "--subject", dest="subject_prefix",
74 help="the prefix of mail subject as filter",
metavar="subject_prefix",
75 default='[zhumao_baoyu_mail]')
76
77 (opt, args) = p.parse_args()
78

4.4 准备连接邮件服务器

82 parser = Parser()
83 server = poplib.POP3(opt.pop)
84 server.user(opt.user)
85 server.pass_(opt.password)
86 server.set_debuglevel(0)
87 sleep = 0.1
88 count = server.stat()[0]
89 d = dict()

使用pop3接收,各种参数设置。

第88行,取得stat命令时的邮件数量,即未读邮件数量。

第89行,初始化一个字典 (映射)。后面准备把第1封邮件放到"1"的值里面,第2封邮件放到"2"的值里面,依此类推。这样,在拼邮件的时候,可以按key排序。

4.5 接收邮件

检查邮件,根据邮件头过滤,下载并删除符合要求的邮件,显示接收百分比。

从第90行到第107行,是一个循环,用于遍历所有邮件,并在第103行当查找到所有符合要求的邮件 (数量达到要求)以后跳出循环。

90 for i in xrange(1, count, 1):
91 current = get_msg(i)

调用helper函数 get_msg,取得id为i的邮件 (的邮件头和body第一行) 。

92 if (get_from(current).find(opt.fromaddr) != -1 and
93 get_subject(current).find(opt.subject_prefix) != -1):
94 b = get_body(i)

如果邮件的发送者符合要求 (比如 zhumao@nenu.edu.cn) ,并且主题的prefix也符合要求 (比如 [base64-2]),取整封邮件。

如果不过滤直接在遍历的时候取整封邮件,因为邮件数量众多,带附件的邮件又很大,性能会比较低。而仅遍历邮件头,对网络带宽就没有那么高的要求了。

95 print '+-------------------------'
96 print '|' + str(i)+' '+ get_subject(current)
97 # print '|' + b
98 print '+-------------------------'

显示进度,避免用户着急。

99 current_seq = get_cur_num(get_subject(current), opt.subject_prefix)
100 all_number = get_all_num(get_subject(current),
opt.subject_prefix)

调用helper函数,取得当前是这一文件的第几包邮件 和 一共几包邮件。一共几句邮件,数值在遍历中一直不会变,但是重复计算了很多次。

101 print (str(current_seq) +'/'+ str(all_number))

再显示进度,第几封,共几封。

102 d [current_seq] = b

把第N封邮件放到字典键N的位置。参见前面提到的第89行,这里是向字典中插入数据。

103 if len(d) >= all_number :
104 break
106 else:
107 print 'filtered out: '+str(i)+'/'+str(count)+'
'+get_subject(current)

如果把这个文件对应的所有邮件都已下载完毕,跳出循环;否则的话,也告诉用户一声,这封邮件不符合条件,不然出现大量不符合条件的邮件时,用户看到的是程序假死。

109 server.quit()

收完邮件,关闭连接,保持优雅。

4.6 合并文件

111 # 合并文件
112 s=""
113 for k in xrange(1, all_number+1, 1):
114 s += d[k]
115

接key的顺序遍历字典d,把对应的值拼接在一起。这个对应的值来自第102行的插入,此处是取出数据。字典d综上所述,在第89行初始化,在第102行插入数据,在第114行读取使用。如果不用字典,而使用一个链表,以下标代替key,也可以。

4.7 解码文件

我们拼接出来的文件是base64编码的,所以需要解码,然后写出。

116 # 解码文件
117 s = base64.standard_b64decode(s)
118
119 file = open (opt.filename, "w")
120 file.write(s)


5. 回顾数据流

发送端的数据流为: 文件binary -> 读入文件 -> base64编码 -> cut -> smtp.

接收端的数据流为:pop3 -> merge -> base64解码 -> 写出文件 -> 文件binary。

如果思想的传递,也能无误地这样传输,该有多好。

6. 进一步的工作

还有很多进一步的工作可以做,让这个协议及应用程序更好一些。比如,加错误检校,指定重传错误的包而不是全部重传,由baoyu访问zhumao的共享目录并指定下载某个文件,不使用SMTP/POP3而是通过论坛的贴子传递信息。

还可以在传送前加压缩 (不过对于视频和音频,没啥意义)。

还可以用这种隧道做成反向代理……

未来有无数种可能。可能这正是人生的迷人之处,如果今天就能看到明天及所有以后的日子,那还有什么希望可言。

完整代码在这里 [http://my.csdn.net/my/code/detail/33574] 和
[http://my.csdn.net/my/code/detail/33573].

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

No comments: