#! /usr/bin/python2.6 # -*- coding: utf-8 -*- # # Copyright (C) <2012> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, # or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . ### Revision 0.1.2 2012/09/14 21:00:00 Base version. ### Change logic & add Whitelist ### Revision 0.1.1 2012/08/20 08:00:00 Base version. ### Change comment logic & add HostCache, DYNSplit, Whitelist ### Revision 0.1.0 2012/03/03 18:00:00 Base version. ### Change comment logic & add pwmode ### Revision 0.0.2 2012/02/24 16:00:00 Test version. ### Change comment logic parameter ### Revision 0.0.1 2012/02/16 16:00:00 Test version. ### Change DNSCheck ### Revision 0.0.0 2012/01/13 14:00:00 Test version. ### Vre 0.0.0 import sys import time import traceback import Milter from Milter.dynip import is_dynip as dynip from Milter.utils import parseaddr, parse_addr import re import smtplib import email from email.mime.text import MIMEText from email.header import Header, decode_header from email.utils import formatdate import logging import logging.handlers ## (システムに合わせて修正が必要です) ## socketname = "inet:1025@localhost" sockettimeout = 600 ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$|' '^\[[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\]$') hostre = re.compile(r'^[a-z0-9][-a-z0-9]*(\.[a-z0-9][-a-z0-9]*)*') fqdn = re.compile(r'^[a-z0-9][-a-z0-9]*' '(\.[a-z0-9][-a-z0-9]*)*(\.[a-z]{2,10})$') fqdnjp = re.compile(r'\.[a-z]{2,2}\.jp$|' '([a-z0-9][-a-z0-9]{2,63}\.[a-z]{2,10})$') ## %(levelno)s Numeric logging level for the message (DEBUG, INFO, ## WARNING, ERROR, CRITICAL) ## (システムに合わせて修正が必要です) ## log_filename = "/var/log/pwmail/pwfilter.log" log_level = logging.INFO my_logger = logging.getLogger("pwfilter") my_logger.setLevel(log_level) log_fh = logging.handlers.RotatingFileHandler(log_filename, maxBytes=1024000, backupCount=10) log_fh.setLevel(logging.DEBUG) log_fm = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") log_fh.setFormatter(log_fm) my_logger.addHandler(log_fh) ## (起動時にパラメータで設定する) ## ## 受信ドメイン名リスト my_domainlist = () ## 受信サーバのグローバルIP my_hnameip = "192.168.200.100" ## ロギング用メールアドレス「全て小文字」 to_pwadmin = "pwadmin@xxxx1.jp" ## (システムに合わせて修正が必要です) ## ## ロギング用メールポート sendmail_port = 1026 ## postfix recipient_delimiter 受信者の拡張アドレスの区切り文字 recipient_delimiter = '+' ### ### dns コピーを修正 import DNS from DNS import DNSError MAX_CNAME = 10 ## Lookup DNS records by label and RR type. # The response can include records of other types that the DNS # server thinks we might need. # @param name the DNS label to lookup # @param qtype the name of the DNS RR type to lookup # @return a list of ((name,type),data) tuples def DNSLookup(name, qtype): try: # To be thread safe, we create a fresh DnsRequest with # each call. It would be more efficient to reuse # a req object stored in a Session. req = DNS.DnsRequest(name, qtype=qtype) resp = req.req() #resp.show() # key k: ('wayforward.net', 'A'), value v # FIXME: pydns returns AAAA RR as 16 byte binary string, but # A RR as dotted quad. For consistency, this driver should # return both as binary string. if resp.header['tc'] == True: try: req = DNS.DnsRequest(name, qtype=qtype, protocol='tcp') resp = req.req() except IOError, x: raise DNSError('TCP Fallback error: ' + str(x)) return [((a['name'], a['typename']), a['data']) for a in resp.answers] except IOError, x: raise DNSError(str(x)) class DNSSession(object): """A Session object has a simple cache with no TTL that is valid for a single "session", for example an SMTP conversation.""" def __init__(self): self.cache = {} ## Additional DNS RRs we can safely cache. # We have to be careful which additional DNS RRs we cache. For # instance, PTR records are controlled by the connecting IP, and they # could poison our local cache with bogus A and MX records. # Each entry is a tuple of (query_type,rr_type). So for instance, # the entry ('MX','A') says it is safe (for milter purposes) to cache # any 'A' RRs found in an 'MX' query. SAFE2CACHE = frozenset(( ('MX','MX'), ('MX','A'), ('CNAME','CNAME'), ('CNAME','A'), ('A','A'), ('AAAA','AAAA'), ('PTR','PTR'), ('NS','NS'), ('NS','A'), ('TXT','TXT'), ('SPF','SPF'))) ## Cached DNS lookup. # @param name the DNS label to query # @param qtype the query type, e.g. 'A' # @param cnames tracks CNAMES already followed in recursive calls def dns(self, name, qtype, cnames=None): """DNS query. If the result is in cache, return that. Otherwise pull the result from DNS, and cache ALL answers, so additional info is available for further queries later. CNAMEs are followed. If there is no data, [] is returned. pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF'] post: isinstance(__return__, types.ListType) """ result = self.cache.get((name, qtype)) cname = None if not result: safe2cache = DNSSession.SAFE2CACHE for k, v in DNSLookup(name, qtype): if k == (name, 'CNAME'): cname = v if (qtype, k[1]) in safe2cache: self.cache.setdefault(k, []).append(v) result = self.cache.get((name, qtype), []) if not result and cname: if not cnames: cnames = {} elif len(cnames) >= MAX_CNAME: #return result # if too many == NX_DOMAIN raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME) cnames[name] = cname if cname in cnames: raise DNSError('CNAME loop') result = self.dns(cname, qtype, cnames=cnames) return result DNS.DiscoverNameServers() ###### import threading class HostCache: # ホスト名 # 更新エンドタイム # 更新回数 # アクセス回数 def __init__(self, fname=None, maxcnt=7, maxrng=2000): self.maxcnt = maxcnt self.maxrng = maxrng self.rngcnt = 0 self.cache = {} self.fname = fname self.lock = threading.Lock() #with self.lock #self.lock.acquire() #try: # #finally: # self.lock.release() def hostcheck(self, hostname, dyn): with self.lock: if not self.cache.has_key(hostname): return None et, dc, ac, dy = self.cache[hostname] now = time.time() if et > now: ac += 1 self.cache[hostname] = (et, dc, ac, dy) return True if ac > 0: ac = 0 if (not dyn) and (dc <= self.maxcnt): dc += 1 et = now + dc*24*60*60 self.cache[hostname] = (et, dc, ac, dy) return False def hostadd(self, hostname, dyn): with self.lock: if not self.cache.has_key(hostname): if self.rngcnt < self.maxrng: et = time.time() + 24*60*60 dc = 1 ac = 0 dy = int(dyn) self.cache[hostname] = (et, dc, ac, dy) self.rngcnt += 1 else: et, dc, ac, dy = self.cache[hostname] ac += 1 self.cache[hostname] = (et, dc, ac, dy) def hostdel(self, hostname): with self.lock: if self.cache.has_key(hostname): del self.cache[hostname] self.rngcnt -= 1 def load(self, fname=None, maxcnt=0): if maxcnt > 0: self.maxcnt = maxcnt if fname: self.fname = fname self.rngcnt = 0 self.cache = {} try: with open(self.fname) as fp: for ln in fp: try: hostname, edtime, dcnt, acnt, dyn = ln.strip().split(',') et = float(edtime) dc = int(dcnt) ac = int(acnt) dy = int(dyn) self.cache[hostname] = (et, dc, ac, dy) self.rngcnt += 1 except: continue except: pass def save(self, fname=None, maxcnt=0): if maxcnt > 0: self.maxcnt = maxcnt if fname: self.fname = fname now = time.time() - self.maxcnt*24*60*60 try: with open(self.fname, "w") as fp: for hostname, v1 in self.cache.items(): try: et, dc, ac, dy = v1 if (ac > 0) or ((ac == 0) and (et > now)): fp.write('%s,%f,%d,%d,%d\n' % (hostname, et, dc, ac, dy)) except: continue except: pass ## (システムに合わせて修正が必要です) ## dsnerror = HostCache(fname="/var/log/pwmail/dsnerror.log", maxcnt=10) ## ダイナミックドメインホスト ##  ドメイン部分の抽出 fqchar = '-abcdefghijklmnopqrstuvwxyz' def DYNSplit(name, ipad): def DNSplit(name, tad): tn = name.rpartition(tad) if tn[1] == '': return None if tn[0] != '': pc = tn[0].count('.') if pc > 1: return None pc = tn[2].find('.') return tn[2][pc + 1:] rv = DNSplit(name, ipad) if rv: return rv ab = ipad.split('.') rab = [] + ab rab.reverse() ripad = '.'.join(rab) rv = DNSplit(name, ripad) if rv: return rv ad2 = ab[0] + '-' + ab[1] rad2 = ab[1] + '-' + ab[0] rv = DNSplit(name, rad2) if rv: pc = rv.find('.') prv = rv[:pc] if prv.find(rad2) >= 0: wrv = rv[pc + 1:] if fqdnjp.search(wrv): rv = wrv elif prv.find(ad2) >= 0: wrv = rv[pc + 1:] if fqdnjp.search(wrv): rv = wrv return rv rv = DNSplit(name, ad2) if rv: pc = rv.find('.') prv = rv[:pc] if prv.find(ad2) >= 0: wrv = rv[pc + 1:] if fqdnjp.search(wrv): rv = wrv elif prv.find(rad2) >= 0: wrv = rv[pc + 1:] if fqdnjp.search(wrv): rv = wrv return rv sad3 = ab[0].rjust(3,'0') + ab[1].rjust(3,'0') + ab[2].rjust(3,'0') rv = DNSplit(name, sad3) if rv: return rv hn = name.split('.') if (hn[0].lstrip(fqchar) == ab[3]) and (hn[1].lstrip(fqchar) == ab[2]): if (hn[2].lstrip(fqchar) in ab[0:2]): if (hn[3].lstrip(fqchar) in ab[0:2]): rv = '.'.join(hn[4:]) if fqdnjp.search(rv): return rv rv = '.'.join(hn[3:]) if fqdnjp.search(rv): return rv rv = '.'.join(hn[2:]) return rv rv = '.'.join(hn[1:]) return rv ## ダイナミックドメインホスト ##  ドメイン部分の抽出テスト用ログ class DYNDomainCheck: # ドメイン名 # フルホスト名 # IPアドレス # アクセス回数 def __init__(self, fname=None, maxrng=3000): self.maxrng = maxrng self.rngcnt = 0 self.cache = {} self.fname = fname self.lock = threading.Lock() #with self.lock #self.lock.acquire() #try: # #finally: # self.lock.release() def DYNdomain(self, hname, ipad): dname = DYNSplit(hname, ipad) with self.lock: if not self.cache.has_key(dname): if self.rngcnt < self.maxrng: ac = 1 self.rngcnt += 1 self.cache[dname] = (hname, ipad, ac) return dname wn, wad, ac = self.cache[dname] ac += 1 self.cache[dname] = (hname, ipad, ac) return dname def load(self, fname=None): if fname: self.fname = fname self.rngcnt = 0 self.cache = {} try: with open(self.fname) as fp: for ln in fp: try: dname, hname, ipad, acnt = ln.strip().split(',') ac = int(acnt) self.cache[dname] = (hname, ipad, ac) self.rngcnt += 1 except: continue except: pass def save(self, fname=None): if fname: self.fname = fname try: with open(self.fname, "w") as fp: for dname, v1 in self.cache.items(): try: hname, ipad, ac = v1 fp.write('%s,%s,%s,%d\n' % (dname, hname, ipad, ac)) except: continue except: pass ## (システムに合わせて修正が必要です) ## dyndcheck = DYNDomainCheck(fname="/var/log/pwmail/dyndcheck.log") ## ホワイトリストチェック class WhitelistCheck: # connect : CBdomain # hello : HBdomain # mailfrom : FBdomain # モード : 送信者・差出人のアドレス管理 # : d(ドメイン違い), u(ユーザ違い), n(同アドレス) # return : モード + None(nolist) def __init__(self, fname=None): self.rngcnt = 0 self.cache = {} self.fname = fname #with self.lock #self.lock.acquire() #try: # #finally: # self.lock.release() def WhiteCheck(self, CBdomain, HBdomain, FBdomain): if not self.cache.has_key((CBdomain, HBdomain, FBdomain)): return None return self.cache[(CBdomain, HBdomain, FBdomain)] def load(self, fname=None): if fname: self.fname = fname self.rngcnt = 0 self.cache = {} try: with open(self.fname) as fp: for ln in fp: try: CBdomain, HBdomain, FBdomain, Amode = ln.strip().split(',') if len(Amode) == 1: self.cache[(CBdomain, HBdomain, FBdomain)] = Amode else: self.cache[(CBdomain, HBdomain, FBdomain)] = Amode + ' ' self.rngcnt += 1 except: continue except: pass ## (システムに合わせて修正が必要です) ## wlistcheck = WhitelistCheck(fname="/usr/local/pwmail/whitelist.dat") ###外部############### # Shift JIS UTF8 コードをunicodeに編集 def msg_cnvt(s): u = u"" for enc1 in ('utf8', 'cp932'): try: u = unicode(s, enc1) except UnicodeDecodeError: continue break if u == u"": u = unicode(s, 'utf8', 'replace') return u ### ### Milter.utils コピーを修正 def parse_header(val): """Decode headers gratuitously encoded to hide the content. """ try: h = decode_header(val) if not len(h): return val u = [] for s, enc in h: if enc: try: u.append(unicode(s, enc)) except LookupError: u.append(unicode(s)) else: if isinstance(s, unicode): u.append(s) else: u.append(msg_cnvt(s)) u = ''.join(u) for enc in ('us-ascii', 'iso-8859-1', 'utf8'): try: return u.encode(enc) except UnicodeError: continue except UnicodeDecodeError: pass except LookupError: pass except email.Errors.HeaderParseError: pass return val # FQDNシンタックスチェック def fqdncheck(s): if fqdn.match(s): if s.find('-.') < 0: return True return False # 受信ローカルドメインチェック def my_domain_check(td): tl = len(td) for d, l in my_domainlist: if tl == l: if td == d: return True elif tl > l: if td.endswith(d): pw = td[tl - l -1] if (pw == '.') or (pw == '@'): return True return False # ドメイン部分が同じかをチェック def eq_domain_check(td, d): tl = len(td) l = len(d) if tl == l: if td == d: return True elif tl > l: if td.endswith(d): if td[tl - l -1] == '.': return True return False # ドメイン部分が抽出 def DomainSplit(name): pos = name.find('.') dmain = name[pos + 1:] if fqdnjp.search(dmain): return dmain return None class myMilter(Milter.Base): def __init__(self): # A new instance with each new connection. self.id = Milter.uniqueID() # Integer incremented with each call. # SMTP エンベロープを編集 def msg_connect(self, rcpt_addr): if not self.Cname: Cname = u"" else: Cname = msg_cnvt(self.Cname) if not self.CBdomain: CBdomain = u"" else: CBdomain = msg_cnvt(self.CBdomain) if not self.IP: IP = u"" else: IP = msg_cnvt(self.IP) if not self.Hname: Hname = u"" else: Hname = msg_cnvt(self.Hname) if not self.HBdomain: HBdomain = u"" else: HBdomain = msg_cnvt(self.HBdomain) if not self.Fname: Fname = u"" else: Fname = msg_cnvt(self.Fname) if not self.FBdomain: FBdomain = u"" else: FBdomain = msg_cnvt(self.FBdomain) if not rcpt_addr: wrcpt_addr = u"" else: wrcpt_addr = msg_cnvt(rcpt_addr) try: msg = (u"connect from %s(%s) at %s\n" "helo: %s(%s)\n" "mail from: %s(%s)\n" % (Cname, CBdomain, IP, Hname, HBdomain, Fname, FBdomain)) if rcpt_addr: msg += (u"rcpt to: " + wrcpt_addr + "\n") return msg except: self.log_warning("msg_connect:", traceback.format_exc()) return u"" # SMTP メールヘッダを編集 def msg_Header(self): if not self.HFrom: hfrom = u"" else: hfrom = msg_cnvt(self.HFrom) if not self.Subject: subject = u"" else: subject = msg_cnvt(self.Subject) if not self.HDate: hdate = u"" else: hdate = msg_cnvt(self.HDate) if not self.HMid: hmid = u"" else: hmid = msg_cnvt(self.HMid) try: return (u"Header-From: %s\n" "Header-Subject: %s\n" "Header-Date: %s\n" "Message-ID: %s\n" % (hfrom, subject, hdate, hmid)) except: self.log_warning("msg_Header:", traceback.format_exc()) return u"" # ログメール システム用(SMTP エンベロープ) def send_admin0(self, subject, ERlevel): encoding = "ISO-2022-JP" body = self.msg_connect(None) + u"X-PWmail: %s\n" % self.PWmsg mf = parse_addr(to_pwadmin) un = mf[0] pos = un.find('+') if pos != -1: un = un[:pos] from_addr = un + "+admin@" + mf[1] to_addr = un + "+" + ERlevel + "@" + mf[1] try: msg = MIMEText(body.encode(encoding, 'replace'), 'plain', encoding) msg['Subject'] = Header(subject, encoding) msg['From'] = from_addr msg['To'] = to_addr msg['Date'] = formatdate() except: self.log_warning('send_admin1 MIMEText:', traceback.format_exc()) return s = smtplib.SMTP('localhost', sendmail_port, 'localhost') try: s.sendmail(from_addr, [to_pwadmin], msg.as_string()) except: self.log_warning('send_admin1 sendmail:', traceback.format_exc()) s.quit() # ログメール システム用(SMTP エンベロープ) def send_admin1(self, rcpt_addr, subject, ERlevel): encoding = "ISO-2022-JP" body = self.msg_connect(rcpt_addr) + u"X-PWmail: %s\n" % self.PWmsg mf = parse_addr(rcpt_addr) un = mf[0] pos = un.find('+') if pos != -1: un = un[:pos] from_addr = un + "+admin@" + mf[1] to_addr = un + "+" + ERlevel + "@" + mf[1] try: msg = MIMEText(body.encode(encoding, 'replace'), 'plain', encoding) msg['Subject'] = Header(subject, encoding) msg['From'] = from_addr msg['To'] = to_addr msg['Date'] = formatdate() except: self.log_warning('send_admin1 MIMEText:', traceback.format_exc()) return s = smtplib.SMTP('localhost', sendmail_port, 'localhost') try: s.sendmail(from_addr, [to_pwadmin], msg.as_string()) except: self.log_warning('send_admin1 sendmail:', traceback.format_exc()) s.quit() # ログメール システム用 #(SMTP エンベロープ・メールヘッダ) def send_admin2(self, rcpt_addr, subject, ERlevel): encoding = "ISO-2022-JP" body = self.msg_connect(rcpt_addr) + self.msg_Header() + \ u"X-PWmail: %s\n" % (self.PWmsg) mf = parse_addr(rcpt_addr) un = mf[0] pos = un.find('+') if pos != -1: un = un[:pos] from_addr = un + "+admin@" + mf[1] to_addr = un + "+" + ERlevel + "@" + mf[1] try: msg = MIMEText(body.encode(encoding, 'replace'), 'plain', encoding) msg['Subject'] = Header(subject, encoding) msg['From'] = from_addr msg['To'] = to_addr if not self.HDate: msg['Date'] = formatdate() else: msg['Date'] = self.HDate except: self.log_warning('send_admin2 MIMEText:', traceback.format_exc()) return s = smtplib.SMTP('localhost', sendmail_port, 'localhost') try: s.sendmail(from_addr, [to_pwadmin], msg.as_string()) except: self.log_warning('send_admin2 sendmail:', traceback.format_exc()) s.quit() # ログメール システム&ユーザ用 #(SMTP エンベロープ・メールヘッダ) def send_admin3(self, rcpt_addr, subject, ERlevel): encoding = "ISO-2022-JP" body = self.msg_connect(rcpt_addr) + self.msg_Header() + \ u"X-PWmail: %s\n" % (self.PWmsg) mf = parse_addr(rcpt_addr) un = mf[0] pos = un.find('+') if pos != -1: un = un[:pos] from_addr = un + "+admin@" + mf[1] to_addr = un + "+" + ERlevel + "@" + mf[1] try: msg = MIMEText(body.encode(encoding, 'replace'), 'plain', encoding) msg['Subject'] = Header(subject, encoding) msg['From'] = from_addr msg['To'] = to_addr if not self.HDate: msg['Date'] = formatdate() else: msg['Date'] = self.HDate except: self.log_warning('send_admin3 MIMEText:', traceback.format_exc()) return s = smtplib.SMTP('localhost', sendmail_port, 'localhost') try: s.sendmail(from_addr, [to_addr, to_pwadmin], msg.as_string()) except: self.log_warning('send_admin3 sendmail:', traceback.format_exc()) s.quit() ###外部????############### # nameがipadを示しているかをチェック # nameがホスト名の場合にアドレスが存在するかをチェック # nameがドメイン名の場合にMXレコードが存在するかをチェック # DNSが正常かをチェック def DNSCheck(self, name, ipad): adok = False ad4ok = False adng = False mxok = False mx4ok = False mx6ok = False mxng = False rv = False rm = rt = '' s = DNSSession() try: ads = s.dns(name, 'A') if len(ads) > 0: ad4ok = True for a in ads: if a == ipad: adok = True elif not a: adng = True mxr = s.dns(name, 'MX') if len(mxr) > 0: for v, n in mxr: ads = s.dns(n, 'A') if len(ads) > 0: mx4ok = True for a in ads: if a == ipad: mxok = True elif not a: mxng = True else: ads = s.dns(n, 'AAAA') if len(ads) > 0: mx6ok = True for a in ads: if not a: mxng = True else: mxng = True if adng or mxng or ((not ad4ok) and (not mx4ok)): rv = None else: if adok: rv = True rm += 'A' if mxok: rv = True rm += 'M' if ad4ok: rt += 'A' if mx4ok: rt += 'M' if mx6ok: rt += '6' except: self.log_warning("DNS:", traceback.format_exc()) rv = None rm = rt = 'DNS' return (rv, rm, rt) def HostCheck(self, Hname, ipad, dyn): ## ホスト名部分のキープ #################################### hcheck = dsnerror.hostcheck(Hname, dyn) if hcheck: ipok = (None, '', '') else: ipok = self.DNSCheck(Hname, ipad) if ipok[0] == None: if hcheck == None: dsnerror.hostadd(Hname, dyn) elif hcheck == False: dsnerror.hostdel(Hname) return ipok # @Milter.noreply def connect(self, IPname, family, hostaddr): # 設定する変数 ## self.IPname 未使用 # self.Cname # self.Cfqdn # self.Cdynip # self.Cipok # self.Sabort # # REJECTの使用可能なエラーコマンドとステイタス # 554 Transaction failed (Or, in the case of a connection-opening # response, "No SMTP service here") # X.3.5 System incorrectly configured self.log("connect from %s at %s" % (IPname, hostaddr)) # Ini Setup self.PWmsg = "" self.PWmode = "" self.Sabort = None if hostaddr and len(hostaddr) > 0: self.IP = hostaddr[0] else: self.log_critical("REJECT: connect attacks") self.setreply('554', '5.3.5', 'Banned for connect attacks') return Milter.REJECT self.IP = hostaddr[0] ## self.port = hostaddr[1] self.Cname = IPname.lower() # Name from a reverse IP lookup if fqdncheck(self.Cname): self.Cfqdn = True self.Cdynip = dynip(self.Cname, self.IP) self.Cipok = self.HostCheck(IPname, self.IP, self.Cdynip) if (not self.Cipok[0]) and (self.Cname != IPname): self.Cipok = self.HostCheck(self.Cname, self.IP, self.Cdynip) ## 現在postfix&Milter側でREJECTされている if not self.Cipok[0]: self.log_critical("REJECT: connect attacks: DNS Host not found.") self.setreply('554', '5.3.5', 'Banned for connect attacks: DNS Host not found.') return Milter.REJECT if self.Cdynip: self.CBdomain = dyndcheck.DYNdomain(self.Cname, self.IP) else: self.CBdomain = DomainSplit(self.Cname) if not self.CBdomain: self.CBdomain = self.Cname else: self.Cfqdn = False self.Cipok = (None, '', '') self.Cdynip = None self.CBdomain = None self.Hname = None self.Hmyd = None self.Hipok = (None, '', '') self.Hrelay = None return Milter.CONTINUE # @Milter.noreply def hello(self, heloname): # 設定する変数 ## self.heloname 未使用 # self.Hname # self.Hfqdn # self.Hmyd # self.Hdynip # self.Hipok # # REJECTの使用可能なエラーコマンドとステイタス # 504 Command parameter not implemented # X.5.1 Invalid command # X.5.1 不正なコマンド # X.5.2 Syntax error # X.5.2 構文エラー hname = msg_cnvt(heloname) self.log("HELO", hname.encode('utf8', 'replace')) # Ini Setup self.PWmsg = "" self.PWmode = "" if self.Cfqdn: if self.Cdynip: self.PWmsg = self.PWmsg + "Cdynip " # yellow if self.Cipok[0] == None: self.PWmsg = self.PWmsg + "ngCipok " # error elif self.Cipok[0] == False: self.PWmsg = self.PWmsg + "noCipok " # error gray else: self.PWmsg = self.PWmsg + "noCfqdn " self.Hname = heloname.lower() self.Hmyd = None self.Hipok = (None, '', '') self.Hrelay = None if not fqdncheck(self.Hname): # if ip4re.match(self.Hname): hnameip = self.Hname if hnameip[0] == '[': hnameip = hnameip[1:-1] if hnameip == my_hnameip: self.Hmyd = True self.PWmsg = self.PWmsg + "Hmyd " # error # 不正なコマンド として REJECT する必要性がある elif not hostre.match(self.Hname): self.log_critical("REJECT: Helo command Syntax error") self.setreply('554', '5.5.2', '<%s>: Helo command Syntax error' % (heloname)) return Milter.REJECT # self.Hfqdn = False self.Hdynip = None self.HBdomain = None self.PWmsg = self.PWmsg + "noHfqdn " # error gray return Milter.CONTINUE self.Hfqdn = True self.HBdomain = self.Hname self.Hrelay = False self.Hmyd = my_domain_check(self.Hname) if self.Hmyd: self.PWmsg = self.PWmsg + "Hmyd " # error return Milter.CONTINUE self.Hdynip = dynip(self.Hname, self.IP) if self.Hdynip: self.PWmsg = self.PWmsg + "Hdynip " # yellow if (not self.Cdynip) or (self.Cname != self.Hname): self.HBdomain = dyndcheck.DYNdomain(self.Hname, self.IP) if self.Cfqdn: if self.Cname == self.Hname: self.Hipok = self.Cipok self.HBdomain = self.CBdomain if self.Hipok[0] == None: self.PWmsg = self.PWmsg + "ngHipok " # error elif self.Hipok[0] == False: self.PWmsg = self.PWmsg + "noHipok " # error gray return Milter.CONTINUE if self.Cipok[0]: if self.Hdynip: if eq_domain_check(self.Cname, self.HBdomain): #type2 wHipok = self.HostCheck(self.Hname, self.IP, self.Hdynip) if wHipok[0]: self.Hipok = wHipok else: self.Hipok = (True, self.Cipok[1], wHipok[2]) # 同一ドメイン内のリレー及び記述ミスの回避 wHipok = self.HostCheck(self.HBdomain, self.IP, False) if wHipok[0]: self.Hipok = wHipok return Milter.CONTINUE else: if eq_domain_check(self.Cname, self.Hname): #type1 wHipok = self.HostCheck(self.Hname, self.IP, self.Hdynip) if wHipok[0]: self.Hipok = wHipok else: self.Hipok = (True, self.Cipok[1], wHipok[2]) return Milter.CONTINUE # 同一ドメイン内のリレー及び記述ミスの回避 Hdmain = DomainSplit(self.Hname) if Hdmain: if eq_domain_check(self.Cname, Hdmain): #type2 self.HBdomain = Hdmain wHipok = self.HostCheck(Hdmain, self.IP, False) if wHipok[0]: self.Hipok = wHipok else: self.Hipok = (True, self.Cipok[1], wHipok[2]) return Milter.CONTINUE self.Hrelay = True self.Hipok = self.HostCheck(self.Hname, self.IP, self.Hdynip) # 送信ホスト名の記述ミスの回避 if (not self.Hipok[0]): if (self.Hname != heloname): self.Hipok = self.HostCheck(heloname, self.IP, self.Hdynip) if self.Hdynip: if not self.Hipok[0]: wHipok = self.HostCheck(self.HBdomain, self.IP, False) if wHipok[0]: self.Hipok = wHipok else: if self.Hipok[2].find('M') < 0: Hdmain = DomainSplit(self.Hname) if Hdmain: self.HBdomain = Hdmain # 送信ホスト名の記述ミスの回避 wHipok = self.HostCheck(self.HBdomain, self.IP, False) if wHipok[0]: self.Hipok = wHipok if self.Hipok[0] == None: self.PWmsg = self.PWmsg + "ngHipok " # error elif self.Hipok[0] == False: self.PWmsg = self.PWmsg + "noHipok " # error gray if self.Hrelay: self.PWmsg = self.PWmsg + "Hrelay " # yellow return Milter.CONTINUE # @Milter.noreply def envfrom(self, mailfrom, *str): def fromcheck(wmailfrom): # 設定する変数 # self.Fname # self.Fad # self.Fd # self.Ffqdn # self.Fmyd # self.Fdynip # self.Fipok self.Fname = wmailfrom self.Fad = '' self.Fd = '' self.Ffqdn = False self.Fmyd = False self.Fdynip = None self.Fipok = (None, '', '') if wmailfrom == '<>': self.Ffqdn = None return None mb = parseaddr(wmailfrom) if (mb[1] == None) or (mb[1] == ''): self.PWmsg = self.PWmsg + "noFfqdn " # error return None self.Fad = mb[1] mf = parse_addr(mb[1]) if len(mf) != 2: self.PWmsg = self.PWmsg + "noFfqdn " # error return None self.Fd = mf[1].lower() if not fqdncheck(self.Fd): self.PWmsg = self.PWmsg + "noFfqdn " # error return None self.Ffqdn = True self.FBdomain = self.Fd self.Fmyd = my_domain_check(self.Fd) if self.Fmyd: self.PWmsg = self.PWmsg + "Fmyd " # error return None self.Fdynip = dynip(self.Fd, self.IP) if self.Fdynip: # 通常は不正なコマンド として REJECT する必要性がある self.PWmsg = self.PWmsg + "Fdynip " # error self.FBdomain = dyndcheck.DYNdomain(self.Fd, self.IP) self.Fipok = self.HostCheck(self.Fd, self.IP, self.Fdynip) if self.Fipok[0] == None: self.PWmsg = self.PWmsg + "ngFipok " # error not(MX/A) elif self.Fipok[0] == False: self.PWmsg = self.PWmsg + "noFipok " # OK else: # True return False # 同ドメイン内のリレーチェック(許可) if self.Hfqdn: if eq_domain_check(self.Hname, self.FBdomain): #type1 return False if not self.Fdynip: Fdmain = DomainSplit(self.Fd) if Fdmain: if eq_domain_check(self.Hname, Fdmain): #type2 self.FBdomain = Fdmain return False return True return False # 設定する変数 # self.Fname # self.Fad # self.Fd # self.Ffqdn # self.Fmyd # self.Fdynip # self.Fipok # self.Relay # # REJECTの使用可能なエラーコマンドとステイタス # 550 # 555 MAIL FROM/RCPT TO parameters not recognized or not implemented # 555 MAIL FROM/RCPT TO のパラメータが認められていないか実装されていません # X.1.8 Bad sender's system address # X.1.8 正しくない送り手のシステムのアドレス # X.4.3 Directory server failure # X.4.3 ディレクトリサーバの失敗 # X.4.4 Unable to route # X.4.4 ルーティング不能 self.log("mail from:", mailfrom, *str) # Ini Setup self.Rname = [] # list of recipients self.Relay = fromcheck(mailfrom) if self.Relay: self.PWmsg = self.PWmsg + "relay " # yellow ### ### 送信はsubmission経由を前提に作成しています。 ### ### Abort 条件の追加 以降のフィルターは、 ### rcptコマンド迄にREJECTするとハングするかheloコマンドからリトライするサーバがあります。 ### ### 全ての宛先をログに残す場合は、エラーコマンドの 550 を 450 に修正して下さい。 ### この場合には、何らかの方法(DB等)で検証して2回目からは 550 で対応する事を進めます。 ## Abort 条件の追加 ## 送信者ドメイン等でIP確定する場合には、connect Helo コマンドでのチェックはしない。 ## 「マルチドメインサーバに対する対応と初心者の設定ミスに対しての対応」 if not self.Fipok[0]: ### 送信サーバがホスト名等でIP確定出来ない ### 現在connectコマンド側で処理されている。 #if (self.Cfqdn) and (not self.Cipok[0]): # self.setreply('550', '5.1.8', # '(%s:%s): connectHost rejected: DNS Host not found.' % # (self.Cname,self.IP)) # self.log_error('550', 'connectHost rejected: DNS Host not found.') # ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする # #self.send_admin0(u"送信サーバがホスト名等でIP確定出来ない", "abort0") # #self.send_admin1(recipient, u"送信サーバがホスト名等でIP確定出来ない", "abort0") # self.Sabort = False # return Milter.REJECT ## 基本は、Helo コマンドの FQDN である if not self.Hfqdn: self.setreply('550', '5.5.2', '<%s>: Helo command rejected: ' 'need fully-qualified hostname.' % (self.Hname)) self.log_error('550', 'Helo command rejected: need fully-qualified hostname.') ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする #self.send_admin0(u"Heloコマンドのホスト名は、FQDNでない", "abort0") #self.send_admin1(recipient, u"Heloコマンドのホスト名は、FQDNでない", "abort0") self.Sabort = False return Milter.REJECT ### Helo コマンドのドメイン名等でIP確定する場合は、connectHostのチェックはしない。 if (self.Hipok[0] == None) or (self.Hipok[0] == False) and (not self.Cipok[0]): ### Helo コマンドのドメイン名等でIP確定出来ない self.setreply('550', '5.1.8', '(%s:%s): Helo command rejected: DNS Host(MX) not found.' % (self.Hname,self.IP)) self.log_error('550', 'Helo command rejected: DNS Host(MX) not found.') ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする #self.send_admin0(u"Helo コマンドのドメイン名等でIP確定出来ない", "abort0") #self.send_admin1(recipient, u"Helo コマンドのドメイン名等でIP確定出来ない", "abort0") self.Sabort = False return Milter.REJECT ### Helo コマンドのホスト名が受信ドメインである ### 送信がsubmission経由の場合には、ありえない事です。 if self.Hmyd: self.setreply('550', '5.5.2', '<%s>: Helo command rejected: Breach of Local Policy.' % (self.Hname)) self.log_critical('550', 'Helo command rejected: Breach of Local Policy.') ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする self.send_admin0(u"送信ホスト名がマイドメイン", "abort") #self.send_admin1(recipient, u"送信ホスト名がマイドメイン", "abort") self.Sabort = False return Milter.REJECT if self.Fname != '<>': # (postmaster) OK ## SenderHost は、FQDN記述が必要である(ダイナミックDNSでは、返信不能の可能性がある) if (self.Ffqdn == False) or self.Fdynip: self.setreply('550', '5.5.2', '%s: Sender address rejected: ' 'need fully-qualified address.' % (self.Fname)) self.log_critical('550', 'Sender address rejected: ' 'need fully-qualified address.') ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする self.send_admin0(u"送信者名エラー", "abort") #self.send_admin1(recipient, u"送信者名エラー", "abort") self.Sabort = False return Milter.REJECT ### SenderHostが返信不能である ## リトライで真意の確認をする必要性があるのか? ## その場合は、Message-ID の確認が必要と思われる if self.Fipok[0] == None: self.setreply('550', '5.4.3', '%s: Sender address rejected: Breach of Domain.' % (self.Fname)) self.log_critical('550', 'Sender address rejected: Breach of Domain.') ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする self.send_admin0(u"送信者名がDNSエラー", "abort") #self.send_admin1(recipient, u"送信者名がDNSエラー", "abort") self.Sabort = False return Milter.REJECT ### SenderHostが受信ドメインである ### 送信がsubmission経由の場合には、ありえない事です。 if self.Fmyd: self.setreply('550', '5.5.2', '%s: Sender address rejected: Breach of Local Policy.' % (self.Fname)) self.log_critical('550', 'Sender address rejected: Breach of Local Policy.') ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする self.send_admin0(u"送信者がマイドメイン", "abort") #self.send_admin1(recipient, u"送信者がマイドメイン", "abort") self.Sabort = False return Milter.REJECT return Milter.CONTINUE # @Milter.noreply def envrcpt(self, recipient, *str): # 設定する変数 # self.Rname # # REJECTの使用可能なエラーコマンドとステイタス # 550 # 553 Requested action not taken: mailbox name not allowed # (e.g.,mailbox syntax incorrect) # X.1.1 Bad destination mailbox address # X.1.1 正しくない宛先メールボックスのアドレス # X.7.1 Delivery not authorized, message refused # X.7.1 配送が正当と認められず、メッセージが拒絶された # X.2.2 Mailbox full # X.2.2 メールボックスが一杯 # # 450 Requested mail action not taken: mailbox unavailable # (e.g.,mailbox busy or temporarily blocked for policy reasons) # 450 メールボックスが利用できないため(例えば、メールボックスが使用中 # であったり、ポリシーの理由で一時的にブロックされたなど)要求された # メールの動作ができません # X.2.1 Mailbox disabled, not accepting messages # X.2.1 メールボックスが利用不可能、メッセージを受け取らない # X.2.2 Mailbox full # X.2.2 メールボックスが一杯 self.log("rcpt to:", recipient, ":", *str) self.Rname.append(recipient) ### ### 送信はsubmission経由を前提に作成しています。 ### ## 外部からはロギング用メールアドレスを拒否する。 ## 拒否しない場合、コメントアウトする if recipient.lower() == to_pwadmin: self.setreply('550', '5.1.1', '<%s>: Recipient address rejected: User unknown.' % (recipient)) self.log_critical('550', 'Recipient address rejected: User unknown.') ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする self.send_admin1(recipient, u"ロギング用メールアドレス", "abort") self.Sabort = False return Milter.REJECT ## 外部からは拡張アドレスを拒否する。 ## ユーザ名に+が入っているとエラー処理(postfix recipient_delimiter = +)の設定で修正必要 ## 拒否しない場合、コメントアウトする if recipient.find(recipient_delimiter) != -1: self.setreply('550', '5.1.1', '<%s>: Recipient address rejected: User unknown.' % (recipient)) self.log_critical('550', 'Recipient address rejected: User unknown.') ## ロギング用メールアドレスに記録を残さない場合、下記1行をコメントアウトする self.send_admin1(recipient, u"ユーザ名エラー", "abort") self.Sabort = False return Milter.REJECT return Milter.CONTINUE # @Milter.noreply def data(self): # # REJECTの使用可能なエラーコマンドとステイタス # 550 rejections for policy reasons # 450 rejections for policy reasons # 550 450 ポリシーによる理由での拒否 # X.7.1 Delivery not authorized, message refused # X.7.1 配送が正当と認められず、メッセージが拒絶された # # X.1.8 Bad sender's system address # X.1.8 正しくない送り手のシステムのアドレス # X.4.3 Directory server failure # X.4.3 ディレクトリサーバの失敗 # X.4.4 Unable to route # X.4.4 ルーティング不能 # X.5.2 Syntax error # X.5.2 構文エラー ## self.log("data") self.log_debug("data") # Ini Setup self.FromAD = [] self.HFrom = None self.HDate = None self.Subject = None self.HMid = None self.HList = None return Milter.CONTINUE # @Milter.noreply def header(self, name, hval): ### self.log_debug("header:%s: %s" % (name,hval)) nbuf = name.lower() if nbuf == "from": ms = [] adbuf = hval.split(',') for ad in adbuf: ma = parseaddr(ad) mn = parse_header(ma[0]) ms.append(mn + ' <' + ma[1] + '>') self.FromAD.append((mn, ma[1])) mf = ",".join(ms) if not self.HFrom: self.HFrom = mf else: self.HFrom = self.HFrom + ',' + mf self.log_debug("Header-From-B:", hval) self.log("Header-From:", mf) elif nbuf == "date": self.HDate = hval elif nbuf == "subject": self.Subject = parse_header(hval) self.log_debug("Subject-B:", hval) self.log("Subject:", self.Subject) elif nbuf == "message-id": self.log("Message-ID:", hval) self.HMid = hval elif nbuf.startswith("list-"): self.HList = True return Milter.CONTINUE # @Milter.noreply def eoh(self): def whitech(): CBdomain = '' HBdomain = '' if not self.Fipok[0]: if self.Relay: HBdomain = self.HBdomain if self.Hrelay: HBdomain = self.HBdomain CBdomain = self.CBdomain wlch = wlistcheck.WhiteCheck(CBdomain, HBdomain, self.FBdomain) if wlch: self.PWmsg = self.PWmsg + "White " if wlch == 'd': return True elif wlch == 'u': if self.HFdnok: return True elif wlch == 'n': if self.HFadok: return True else: if wlch[0] == 'd': if wlch.find(' ' + self.HFromDN + ' ') >= 0: return True elif wlch[0] == 'a': if wlch.find(' ' + self.HFromAD + ' ') >= 0: return True return False ## self.log("eoh") self.log_debug("eoh") ### 日付フォーマットチェックが必要 if not self.HDate: self.PWmsg = self.PWmsg + "noHDate " # error if not self.HMid: self.PWmsg = self.PWmsg + "noHMid " # yellow (self.Cdynip,self.Hdynip,self.Relay) True:Error # Ini Setup self.HFromAD = None self.HFromDN = None self.HFfqdn = None self.HFmyd = False self.HFdynip = None self.HFipok = (None, '', '') self.HFdnok = None self.HFadok = None if len(self.FromAD) != 1: self.HFadER = True self.PWmsg = self.PWmsg + "HFadER " else: self.HFadER = False ad = self.FromAD[0][1] ### アマゾン特別対応 if (ad[0] == "'") and (ad[-1] == "'"): ad = ad[1:-1] ### アマゾン特別対応 self.HFromAD = ad if (ad == None) or (ad == ''): self.HFfqdn = False self.PWmsg = self.PWmsg + "noHFfqdn " else: mf = parse_addr(ad) if len(mf) != 2: self.HFfqdn = False self.PWmsg = self.PWmsg + "noHFfqdn " else: dn = mf[1].lower() if not fqdncheck(dn): self.HFfqdn = False self.PWmsg = self.PWmsg + "noHFfqdn " else: self.HFfqdn = True self.HFromDN = dn self.HFmyd = my_domain_check(dn) if self.HFmyd: self.PWmsg = self.PWmsg + "HFmyd " else: self.HFdynip = dynip(dn, self.IP) if self.HFdynip: self.PWmsg = self.PWmsg + "HFdynip " self.HFipok = self.HostCheck(dn, self.IP, self.HFdynip) if self.HFipok[0] == None: self.PWmsg = self.PWmsg + "ngHFipok " elif self.HFipok[0] == False: self.PWmsg = self.PWmsg + "noHFipok " if self.Ffqdn: if eq_domain_check(self.Fd, dn): #type1 self.HFdnok = True else: pos = dn.find('.') sdn = dn[pos + 1:] if fqdnjp.search(sdn): if eq_domain_check(self.Fd, sdn): #type2 self.HFdnok = True if not self.HFdnok: self.HFdnok = False self.PWmsg = self.PWmsg + "noHFdnok " if self.Fad == ad: self.HFadok = True else: self.HFadok = False self.PWmsg = self.PWmsg + "noHFadok " if self.HList: self.PWmsg = self.PWmsg + "List " ### 日付は、必要事項である if not self.HDate: self.setreply('550', '5.7.1', 'Breach of Header-Date Policy.') self.log_critical('550', 'Breach of Header-Date Policy.') ## ロギング用メールアドレスに記録を残さない場合、下記2行をコメントアウトする for rad in self.Rname: self.send_admin2(rad, u"ヘッダー日付エラー", "abort") return Milter.REJECT ### 差出人への返信不能 ### 差出人は、複数登録できるが現在では一名を推奨している。 ### またメーラの基本操作では登録出来ない。(self.HFadER) if self.HFadER or (not self.HFfqdn) or self.HFmyd or \ self.HFdynip or (self.HFipok[0] == None): self.setreply('550', '5.7.1', '%s: Breach of Header-From Local Policy.' % (','.join([m for d, m in self.FromAD]))) self.log_critical('550', 'Breach of Header-From Local Policy.') ## ロギング用メールアドレスに記録を残さない場合、下記3行をコメントアウトする for rad in self.Rname: self.send_admin3(rad, u"差出人エラー", "abort") return Milter.REJECT ### 通常のメールサーバから送信されていない ### 再送要求上不可欠の要素である。また法令上の開示請求に対応できない。 ### RFC上では、任意であるが早く必要事項としてフォーマットを含め定義するべきである。 if not self.HMid: self.setreply('550', '5.7.1', '%s: Breach of Message-ID Local Policy.' % (','.join([m for d, m in self.FromAD]))) self.log_critical('550', 'Breach of Message-ID Local Policy.') ## ロギング用メールアドレスに記録を残さない場合、下記3行をコメントアウトする for rad in self.Rname: self.send_admin3(rad, u"Message-IDエラー", "black") return Milter.REJECT ## ホワイトリストチェック ## チェックルールのテストロジック if whitech(): self.PWmode = "white" for rad in self.Rname: self.send_admin2(rad, u"ホワイトリスト", "white") return Milter.CONTINUE ## 警告用ログと警告モードを残す。 ## ML・NEWS等のルールが規格化されているのであれば、厳格に管理出きるのですが? ## 現状危険なメールを監視するように心がけています。 ## 何らかの方法(DB等)で安心出来るheloホスト名等と送信者ドメインを管理出来れば警告を削減出来るのですが? if self.Cdynip or self.Hdynip or self.Hrelay or self.Relay or (not self.Hipok[0]) or \ (self.Ffqdn and (not self.HFdnok) or (not self.HFadok)) or \ (self.Fname == '<>'): self.PWmode = "gray" ## ロギング用メールアドレスに記録を残さない場合、下記をコメントアウトする msg = u"要注意:" if self.Cdynip or self.Hdynip: msg = msg + u"ダイナミックDNS " if not self.Hipok[0]: msg = msg + u"HELO " if self.Fname == '<>': msg = msg + u"Postmaster " if self.Ffqdn: if not self.HFdnok: msg = msg + u"送信者・差出人のドメイン違い " elif not self.HFadok: msg = msg + u"送信者・差出人のアドレス違い " if self.Hrelay or self.Relay: msg = msg + u"リレーサーバ " if self.HList: msg = msg + u"ML" for rad in self.Rname: ##self.send_admin2(rad, msg, "gray") # admin Only self.send_admin3(rad, msg, "gray") # admin or user return Milter.CONTINUE def eom(self): ## self.log("eom") self.log_debug("eom") # ヘッダーにセンダーアドレスを追加する 迷惑メール対応をメーラーで行う為 self.addheader('X-PWfrom', self.Fname) # ヘッダーにフィルターステイタスを追加する 迷惑メール対応をメーラーで行う為 if self.PWmsg != "": self.addheader('X-PWmail', self.PWmsg) if self.PWmode != "": self.addheader('X-PWmode', self.PWmode) if self.PWmsg != "": self.log("X-PWmail:", self.PWmsg) self.PWmsg = "" if self.PWmode != "": self.log("X-PWmode:", self.PWmode) self.PWmode = "" return Milter.CONTINUE def abort(self): self.log_debug("abort") if self.Sabort == None: self.Sabort = True return Milter.CONTINUE def close(self): # # End Setup # # abort 時の注意点を指示する。 if self.Sabort: self.log_warning("sever abort: mail server log read") if self.PWmsg != "": self.log("X-PWmail:", self.PWmsg) if self.PWmode != "": self.log("X-PWmode:", self.PWmode) self.log("close") return Milter.CONTINUE ## === Support Functions === def log_debug(self, *msg): my_logger.debug('[%d] %s', self.id, ' '.join([str(m) for m in msg])) def log(self, *msg): my_logger.info('[%d] %s', self.id, ' '.join([str(m) for m in msg])) def log_warning(self, *msg): my_logger.warning('[%d] %s', self.id, ' '.join([str(m) for m in msg])) def log_error(self, *msg): my_logger.error('[%d] %s', self.id, ' '.join([str(m) for m in msg])) def log_critical(self, *msg): my_logger.critical('[%d] %s', self.id, ' '.join([str(m) for m in msg])) ## === def main(): my_logger.info("pwfilter startup") global my_domainlist s = sys.argv[1] for v in s.split(','): p = (v, len(v)) my_domainlist = my_domainlist + (p,) global my_hnameip my_hnameip = sys.argv[2] global to_pwadmin to_pwadmin = sys.argv[3] my_logger.info("mydomain:" + str(my_domainlist)) dsnerror.load() dyndcheck.load() wlistcheck.load() # Register to have the Milter factory create instances of your class: Milter.factory = myMilter flags = Milter.ADDHDRS Milter.set_flags(flags) # tell Sendmail which features we use Milter.runmilter("pwfilter", socketname, sockettimeout) dyndcheck.save() dsnerror.save() my_logger.info("pwfilter shutdown") if __name__ == "__main__": main()