#! /usr/bin/python2.6
# -*- coding: utf-8 -*-

# <pwfilter.py  Receive e-mail filters And white list filter.>
# Copyright (C) <2012> <yasuyosi kimura>
#
# 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 <http://www.gnu.org/licenses/>.

### 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
from Milter.dns import Session as dnsSession

import re

import smtplib
from email.MIMEText 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 = ()               ## 受信ドメイン名リスト(起動時にパラメータで設定する)
my_hnameip = "192.168.200.100"   ## 受信サーバのグローバルIP(起動時にパラメータで設定する)
to_pwadmin = "pwadmin@xxxx1.jp"  ## ロギング用メールアドレス「全て小文字」(起動時にパラメータで設定する)

sendmail_port = 1026  ## ロギング用メールポート（システムに合わせて修正が必要です）
recipient_delimiter = '+'  ## postfix recipient_delimiter 受信者の拡張アドレスの区切り文字（システムに合わせて修正が必要です）

# 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

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


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.IP:
      IP = u""
    else:
      IP = msg_cnvt(self.IP)
    if not self.Hname:
      Hname = u""
    else:
      Hname = msg_cnvt(self.Hname)
    if not self.Fname:
      Fname = u""
    else:
      Fname = msg_cnvt(self.Fname)
    if not rcpt_addr:
      wrcpt_addr = u""
    else:
      wrcpt_addr = msg_cnvt(rcpt_addr)
    try:
      return u"connect from %s at %s\nhelo: %s\nmail from: %s\nrcpt to: %s\n" % (Cname, IP, Hname, Fname, wrcpt_addr) 
    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)
    try:
      return u"Header-From: %s\nHeader-Subject: %s\nHeader-Date: %s\n" % (hfrom, subject, hdate)
    except:
      self.log_warning("msg_Header:", traceback.format_exc())
      return u""



  # ログメール システム用（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()


  # FQDNシンタックスチェック
  def fqdncheck(self, s):
    if fqdn.match(s):
      if s.find('-.') < 0:
        return True
    return False

  # 受信ローカルドメインチェック
  def my_domain_check(self,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(self, 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

  # 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)


#  @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.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 self.fqdncheck(self.Cname):
      self.Cfqdn = True
      self.Cdynip = dynip(self.Cname, self.IP)
      self.Cipok = self.DNSCheck(IPname, self.IP)
      if (not self.Cipok[0]) and (self.Cname != IPname):
        self.Cipok = self.DNSCheck(self.Cname, self.IP)
    else:
      self.Cfqdn = False
      self.Cipok = (None, '', '')
      self.Cdynip = None

    self.Hname = None
    self.Hmyd = None
    self.Hipok = (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  構文エラー

    self.log("HELO",msg_cnvt(heloname))

    # Ini Setup
    self.PWmsg = ""
    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, '', '')

    if not self.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.PWmsg = self.PWmsg + "noHfqdn "              # error gray
      return Milter.CONTINUE

    self.Hfqdn = True

    self.Hmyd = self.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 self.Cfqdn:
      if self.Cname == self.Hname:
        self.Hipok = self.Cipok
        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.eq_domain_check(self.Cname, self.Hname): #type1
          wHipok = self.DNSCheck(self.Hname, self.IP)
          if wHipok[0]:
            self.Hipok = wHipok
          else:
            self.Hipok = (True, self.Cipok[1], wHipok[2])
          return Milter.CONTINUE

        # 同一ドメイン内のリレー及び記述ミスの回避
        Hdmain = self.Hname
        pos = Hdmain.find('.')
        Hdmain = Hdmain[pos+1:]
        if fqdnjp.search(Hdmain):
          if self.eq_domain_check(self.Cname, Hdmain): #type2
            wHipok = self.DNSCheck(Hdmain, self.IP)
            if wHipok[0]:
              self.Hipok = wHipok
            else:
              self.Hipok = (True, self.Cipok[1], wHipok[2])
            return Milter.CONTINUE

    self.Hipok = self.DNSCheck(self.Hname, self.IP)
    # 送信ホスト名のDNSの記述ミスの回避(ダイナミックIPも含む)
    if (not self.Hipok[0]) and (self.Hname != heloname):
      self.Hipok = self.DNSCheck(heloname, self.IP)

    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


#  @Milter.noreply
  def envfrom(self, mailfrom, *str):
    # 設定する変数
    # 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.Fname = mailfrom
    self.Fad = ''
    self.Fd = ''
    self.Ffqdn = None
    self.Fmyd = False
    self.Fdynip = None
    self.Fipok = (None, '', '')
    self.Relay = False
    
    self.Rname = []  # list of recipients

    if self.Fname == '<>':
      return Milter.CONTINUE

    mb = parseaddr(self.Fname)
    if (mb[1] == None) or (mb[1] == ''):
      self.Ffqdn = False
      self.PWmsg = self.PWmsg + "noFfqdn "              # error
      return Milter.CONTINUE

    self.Fad = mb[1]
    mf = parse_addr(mb[1])
    if len(mf) != 2:
      self.Ffqdn = False
      self.PWmsg = self.PWmsg + "noFfqdn "              # error
      return Milter.CONTINUE

    self.Fd = mf[1].lower()
    if not self.fqdncheck(self.Fd):
      self.Ffqdn = False
      self.PWmsg = self.PWmsg + "noFfqdn "              # error
      return Milter.CONTINUE

    self.Ffqdn = True

    self.Fmyd = self.my_domain_check(self.Fd)
    if self.Fmyd:
      self.PWmsg = self.PWmsg + "Fmyd "               # error
      return Milter.CONTINUE

    self.Fdynip = dynip(self.Fd, self.IP)
    if self.Fdynip:
      self.PWmsg = self.PWmsg + "Fdynip "             # error

    self.Fipok = self.DNSCheck(self.Fd, self.IP)
    if self.Fipok[0] == None:
      self.PWmsg = self.PWmsg + "ngFipok "            # error not(MX/A)
      return Milter.CONTINUE

    # 同ドメイン内のリレーチェック（許可）
    if self.Fipok[0] == False:
      self.PWmsg = self.PWmsg + "noFipok "            # OK
      if self.Hipok[0]:
        if self.eq_domain_check(self.Hname, self.Fd): #type1
          return Milter.CONTINUE
      if self.Cipok[0]:
        if self.eq_domain_check(self.Cname, self.Fd): #type1
          return Milter.CONTINUE

      Fdmain = self.Fd
      pos = Fdmain.find('.')
      Fdmain = Fdmain[pos+1:]
      if fqdnjp.search(Fdmain):
        if self.Hipok[0]:
          if self.eq_domain_check(self.Hname, Fdmain): #type2
            return Milter.CONTINUE
        if self.Cipok[0]:
          if self.eq_domain_check(self.Cname, Fdmain): #type2
            return Milter.CONTINUE

      self.Relay = True
      self.PWmsg = self.PWmsg + "relay "              # yellow

    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)

    ## 外部からはロギング用メールアドレスを拒否する。
    ## 拒否しない場合、コメントアウトする
    if recipient.lower() == to_pwadmin:
      self.setreply('550','5.1.1','<%s>: Recipient address rejected: User unknown.' % (recipient))
      self.log_critical('550','(%s:%s) %s %s: Recipient address rejected: User unknown.' % (self.Hname,self.IP,self.Fname,recipient))
      ## ロギング用メールアドレスに記録を残さない場合、下記１行をコメントアウトする
      self.send_admin1(recipient, u"ロギング用メールアドレス", "abort")
      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','(%s:%s) %s %s: Recipient address rejected: User unknown.' % (self.Hname,self.IP,self.Fname,recipient))
      ## ロギング用メールアドレスに記録を残さない場合、下記１行をコメントアウトする
      self.send_admin1(recipient, u"ユーザ名エラー", "abort")
      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")

    ## Abort 条件の追加
    ##
    ## 基本は、Helo コマンドの FQDN であるが Cipok、Fipok で送信サーバの確定を補足している
    if (not self.Hfqdn) and (not self.Cipok[0]) and (not self.Fipok[0]):
      self.setreply('550','5.5.2','<%s>: Helo command rejected: need fully-qualified hostname.' % (self.Hname))
      self.log_critical('550','(%s:%s) %s %s: Helo command rejected: need fully-qualified hostname.' % (self.Hname,self.IP,self.Fname,self.Rname))
      return Milter.REJECT

    ### 送信サーバが各ドメイン名等で確定出来ない
    if (not self.Cipok[0]) and (not self.Hipok[0]) and (not self.Fipok[0]):
      self.setreply('550','5.1.8','(%s:%s): SendHost rejected: DNS Host not found.' % (self.Hname,self.IP))
      self.log_critical('550','(%s:%s) %s %s: SendHost rejected: DNS Host not found.' % (self.Hname,self.IP,self.Fname,self.Rname))
      return Milter.REJECT

    ### Helo コマンドのホスト名が受信ドメインである
    if self.Hmyd:
      self.setreply('550','5.5.2','<%s>: Helo command rejected: Breach of Local Policy.' % (self.Hname))
      self.log_critical('550','(%s:%s) %s %s: Helo command rejected: Breach of Local Policy.' % (self.Hname,self.IP,self.Fname,self.Rname))
      ## ロギング用メールアドレスに記録を残さない場合、下記２行をコメントアウトする
      for rad in self.Rname:
        self.send_admin1(rad, u"送信ホスト名がマイドメイン", "abort")
      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','(%s:%s) %s %s: Sender address rejected: need fully-qualified address.' % (self.Hname,self.IP,self.Fname,self.Rname))
        ## ロギング用メールアドレスに記録を残さない場合、下記２行をコメントアウトする
        for rad in self.Rname:
          self.send_admin1(rad, u"送信者名エラー", "abort")
        return Milter.REJECT

      ### SenderHostが受信ドメインである
      if self.Fmyd:
        self.setreply('550','5.5.2','%s: Sender address rejected: Breach of Local Policy.' % (self.Fname))
        self.log_critical('550','(%s:%s) %s %s: Sender address rejected: Breach of Local Policy.' % (self.Hname,self.IP,self.Fname,self.Rname))
        ## ロギング用メールアドレスに記録を残さない場合、下記２行をコメントアウトする
        for rad in self.Rname:
          self.send_admin1(rad, u"送信者がマイドメイン", "abort")
        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','(%s:%s) %s %s: Sender address rejected: Breach of Domain.' % (self.Hname,self.IP,self.Fname,self.Rname))
        ## ロギング用メールアドレスに記録を残さない場合、下記２行をコメントアウトする
        for rad in self.Rname:
          self.send_admin1(rad, u"送信者ドメインエラー", "abort")
        return Milter.REJECT

    # 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):
##    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.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]
      self.HFromAD = ad
### アマゾン特別対応
      if (ad[0] == "'") and (ad[-1] == "'"):
        ad = ad[1:-1]
### アマゾン特別対応
      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 self.fqdncheck(dn):
            self.HFfqdn = False
            self.PWmsg = self.PWmsg + "noHFfqdn "
          else:
            self.HFfqdn = True
            self.HFmyd = self.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.DNSCheck(dn, self.IP)
              if self.HFipok[0] == None:
                self.PWmsg = self.PWmsg + "ngHFipok "
              elif self.HFipok[0] == False:
                self.PWmsg = self.PWmsg + "noHFipok "
              if self.Ffqdn:
                if self.Fd == dn:
                  self.HFdnok = True
                else:
                  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.')
      ## ロギング用メールアドレスに記録を残さない場合、下記２行をコメントアウトする
      for rad in self.Rname:
        self.send_admin2(rad, u"ヘッダー日付エラー", "abort")
      return Milter.REJECT

    ###--------------------Abort
    # 下位ロジックに纏める。
    #if not self.HFfqdn:
    #  self.setreply('550','5.7.1','%s: Header-From address rejected: need fully-qualified address.' % (self.HFromAD))
    #  self.log_critical('550','%s: Header-From address rejected: need fully-qualified address.' % (self.HFromAD))
    #  ## ロギング用メールアドレスに記録を残す場合、コメントアウトする
    #  ##for rad in self.Rname:
    #  ##  self.send_admin2(rad, u"差出人名エラー", "abort")
    #  return Milter.REJECT

    ### 差出人への返信不能
    if (self.HFadER) or (not self.HFfqdn) or (self.HFmyd) or (self.HFdynip):
      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','(%s): Breach of Header-From Local Policy.' % (','.join([m for d, m in self.FromAD])))
      ## ロギング用メールアドレスに記録を残さない場合、下記２行をコメントアウトする
      for rad in self.Rname:
        self.send_admin2(rad, u"差出人エラー", "abort")
      return Milter.REJECT

    ### 通常のメールサーバから送信されていない
    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','(%s): Breach of Message-ID Local Policy.' % (','.join([m for d, m in self.FromAD])))
      ## ロギング用メールアドレスに記録を残さない場合、下記３行をコメントアウトする
      for rad in self.Rname:
    ##    self.send_admin2(rad, u"Message-IDエラー", "black")
        self.send_admin3(rad, u"Message-IDエラー", "black")
      return Milter.REJECT

    ## ダイナミックＤＮＳの場合の処理　　　black
    ## 再検討する
    if (self.Cdynip) and ((self.Hipok[0] == None) or (not self.Hfqdn)):
      self.setreply('550','5.7.1','(%s:%s): Helo command rejected: Host not found.' % (self.Hname,self.IP))
      self.log_critical('550','(%s:%s) %s %s: Helo command rejected: Host not found.' % (self.Hname,self.IP,self.Fname,self.Rname))
      ## ロギング用メールアドレスに記録を残さない場合、下記３行をコメントアウトする
      for rad in self.Rname:
      ##  self.send_admin2(rad, u"ダイナミックＤＮＳエラー", "black") # admin Only
        self.send_admin3(rad, u"ダイナミックＤＮＳエラー", "black") # admin or user
      return Milter.REJECT

    ## 警告用ログを残す。
    ## ロギング用メールアドレスに記録を残さない場合、下記１１行をコメントアウトする
    if (self.Cdynip) or (self.Hdynip) or (self.Relay):
      msg = u"要注意："
      if (self.Cdynip) or (self.Hdynip):
        msg = msg + u"ダイナミックＤＮＳ　"
      if (self.Relay):
        msg = msg + u"リレーサーバ　"
      if (self.HList):
        msg = msg + u"ＭＬ"
      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)

    return Milter.CONTINUE

  def abort(self):
    self.log_debug("abort")
    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)

    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))

  # 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)
  my_logger.info("pwfilter shutdown")

if __name__ == "__main__":
  main()

