Develop and Download Open Source Software

Browse Subversion Repository

Contents of /pwfilter/trunk/pwfilter/pwfilter.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 50 - (show annotations) (download) (as text)
Mon May 7 13:30:44 2012 UTC (12 years ago) by ykimura
File MIME type: text/x-python
File size: 45201 byte(s)
class HostCache 
・辞書型変数(キャシング)のリミットの追加
・ダイナミックドメインの処理の追加
1 #! /usr/bin/python2.6
2 # -*- coding: utf-8 -*-
3
4 # <pwfilter.py Receive e-mail filters And white list filter.>
5 # Copyright (C) <2012> <yasuyosi kimura>
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License,
10 # or (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15 # See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 ### Revision 0.1.0 2012/03/03 18:00:00 Base version.
21 ### Change comment logic & add pwmode
22 ### Revision 0.0.2 2012/02/24 16:00:00 Test version.
23 ### Change comment logic parameter
24 ### Revision 0.0.1 2012/02/16 16:00:00 Test version.
25 ### Change DNSCheck
26 ### Revision 0.0.0 2012/01/13 14:00:00 Test version.
27 ### Vre 0.0.0
28
29
30 import sys
31 import time
32 import traceback
33 import Milter
34 from Milter.dynip import is_dynip as dynip
35 from Milter.utils import parseaddr, parse_addr
36
37 import re
38
39 import smtplib
40 import email
41 from email.mime.text import MIMEText
42 from email.header import Header, decode_header
43 from email.utils import formatdate
44
45 import logging
46 import logging.handlers
47
48 ## ������������������������������������������������������ ##
49 socketname = "inet:1025@localhost"
50 sockettimeout = 600
51
52 ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$|'
53 '^\[[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\]$')
54 hostre = re.compile(r'^[a-z0-9][-a-z0-9]*(\.[a-z0-9][-a-z0-9]*)*')
55 fqdn = re.compile(r'^[a-z0-9][-a-z0-9]*'
56 '(\.[a-z0-9][-a-z0-9]*)*(\.[a-z]{2,10})$')
57 fqdnjp = re.compile(r'\.[a-z]{2,2}\.jp$|'
58 '([a-z0-9][-a-z0-9]{2,63}\.[a-z]{2,10})$')
59
60 ## %(levelno)s Numeric logging level for the message (DEBUG, INFO,
61 ## WARNING, ERROR, CRITICAL)
62 ## ������������������������������������������������������ ##
63 log_filename = "/var/log/pwmail/pwfilter.log"
64 log_level = logging.INFO
65
66 my_logger = logging.getLogger("pwfilter")
67 my_logger.setLevel(log_level)
68 log_fh = logging.handlers.RotatingFileHandler(log_filename,
69 maxBytes=1024000,
70 backupCount=10)
71 log_fh.setLevel(logging.DEBUG)
72 log_fm = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
73 log_fh.setFormatter(log_fm)
74 my_logger.addHandler(log_fh)
75
76 ## (������������������������������������������) ##
77 ## ������������������������������
78 my_domainlist = ()
79 ## ���������������������������������IP
80 my_hnameip = "192.168.200.100"
81 ## ���������������������������������������������������������
82 to_pwadmin = "pwadmin@xxxx1.jp"
83
84 ## ������������������������������������������������������ ##
85 ## ���������������������������������
86 sendmail_port = 1026
87 ## postfix recipient_delimiter ������������������������������������������������
88 recipient_delimiter = '+'
89
90
91 ###
92 ### dns ������������������
93 import DNS
94 from DNS import DNSError
95 MAX_CNAME = 10
96
97 ## Lookup DNS records by label and RR type.
98 # The response can include records of other types that the DNS
99 # server thinks we might need.
100 # @param name the DNS label to lookup
101 # @param qtype the name of the DNS RR type to lookup
102 # @return a list of ((name,type),data) tuples
103
104
105 def DNSLookup(name, qtype):
106 try:
107 # To be thread safe, we create a fresh DnsRequest with
108 # each call. It would be more efficient to reuse
109 # a req object stored in a Session.
110 req = DNS.DnsRequest(name, qtype=qtype)
111 resp = req.req()
112 #resp.show()
113 # key k: ('wayforward.net', 'A'), value v
114 # FIXME: pydns returns AAAA RR as 16 byte binary string, but
115 # A RR as dotted quad. For consistency, this driver should
116 # return both as binary string.
117 if resp.header['tc'] == True:
118 try:
119 req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp')
120 resp = req.req()
121 except IOError, x:
122 raise DNSError('TCP Fallback error: ' + str(x))
123 return [((a['name'], a['typename']), a['data']) for a in resp.answers]
124 except IOError, x:
125 raise DNSError(str(x))
126
127
128 class DNSSession(object):
129 """A Session object has a simple cache with no TTL that is valid
130 for a single "session", for example an SMTP conversation."""
131
132 def __init__(self):
133 self.cache = {}
134
135 ## Additional DNS RRs we can safely cache.
136 # We have to be careful which additional DNS RRs we cache. For
137 # instance, PTR records are controlled by the connecting IP, and they
138 # could poison our local cache with bogus A and MX records.
139 # Each entry is a tuple of (query_type,rr_type). So for instance,
140 # the entry ('MX','A') says it is safe (for milter purposes) to cache
141 # any 'A' RRs found in an 'MX' query.
142 SAFE2CACHE = frozenset((
143 ('MX','MX'), ('MX','A'),
144 ('CNAME','CNAME'), ('CNAME','A'),
145 ('A','A'), ('AAAA','AAAA'),
146 ('PTR','PTR'),
147 ('NS','NS'), ('NS','A'),
148 ('TXT','TXT'),
149 ('SPF','SPF')))
150
151 ## Cached DNS lookup.
152 # @param name the DNS label to query
153 # @param qtype the query type, e.g. 'A'
154 # @param cnames tracks CNAMES already followed in recursive calls
155
156 def dns(self, name, qtype, cnames=None):
157 """DNS query.
158
159 If the result is in cache, return that. Otherwise pull the
160 result from DNS, and cache ALL answers, so additional info
161 is available for further queries later.
162
163 CNAMEs are followed.
164
165 If there is no data, [] is returned.
166
167 pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
168 post: isinstance(__return__, types.ListType)
169 """
170 result = self.cache.get((name, qtype))
171 cname = None
172
173 if not result:
174 safe2cache = DNSSession.SAFE2CACHE
175 for k, v in DNSLookup(name, qtype):
176 if k == (name, 'CNAME'):
177 cname = v
178 if (qtype, k[1]) in safe2cache:
179 self.cache.setdefault(k, []).append(v)
180 result = self.cache.get((name, qtype), [])
181 if not result and cname:
182 if not cnames:
183 cnames = {}
184 elif len(cnames) >= MAX_CNAME:
185 #return result # if too many == NX_DOMAIN
186 raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
187 cnames[name] = cname
188 if cname in cnames:
189 raise DNSError('CNAME loop')
190 result = self.dns(cname, qtype, cnames=cnames)
191 return result
192
193 DNS.DiscoverNameServers()
194
195 ######
196 import threading
197
198
199 class HostCache:
200 # ������������
201 # ������������������������
202 # ������������
203 # ������������������
204
205 def __init__(self, fname=None, maxcnt=7, maxrng=1000):
206 self.maxcnt = maxcnt
207 self.maxrng = maxrng
208 self.rngcnt = 0
209 self.cache = {}
210 self.fname = fname
211 self.lock = threading.Lock()
212 #with self.lock
213 #self.lock.acquire()
214 #try:
215 #
216 #finally:
217 # self.lock.release()
218
219 def hostcheck(self, hostname, dyn):
220 with self.lock:
221 if not self.cache.has_key(hostname):
222 return None
223 et, dc, ac = self.cache[hostname]
224 now = time.time()
225 if et > now:
226 ac += 1
227 self.cache[hostname] = (et, dc, ac)
228 return True
229 if ac > 0:
230 ac = 0
231 if (not dyn) and (dc <= self.maxcnt):
232 dc += 1
233 et = now + dc*24*60*60
234 self.cache[hostname] = (et, dc, ac)
235 return False
236
237 def hostadd(self, hostname):
238 with self.lock:
239 if not self.cache.has_key(hostname):
240 if self.rngcnt < self.maxrng:
241 et = time.time() + 24*60*60
242 dc = 1
243 ac = 0
244 self.cache[hostname] = (et, dc, ac)
245 self.rngcnt +=1
246 else:
247 et, dc, ac = self.cache[hostname]
248 ac += 1
249 self.cache[hostname] = (et, dc, ac)
250
251 def hostdel(self, hostname):
252 with self.lock:
253 if self.cache.has_key(hostname):
254 del self.cache[hostname]
255 self.rngcnt -=1
256
257
258 def load(self, fname=None, maxcnt=0):
259 if maxcnt > 0:
260 self.maxcnt = maxcnt
261 if fname:
262 self.fname = fname
263 self.rngcnt = 0
264 self.cache = {}
265 try:
266 with open(self.fname) as fp:
267 for ln in fp:
268 try:
269 hostname, edtime, dcnt, acnt = ln.strip().split(',')
270 et = float(edtime)
271 dc = int(dcnt)
272 ac = int(acnt)
273 self.cache[hostname] = (et, dc, ac)
274 self.rngcnt +=1
275 except:
276 continue
277 except:
278 pass
279
280 def save(self, fname=None, maxcnt=0):
281 if maxcnt > 0:
282 self.maxcnt = maxcnt
283 if fname:
284 self.fname = fname
285 now = time.time() - self.maxcnt*24*60*60
286 try:
287 with open(self.fname, "w") as fp:
288 for hostname, v1 in self.cache.items():
289 try:
290 et, dc, ac = v1
291 if (ac > 0) or ((ac == 0) and (et > now)):
292 fp.write('%s,%f,%d,%d\n' % (hostname, et, dc, ac))
293 except:
294 continue
295 except:
296 pass
297
298 ## ������������������������������������������������������ ##
299 dsnerror = HostCache(fname="/var/log/pwmail/dsnerror.log", maxcnt=10)
300
301
302 ###������###############
303
304 # Shift JIS UTF8 ������������unicode���������
305 def msg_cnvt(s):
306 u = u""
307 for enc1 in ('utf8', 'cp932'):
308 try:
309 u = unicode(s, enc1)
310 except UnicodeDecodeError:
311 continue
312 break
313 if u == u"":
314 u = unicode(s, 'utf8', 'replace')
315 return u
316
317 ###
318 ### Milter.utils ������������������
319 def parse_header(val):
320 """Decode headers gratuitously encoded to hide the content.
321 """
322 try:
323 h = decode_header(val)
324 if not len(h):
325 return val
326 u = []
327 for s, enc in h:
328 if enc:
329 try:
330 u.append(unicode(s, enc))
331 except LookupError:
332 u.append(unicode(s))
333 else:
334 if isinstance(s, unicode):
335 u.append(s)
336 else:
337 u.append(msg_cnvt(s))
338 u = ''.join(u)
339 for enc in ('us-ascii', 'iso-8859-1', 'utf8'):
340 try:
341 return u.encode(enc)
342 except UnicodeError:
343 continue
344 except UnicodeDecodeError:
345 pass
346 except LookupError:
347 pass
348 except email.Errors.HeaderParseError:
349 pass
350 return val
351
352 # FQDN������������������������������
353 def fqdncheck(s):
354 if fqdn.match(s):
355 if s.find('-.') < 0:
356 return True
357 return False
358
359 # ������������������������������������������
360 def my_domain_check(td):
361 tl = len(td)
362 for d, l in my_domainlist:
363 if tl == l:
364 if td == d:
365 return True
366 elif tl > l:
367 if td.endswith(d):
368 pw = td[tl - l -1]
369 if (pw == '.') or (pw == '@'):
370 return True
371 return False
372
373 # ���������������������������������������������
374 def eq_domain_check(td, d):
375 tl = len(td)
376 l = len(d)
377 if tl == l:
378 if td == d:
379 return True
380 elif tl > l:
381 if td.endswith(d):
382 if td[tl - l -1] == '.':
383 return True
384 return False
385
386
387
388
389 class myMilter(Milter.Base):
390
391 def __init__(self): # A new instance with each new connection.
392 self.id = Milter.uniqueID() # Integer incremented with each call.
393
394 # SMTP ���������������������������
395 def msg_connect(self, rcpt_addr):
396 if not self.Cname:
397 Cname = u""
398 else:
399 Cname = msg_cnvt(self.Cname)
400 if not self.IP:
401 IP = u""
402 else:
403 IP = msg_cnvt(self.IP)
404 if not self.Hname:
405 Hname = u""
406 else:
407 Hname = msg_cnvt(self.Hname)
408 if not self.Fname:
409 Fname = u""
410 else:
411 Fname = msg_cnvt(self.Fname)
412 if not rcpt_addr:
413 wrcpt_addr = u""
414 else:
415 wrcpt_addr = msg_cnvt(rcpt_addr)
416 try:
417 return (u"connect from %s at %s\n"
418 "helo: %s\nmail from: %s\n"
419 "rcpt to: %s\n" %
420 (Cname, IP, Hname, Fname, wrcpt_addr))
421 except:
422 self.log_warning("msg_connect:", traceback.format_exc())
423 return u""
424
425 # SMTP ���������������������������
426 def msg_Header(self):
427 if not self.HFrom:
428 hfrom = u""
429 else:
430 hfrom = msg_cnvt(self.HFrom)
431 if not self.Subject:
432 subject = u""
433 else:
434 subject = msg_cnvt(self.Subject)
435 if not self.HDate:
436 hdate = u""
437 else:
438 hdate = msg_cnvt(self.HDate)
439 if not self.HMid:
440 hmid = u""
441 else:
442 hmid = msg_cnvt(self.HMid)
443 try:
444 return (u"Header-From: %s\n"
445 "Header-Subject: %s\n"
446 "Header-Date: %s\n"
447 "Message-ID: %s\n" % (hfrom, subject, hdate, hmid))
448 except:
449 self.log_warning("msg_Header:", traceback.format_exc())
450 return u""
451
452
453
454 # ��������������� ������������������SMTP ���������������������
455 def send_admin1(self, rcpt_addr, subject, ERlevel):
456 encoding = "ISO-2022-JP"
457
458 body = self.msg_connect(rcpt_addr) + u"X-PWmail: %s\n" % self.PWmsg
459 mf = parse_addr(rcpt_addr)
460 un = mf[0]
461 pos = un.find('+')
462 if pos != -1:
463 un = un[:pos]
464 from_addr = un + "+admin@" + mf[1]
465 to_addr = un + "+" + ERlevel + "@" + mf[1]
466
467 try:
468 msg = MIMEText(body.encode(encoding, 'replace'), 'plain', encoding)
469 msg['Subject'] = Header(subject, encoding)
470 msg['From'] = from_addr
471 msg['To'] = to_addr
472 msg['Date'] = formatdate()
473 except:
474 self.log_warning('send_admin1 MIMEText:', traceback.format_exc())
475 return
476
477 s = smtplib.SMTP('localhost', sendmail_port, 'localhost')
478 try:
479 s.sendmail(from_addr, [to_pwadmin], msg.as_string())
480 except:
481 self.log_warning('send_admin1 sendmail:', traceback.format_exc())
482 s.quit()
483
484
485 # ��������������� ���������������
486 #���SMTP ������������������������������������������
487 def send_admin2(self, rcpt_addr, subject, ERlevel):
488 encoding = "ISO-2022-JP"
489
490 body = self.msg_connect(rcpt_addr) + self.msg_Header() + \
491 u"X-PWmail: %s\n" % (self.PWmsg)
492 mf = parse_addr(rcpt_addr)
493 un = mf[0]
494 pos = un.find('+')
495 if pos != -1:
496 un = un[:pos]
497 from_addr = un + "+admin@" + mf[1]
498 to_addr = un + "+" + ERlevel + "@" + mf[1]
499
500 try:
501 msg = MIMEText(body.encode(encoding, 'replace'), 'plain', encoding)
502 msg['Subject'] = Header(subject, encoding)
503 msg['From'] = from_addr
504 msg['To'] = to_addr
505 if not self.HDate:
506 msg['Date'] = formatdate()
507 else:
508 msg['Date'] = self.HDate
509 except:
510 self.log_warning('send_admin2 MIMEText:', traceback.format_exc())
511 return
512
513 s = smtplib.SMTP('localhost', sendmail_port, 'localhost')
514 try:
515 s.sendmail(from_addr, [to_pwadmin], msg.as_string())
516 except:
517 self.log_warning('send_admin2 sendmail:', traceback.format_exc())
518 s.quit()
519
520 # ��������������� ���������������������������
521 #���SMTP ������������������������������������������
522 def send_admin3(self, rcpt_addr, subject, ERlevel):
523 encoding = "ISO-2022-JP"
524
525 body = self.msg_connect(rcpt_addr) + self.msg_Header() + \
526 u"X-PWmail: %s\n" % (self.PWmsg)
527 mf = parse_addr(rcpt_addr)
528 un = mf[0]
529 pos = un.find('+')
530 if pos != -1:
531 un = un[:pos]
532 from_addr = un + "+admin@" + mf[1]
533 to_addr = un + "+" + ERlevel + "@" + mf[1]
534
535 try:
536 msg = MIMEText(body.encode(encoding, 'replace'), 'plain', encoding)
537 msg['Subject'] = Header(subject, encoding)
538 msg['From'] = from_addr
539 msg['To'] = to_addr
540 if not self.HDate:
541 msg['Date'] = formatdate()
542 else:
543 msg['Date'] = self.HDate
544 except:
545 self.log_warning('send_admin3 MIMEText:', traceback.format_exc())
546 return
547
548 s = smtplib.SMTP('localhost', sendmail_port, 'localhost')
549 try:
550 s.sendmail(from_addr, [to_addr, to_pwadmin], msg.as_string())
551 except:
552 self.log_warning('send_admin3 sendmail:', traceback.format_exc())
553 s.quit()
554
555
556 ###������????###############
557 # name���ipad������������������������������������
558 # name������������������������������������������������������������������������
559 # name������������������������������MX���������������������������������������������
560 # DNS���������������������������
561 def DNSCheck(self, name, ipad):
562 adok = False
563 ad4ok = False
564 adng = False
565
566 mxok = False
567 mx4ok = False
568 mx6ok = False
569 mxng = False
570
571 rv = False
572 rm = rt = ''
573
574 s = DNSSession()
575 try:
576 ads = s.dns(name, 'A')
577 if len(ads) > 0:
578 ad4ok = True
579 for a in ads:
580 if a == ipad:
581 adok = True
582 elif not a:
583 adng = True
584
585 mxr = s.dns(name, 'MX')
586 if len(mxr) > 0:
587 for v, n in mxr:
588 ads = s.dns(n, 'A')
589 if len(ads) > 0:
590 mx4ok = True
591 for a in ads:
592 if a == ipad:
593 mxok = True
594 elif not a:
595 mxng = True
596 else:
597 ads = s.dns(n, 'AAAA')
598 if len(ads) > 0:
599 mx6ok = True
600 for a in ads:
601 if not a:
602 mxng = True
603 else:
604 mxng = True
605
606 if adng or mxng or ((not ad4ok) and (not mx4ok)):
607 rv = None
608 else:
609 if adok:
610 rv = True
611 rm += 'A'
612 if mxok:
613 rv = True
614 rm += 'M'
615
616 if ad4ok:
617 rt += 'A'
618 if mx4ok:
619 rt += 'M'
620 if mx6ok:
621 rt += '6'
622
623 except:
624 self.log_warning("DNS:", traceback.format_exc())
625 rv = None
626 rm = rt = 'DNS'
627 return (rv, rm, rt)
628
629
630 def HostCheck(self, Hname, ipad, dyn):
631 ## ������������������������������ ####################################
632 hcheck = dsnerror.hostcheck(Hname, dyn)
633 if hcheck:
634 ipok = (None, '', '')
635 else:
636 ipok = self.DNSCheck(Hname, ipad)
637 if (ipok[0] == None):
638 if (hcheck == None):
639 dsnerror.hostadd(Hname)
640 elif (hcheck == False):
641 dsnerror.hostdel(Hname)
642 return ipok
643
644
645 # @Milter.noreply
646 def connect(self, IPname, family, hostaddr):
647 # ������������������
648 ## self.IPname ���������
649 # self.Cname
650 # self.Cfqdn
651 # self.Cdynip
652 # self.Cipok
653 # self.Sabort
654 #
655 # REJECT���������������������������������������������������������
656 # 554 Transaction failed (Or, in the case of a connection-opening
657 # response, "No SMTP service here")
658 # X.3.5 System incorrectly configured
659
660 self.log("connect from %s at %s" % (IPname, hostaddr))
661 # Ini Setup
662 self.PWmsg = ""
663 self.PWmode = ""
664 self.Sabort = None
665
666 if hostaddr and len(hostaddr) > 0:
667 self.IP = hostaddr[0]
668 else:
669 self.log_critical("REJECT: connect attacks")
670 self.setreply('554', '5.3.5', 'Banned for connect attacks')
671 return Milter.REJECT
672
673 self.IP = hostaddr[0]
674 ## self.port = hostaddr[1]
675 self.Cname = IPname.lower() # Name from a reverse IP lookup
676 if fqdncheck(self.Cname):
677 self.Cfqdn = True
678 self.Cdynip = dynip(self.Cname, self.IP)
679 self.Cipok = self.HostCheck(IPname, self.IP, self.Cdynip)
680 if (not self.Cipok[0]) and (self.Cname != IPname):
681 self.Cipok = self.HostCheck(self.Cname, self.IP, self.Cdynip)
682 else:
683 self.Cfqdn = False
684 self.Cipok = (None, '', '')
685 self.Cdynip = None
686
687 self.Hname = None
688 self.Hmyd = None
689 self.Hipok = (None, '', '')
690 return Milter.CONTINUE
691
692
693 # @Milter.noreply
694 def hello(self, heloname):
695 # ������������������
696 ## self.heloname ���������
697 # self.Hname
698 # self.Hfqdn
699 # self.Hmyd
700 # self.Hdynip
701 # self.Hipok
702 #
703 # REJECT���������������������������������������������������������
704 # 504 Command parameter not implemented
705 # X.5.1 Invalid command
706 # X.5.1 ���������������������
707 # X.5.2 Syntax error
708 # X.5.2 ���������������
709
710 hname = msg_cnvt(heloname)
711 self.log("HELO", hname.encode('utf8', 'replace'))
712
713 # Ini Setup
714 self.PWmsg = ""
715 self.PWmode = ""
716 if self.Cfqdn:
717 if self.Cdynip:
718 self.PWmsg = self.PWmsg + "Cdynip " # yellow
719 if self.Cipok[0] == None:
720 self.PWmsg = self.PWmsg + "ngCipok " # error
721 elif self.Cipok[0] == False:
722 self.PWmsg = self.PWmsg + "noCipok " # error gray
723 else:
724 self.PWmsg = self.PWmsg + "noCfqdn "
725
726 self.Hname = heloname.lower()
727 self.Hmyd = None
728 self.Hipok = (None, '', '')
729
730 if not fqdncheck(self.Hname):
731 #
732 if ip4re.match(self.Hname):
733 hnameip = self.Hname
734 if hnameip[0] == '[':
735 hnameip = hnameip[1:-1]
736 if hnameip == my_hnameip:
737 self.Hmyd = True
738 self.PWmsg = self.PWmsg + "Hmyd " # error
739 # ��������������������� ��������� REJECT ������������������������
740 elif not hostre.match(self.Hname):
741 self.log_critical("REJECT: Helo command Syntax error")
742 self.setreply('554', '5.5.2',
743 '<%s>: Helo command Syntax error' % (heloname))
744 return Milter.REJECT
745 #
746 self.Hfqdn = False
747 self.Hdynip = None
748 self.PWmsg = self.PWmsg + "noHfqdn " # error gray
749 return Milter.CONTINUE
750
751 self.Hfqdn = True
752
753 self.Hmyd = my_domain_check(self.Hname)
754 if self.Hmyd:
755 self.PWmsg = self.PWmsg + "Hmyd " # error
756 return Milter.CONTINUE
757
758 self.Hdynip = dynip(self.Hname, self.IP)
759 if self.Hdynip:
760 self.PWmsg = self.PWmsg + "Hdynip " # yellow
761
762 if self.Cfqdn:
763 if self.Cname == self.Hname:
764 self.Hipok = self.Cipok
765 if self.Hipok[0] == None:
766 self.PWmsg = self.PWmsg + "ngHipok " # error
767 elif self.Hipok[0] == False:
768 self.PWmsg = self.PWmsg + "noHipok " # error gray
769 return Milter.CONTINUE
770
771 if self.Cipok[0]:
772 if eq_domain_check(self.Cname, self.Hname): #type1
773 wHipok = self.HostCheck(self.Hname, self.IP, self.Hdynip)
774 if wHipok[0]:
775 self.Hipok = wHipok
776 else:
777 self.Hipok = (True, self.Cipok[1], wHipok[2])
778 return Milter.CONTINUE
779
780 # ������������������������������������������������������������
781 Hdmain = self.Hname
782 pos = Hdmain.find('.')
783 Hdmain = Hdmain[pos + 1:]
784 if fqdnjp.search(Hdmain):
785 if eq_domain_check(self.Cname, Hdmain): #type2
786 wHipok = self.HostCheck(Hdmain, self.IP, self.Hdynip)
787 if wHipok[0]:
788 self.Hipok = wHipok
789 else:
790 self.Hipok = (True, self.Cipok[1], wHipok[2])
791 return Milter.CONTINUE
792
793 self.Hipok = self.HostCheck(self.Hname, self.IP, self.Hdynip)
794 # ������������������������������������������
795 if (not self.Hipok[0]):
796 if (self.Hname != heloname):
797 self.Hipok = self.HostCheck(heloname, self.IP, self.Hdynip)
798
799 # ������������������������������������������
800 if (not self.Hipok[0]):
801 Hdmain = self.Hname
802 pos = Hdmain.find('.')
803 Hdmain = Hdmain[pos + 1:]
804 if fqdnjp.search(Hdmain):
805 self.Hipok = self.HostCheck(Hdmain, self.IP, self.Hdynip)
806
807 if self.Hipok[0] == None:
808 self.PWmsg = self.PWmsg + "ngHipok " # error
809 elif self.Hipok[0] == False:
810 self.PWmsg = self.PWmsg + "noHipok " # error gray
811
812 return Milter.CONTINUE
813
814
815 # @Milter.noreply
816 def envfrom(self, mailfrom, *str):
817 # ������������������
818 # self.Fname
819 # self.Fad
820 # self.Fd
821 # self.Ffqdn
822 # self.Fmyd
823 # self.Fdynip
824 # self.Fipok
825 # self.Relay
826 #
827 # REJECT���������������������������������������������������������
828 # 550
829 # 555 MAIL FROM/RCPT TO parameters not recognized or not implemented
830 # 555 MAIL FROM/RCPT TO ���������������������������������������������������������������������������
831 # X.1.8 Bad sender's system address
832 # X.1.8 ������������������������������������������������������
833 # X.4.3 Directory server failure
834 # X.4.3 ������������������������������������
835 # X.4.4 Unable to route
836 # X.4.4 ������������������������
837
838 self.log("mail from:", mailfrom, *str)
839
840 # Ini Setup
841 self.Fname = mailfrom
842 self.Fad = ''
843 self.Fd = ''
844 self.Ffqdn = None
845 self.Fmyd = False
846 self.Fdynip = None
847 self.Fipok = (None, '', '')
848 self.Relay = False
849
850 self.Rname = [] # list of recipients
851
852 if self.Fname == '<>':
853 return Milter.CONTINUE
854
855 mb = parseaddr(self.Fname)
856 if (mb[1] == None) or (mb[1] == ''):
857 self.Ffqdn = False
858 self.PWmsg = self.PWmsg + "noFfqdn " # error
859 return Milter.CONTINUE
860
861 self.Fad = mb[1]
862 mf = parse_addr(mb[1])
863 if len(mf) != 2:
864 self.Ffqdn = False
865 self.PWmsg = self.PWmsg + "noFfqdn " # error
866 return Milter.CONTINUE
867
868 self.Fd = mf[1].lower()
869 if not fqdncheck(self.Fd):
870 self.Ffqdn = False
871 self.PWmsg = self.PWmsg + "noFfqdn " # error
872 return Milter.CONTINUE
873
874 self.Ffqdn = True
875
876 self.Fmyd = my_domain_check(self.Fd)
877 if self.Fmyd:
878 self.PWmsg = self.PWmsg + "Fmyd " # error
879 return Milter.CONTINUE
880
881 self.Fdynip = dynip(self.Fd, self.IP)
882 if self.Fdynip:
883 self.PWmsg = self.PWmsg + "Fdynip " # error
884
885 self.Fipok = self.HostCheck(self.Fd, self.IP, self.Fdynip)
886 if self.Fipok[0] == None:
887 self.PWmsg = self.PWmsg + "ngFipok " # error not(MX/A)
888 return Milter.CONTINUE
889
890 # ������������������������������������������������������
891 if self.Fipok[0] == False:
892 self.PWmsg = self.PWmsg + "noFipok " # OK
893 if self.Hipok[0]:
894 if eq_domain_check(self.Hname, self.Fd): #type1
895 return Milter.CONTINUE
896 if self.Cipok[0]:
897 if eq_domain_check(self.Cname, self.Fd): #type1
898 return Milter.CONTINUE
899
900 Fdmain = self.Fd
901 pos = Fdmain.find('.')
902 Fdmain = Fdmain[pos + 1:]
903 if fqdnjp.search(Fdmain):
904 if self.Hipok[0]:
905 if eq_domain_check(self.Hname, Fdmain): #type2
906 return Milter.CONTINUE
907 if self.Cipok[0]:
908 if eq_domain_check(self.Cname, Fdmain): #type2
909 return Milter.CONTINUE
910
911 self.Relay = True
912 self.PWmsg = self.PWmsg + "relay " # yellow
913
914 return Milter.CONTINUE
915
916
917 # @Milter.noreply
918 def envrcpt(self, recipient, *str):
919 # ������������������
920 # self.Rname
921 #
922 # REJECT���������������������������������������������������������
923 # 550
924 # 553 Requested action not taken: mailbox name not allowed
925 # (e.g.,mailbox syntax incorrect)
926 # X.1.1 Bad destination mailbox address
927 # X.1.1 ���������������������������������������������������������
928 # X.7.1 Delivery not authorized, message refused
929 # X.7.1 ���������������������������������������������������������������������
930 # X.2.2 Mailbox full
931 # X.2.2 ������������������������������
932 #
933 # 450 Requested mail action not taken: mailbox unavailable
934 # (e.g.,mailbox busy or temporarily blocked for policy reasons)
935 # 450 ������������������������������������������������(���������������������������������������������
936 # ���������������������������������������������������������������������������������)���������������
937 # ������������������������������������
938 # X.2.1 Mailbox disabled, not accepting messages
939 # X.2.1 ������������������������������������������������������������������������������
940 # X.2.2 Mailbox full
941 # X.2.2 ������������������������������
942
943
944 self.log("rcpt to:", recipient, ":", *str)
945 self.Rname.append(recipient)
946
947 ###
948 ### ���������submission������������������������������������������
949 ###
950 ### Abort ��������������� ������������������������������
951 ### rcpt������������������REJECT���������������������������helo���������������������������������������������������������������
952 ###
953 ### ��������������������������������������������������������������������� 550 ��� 450 ���������������������������
954 ### ��������������������������������������������������������������������������������������� 550 ������������������������������������
955
956
957 ## ���������������������������������������������������������������������
958 ## ���������������������������������������������������
959 if recipient.lower() == to_pwadmin:
960 self.setreply('550', '5.1.1',
961 '<%s>: Recipient address rejected: User unknown.' %
962 (recipient))
963 self.log_critical('550', 'Recipient address rejected: User unknown.')
964 ## ���������������������������������������������������������������������������������������������������������������
965 self.send_admin1(recipient, u"������������������������������������", "abort")
966 return Milter.REJECT
967
968 ## ���������������������������������������������������
969 ## ������������������������������������������������������(postfix recipient_delimiter = +)������������������������
970 ## ���������������������������������������������������
971 if recipient.find(recipient_delimiter) != -1:
972 self.setreply('550', '5.1.1',
973 '<%s>: Recipient address rejected: User unknown.' %
974 (recipient))
975 self.log_critical('550', 'Recipient address rejected: User unknown.')
976 ## ���������������������������������������������������������������������������������������������������������������
977 self.send_admin1(recipient, u"���������������������", "abort")
978 return Milter.REJECT
979
980 ## Abort ���������������
981 ## (rcpt������������������REJECT���������������������������������������������������������������)
982 ## ������������������������������������������������������������connect Helo ���������������������������������������������
983 ## ������������������������������������������������������������������������������������������������������
984 if not self.Fipok[0]:
985 ## ������������Helo ��������������� FQDN ���������
986 if not self.Hfqdn:
987 self.setreply('550', '5.5.2',
988 '<%s>: Helo command rejected: '
989 'need fully-qualified hostname.' % (self.Hname))
990 self.log_error('550', 'Helo command rejected: need fully-qualified hostname.')
991 ## ���������������������������������������������������������������������������������������������������������������
992 #self.send_admin1(recipient, u"Helo���������������������������������FQDN���������", "abort0")
993 return Milter.REJECT
994
995 ### Helo ������������������������������������������������������������������connectHost������������������������������
996 if not self.Hipok[0]:
997 ### ������������������������������������������������������������
998 if (self.Cfqdn) and (not self.Cipok[0]):
999 self.setreply('550', '5.1.8',
1000 '(%s:%s): connectHost rejected: DNS Host not found.' %
1001 (self.Cname,self.IP))
1002 self.log_error('550', 'connectHost rejected: DNS Host not found.')
1003 ## ���������������������������������������������������������������������������������������������������������������
1004 #self.send_admin1(recipient, u"������������������������������������������������������������", "abort0")
1005 return Milter.REJECT
1006
1007 ### Helo ������������������������������������������������������������
1008 self.setreply('550', '5.1.8',
1009 '(%s:%s): Helo command rejected: DNS Host not found.' %
1010 (self.Hname,self.IP))
1011 self.log_error('550', 'Helo command rejected: DNS Host not found.')
1012 ## ���������������������������������������������������������������������������������������������������������������
1013 #self.send_admin1(recipient, u"Helo ������������������������������������������������������������", "abort0")
1014 return Milter.REJECT
1015
1016 ### Helo ���������������������������������������������������������
1017 ### ���������submission���������������������������������������������������
1018 if self.Hmyd:
1019 self.setreply('550', '5.5.2',
1020 '<%s>: Helo command rejected: Breach of Local Policy.' %
1021 (self.Hname))
1022 self.log_critical('550',
1023 'Helo command rejected: Breach of Local Policy.')
1024 ## ���������������������������������������������������������������������������������������������������������������
1025 self.send_admin1(recipient, u"���������������������������������������", "abort")
1026 return Milter.REJECT
1027
1028 if self.Fname != '<>': # (postmaster) OK
1029
1030 ## SenderHost ������FQDN���������������������������������������������DNS���������������������������������������������
1031 if (self.Ffqdn == False) or (self.Fdynip):
1032 self.setreply('550', '5.5.2',
1033 '%s: Sender address rejected: '
1034 'need fully-qualified address.' % (self.Fname))
1035 self.log_critical('550',
1036 'Sender address rejected: '
1037 'need fully-qualified address.')
1038 ## ���������������������������������������������������������������������������������������������������������������
1039 self.send_admin1(recipient, u"���������������������", "abort")
1040 return Milter.REJECT
1041
1042 ### SenderHost������������������������������
1043 ### ���������submission���������������������������������������������������
1044 if self.Fmyd:
1045 self.setreply('550', '5.5.2',
1046 '%s: Sender address rejected: Breach of Local Policy.' %
1047 (self.Fname))
1048 self.log_critical('550',
1049 'Sender address rejected: Breach of Local Policy.')
1050 ## ���������������������������������������������������������������������������������������������������������������
1051 self.send_admin1(recipient, u"������������������������������", "abort")
1052 return Milter.REJECT
1053
1054 ### SenderHost������������������������
1055 ## ������������������������������������������������������������������
1056 ## ������������������Message-ID ���������������������������������
1057 if self.Fipok[0] == None:
1058 self.setreply('550', '5.4.3',
1059 '%s: Sender address rejected: Breach of Domain.' %
1060 (self.Fname))
1061 self.log_critical('550',
1062 'Sender address rejected: Breach of Domain.')
1063 ## ���������������������������������������������������������������������������������������������������������������
1064 self.send_admin1(recipient, u"���������������������������������", "abort")
1065 return Milter.REJECT
1066
1067 return Milter.CONTINUE
1068
1069
1070 # @Milter.noreply
1071 def data(self):
1072 #
1073 # REJECT���������������������������������������������������������
1074 # 550 rejections for policy reasons
1075 # 450 rejections for policy reasons
1076 # 550 450 ���������������������������������������
1077 # X.7.1 Delivery not authorized, message refused
1078 # X.7.1 ���������������������������������������������������������������������
1079 #
1080 # X.1.8 Bad sender's system address
1081 # X.1.8 ������������������������������������������������������
1082 # X.4.3 Directory server failure
1083 # X.4.3 ������������������������������������
1084 # X.4.4 Unable to route
1085 # X.4.4 ������������������������
1086 # X.5.2 Syntax error
1087 # X.5.2 ���������������
1088
1089 ## self.log("data")
1090 self.log_debug("data")
1091
1092 # Ini Setup
1093 self.FromAD = []
1094 self.HFrom = None
1095 self.HDate = None
1096 self.Subject = None
1097 self.HMid = None
1098 self.HList = None
1099
1100 return Milter.CONTINUE
1101
1102
1103 # @Milter.noreply
1104 def header(self, name, hval):
1105 ### self.log_debug("header:%s: %s" % (name,hval))
1106
1107 nbuf = name.lower()
1108 if nbuf == "from":
1109 ms = []
1110 adbuf = hval.split(',')
1111 for ad in adbuf:
1112 ma = parseaddr(ad)
1113 mn = parse_header(ma[0])
1114 ms.append(mn + ' <' + ma[1] + '>')
1115 self.FromAD.append((mn, ma[1]))
1116 mf = ",".join(ms)
1117 if not self.HFrom:
1118 self.HFrom = mf
1119 else:
1120 self.HFrom = self.HFrom + ',' + mf
1121 self.log_debug("Header-From-B:", hval)
1122 self.log("Header-From:", mf)
1123 elif nbuf == "date":
1124 self.HDate = hval
1125 elif nbuf == "subject":
1126 self.Subject = parse_header(hval)
1127 self.log_debug("Subject-B:", hval)
1128 self.log("Subject:", self.Subject)
1129 elif nbuf == "message-id":
1130 self.log("Message-ID:", hval)
1131 self.HMid = hval
1132 elif nbuf.startswith("list-"):
1133 self.HList = True
1134
1135 return Milter.CONTINUE
1136
1137
1138 # @Milter.noreply
1139 def eoh(self):
1140 ## self.log("eoh")
1141 self.log_debug("eoh")
1142
1143 if not self.HDate:
1144 self.PWmsg = self.PWmsg + "noHDate " # error
1145 if not self.HMid:
1146 self.PWmsg = self.PWmsg + "noHMid " # yellow (self.Cdynip,self.Hdynip,self.Relay) True:Error
1147
1148 # Ini Setup
1149 self.HFromAD = None
1150 self.HFfqdn = None
1151 self.HFmyd = False
1152 self.HFdynip = None
1153 self.HFipok = (None, '', '')
1154 self.HFdnok = None
1155 self.HFadok = None
1156
1157 if len(self.FromAD) != 1:
1158 self.HFadER = True
1159 self.PWmsg = self.PWmsg + "HFadER "
1160 else:
1161 self.HFadER = False
1162 ad = self.FromAD[0][1]
1163 self.HFromAD = ad
1164 ### ������������������������
1165 if (ad[0] == "'") and (ad[-1] == "'"):
1166 ad = ad[1:-1]
1167 ### ������������������������
1168 if (ad == None) or (ad == ''):
1169 self.HFfqdn = False
1170 self.PWmsg = self.PWmsg + "noHFfqdn "
1171 else:
1172 mf = parse_addr(ad)
1173 if len(mf) != 2:
1174 self.HFfqdn = False
1175 self.PWmsg = self.PWmsg + "noHFfqdn "
1176 else:
1177 dn = mf[1].lower()
1178 if not fqdncheck(dn):
1179 self.HFfqdn = False
1180 self.PWmsg = self.PWmsg + "noHFfqdn "
1181 else:
1182 self.HFfqdn = True
1183 self.HFmyd = my_domain_check(dn)
1184 if self.HFmyd:
1185 self.PWmsg = self.PWmsg + "HFmyd "
1186 else:
1187 self.HFdynip = dynip(dn, self.IP)
1188 if self.HFdynip:
1189 self.PWmsg = self.PWmsg + "HFdynip "
1190
1191 self.HFipok = self.HostCheck(dn, self.IP, self.HFdynip)
1192 if self.HFipok[0] == None:
1193 self.PWmsg = self.PWmsg + "ngHFipok "
1194 elif self.HFipok[0] == False:
1195 self.PWmsg = self.PWmsg + "noHFipok "
1196
1197 if self.Ffqdn:
1198 if eq_domain_check(self.Fd, dn): #type1
1199 self.HFdnok = True
1200 else:
1201 pos = dn.find('.')
1202 sdn = dn[pos + 1:]
1203 if fqdnjp.search(sdn):
1204 if eq_domain_check(self.Fd, sdn): #type2
1205 self.HFdnok = True
1206 if not self.HFdnok:
1207 self.HFdnok = False
1208 self.PWmsg = self.PWmsg + "noHFdnok "
1209
1210 if self.Fad == ad:
1211 self.HFadok = True
1212 else:
1213 self.HFadok = False
1214 self.PWmsg = self.PWmsg + "noHFadok "
1215
1216 if self.HList:
1217 self.PWmsg = self.PWmsg + "List "
1218
1219
1220 ### ���������������������������������
1221 if not self.HDate:
1222 self.setreply('550', '5.7.1', 'Breach of Header-Date Policy.')
1223 self.log_critical('550', 'Breach of Header-Date Policy.')
1224 ## ���������������������������������������������������������������������������������������������������������������
1225 for rad in self.Rname:
1226 self.send_admin2(rad, u"���������������������������", "abort")
1227 return Milter.REJECT
1228
1229
1230 ### ���������������������������
1231 ### ���������������������������������������������������������������������������������
1232 ### ���������������������������������������������������������(self.HFadER)
1233 if (self.HFadER) or (not self.HFfqdn) or (self.HFmyd) or \
1234 (self.HFdynip) or (self.HFipok[0] == None):
1235 self.setreply('550', '5.7.1',
1236 '%s: Breach of Header-From Local Policy.' %
1237 (','.join([m for d, m in self.FromAD])))
1238 self.log_critical('550', 'Breach of Header-From Local Policy.')
1239 ## ���������������������������������������������������������������������������������������������������������������
1240 for rad in self.Rname:
1241 ##self.send_admin2(rad, u"������������������", "abort")
1242 self.send_admin3(rad, u"������������������", "abort")
1243 return Milter.REJECT
1244
1245 ### ���������������������������������������������������������
1246 ### ���������������������������������������������������������������������������������������������������
1247 ### RFC������������������������������������������������������������������������������������������������������������������
1248 if not self.HMid:
1249 self.setreply('550', '5.7.1',
1250 '%s: Breach of Message-ID Local Policy.' %
1251 (','.join([m for d, m in self.FromAD])))
1252 self.log_critical('550', 'Breach of Message-ID Local Policy.')
1253 ## ���������������������������������������������������������������������������������������������������������������
1254 for rad in self.Rname:
1255 ##self.send_admin2(rad, u"Message-ID���������", "black")
1256 self.send_admin3(rad, u"Message-ID���������", "black")
1257 return Milter.REJECT
1258
1259
1260 ## ���������������������������������������������
1261 ## ������������������������������������������������������������������������������������������������������������������������
1262 ## ������������������������������������������������������������������������
1263 ## ���������������������������������������������������helo���������������������������������������������������������������������������������������������������
1264 if self.Cdynip or self.Hdynip or self.Relay or (not self.Hipok[0]) or \
1265 ((self.Ffqdn) and (not self.HFdnok) or (not self.HFadok)) or \
1266 (self.Fname == '<>'):
1267 self.PWmode = "gray"
1268 ## ���������������������������������������������������������������������������������������������������������
1269 msg = u"������������"
1270 if (self.Cdynip) or (self.Hdynip):
1271 msg = msg + u"������������������������������"
1272 if (not self.Hipok[0]):
1273 msg = msg + u"���������������"
1274 if (self.Fname == '<>'):
1275 msg = msg + u"Postmaster���"
1276 if (self.Ffqdn):
1277 if (not self.HFdnok):
1278 msg = msg + u"���������������������������������������������"
1279 elif (not self.HFadok):
1280 msg = msg + u"���������������������������������������������"
1281 if (self.Relay):
1282 msg = msg + u"���������������������"
1283 if (self.HList):
1284 msg = msg + u"������"
1285 for rad in self.Rname:
1286 ##self.send_admin2(rad, msg, "gray") # admin Only
1287 self.send_admin3(rad, msg, "gray") # admin or user
1288
1289 return Milter.CONTINUE
1290
1291
1292 def eom(self):
1293 ## self.log("eom")
1294 self.log_debug("eom")
1295
1296 # ���������������������������������������������������������������������������������������������������������
1297 self.addheader('X-PWfrom', self.Fname)
1298
1299 # ���������������������������������������������������������������������������������������������������������������
1300 if self.PWmsg != "":
1301 self.addheader('X-PWmail', self.PWmsg)
1302
1303 if self.PWmode != "":
1304 self.addheader('X-PWmode', self.PWmode)
1305
1306 if self.PWmsg != "":
1307 self.log("X-PWmail:", self.PWmsg)
1308 self.PWmsg = ""
1309
1310 if self.PWmode != "":
1311 self.log("X-PWmode:", self.PWmode)
1312 self.PWmode = ""
1313
1314 return Milter.CONTINUE
1315
1316 def abort(self):
1317 self.log_debug("abort")
1318 self.Sabort = True
1319 return Milter.CONTINUE
1320
1321 def close(self):
1322 #
1323 # End Setup
1324 #
1325 # abort ���������������������������������
1326 if self.Sabort:
1327 self.log_warning("sever abort: mail server log read")
1328
1329 if self.PWmsg != "":
1330 self.log("X-PWmail:", self.PWmsg)
1331
1332 if self.PWmode != "":
1333 self.log("X-PWmode:", self.PWmode)
1334
1335 self.log("close")
1336 return Milter.CONTINUE
1337
1338
1339 ## === Support Functions ===
1340 def log_debug(self, *msg):
1341 my_logger.debug('[%d] %s', self.id, ' '.join([str(m) for m in msg]))
1342
1343 def log(self, *msg):
1344 my_logger.info('[%d] %s', self.id, ' '.join([str(m) for m in msg]))
1345
1346 def log_warning(self, *msg):
1347 my_logger.warning('[%d] %s', self.id, ' '.join([str(m) for m in msg]))
1348
1349 def log_error(self, *msg):
1350 my_logger.error('[%d] %s', self.id, ' '.join([str(m) for m in msg]))
1351
1352 def log_critical(self, *msg):
1353 my_logger.critical('[%d] %s', self.id, ' '.join([str(m) for m in msg]))
1354
1355
1356 ## ===
1357
1358 def main():
1359 my_logger.info("pwfilter startup")
1360
1361 global my_domainlist
1362 s = sys.argv[1]
1363 for v in s.split(','):
1364 p = (v, len(v))
1365 my_domainlist = my_domainlist + (p,)
1366
1367 global my_hnameip
1368 my_hnameip = sys.argv[2]
1369
1370 global to_pwadmin
1371 to_pwadmin = sys.argv[3]
1372
1373
1374 my_logger.info("mydomain:" + str(my_domainlist))
1375 dsnerror.load()
1376
1377 # Register to have the Milter factory create instances of your class:
1378 Milter.factory = myMilter
1379 flags = Milter.ADDHDRS
1380 Milter.set_flags(flags) # tell Sendmail which features we use
1381
1382 Milter.runmilter("pwfilter", socketname, sockettimeout)
1383
1384 dsnerror.save()
1385 my_logger.info("pwfilter shutdown")
1386
1387 if __name__ == "__main__":
1388 main()

Properties

Name Value
svn:executable *

Back to OSDN">Back to OSDN
ViewVC Help
Powered by ViewVC 1.1.26