pymilter  0.9.8
milter-template.py
1 ## To roll your own milter, create a class that extends Milter.
2 # See the pymilter project at http://bmsi.com/python/milter.html
3 # based on Sendmail's milter API http://www.milter.org/milter_api/api.html
4 # This code is open-source on the same terms as Python.
5 
6 ## Milter calls methods of your class at milter events.
7 ## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
8 ## You can also add/del recipients, replacebody, add/del headers, etc.
9 
10 import Milter
11 import StringIO
12 import time
13 import email
14 import sys
15 from socket import AF_INET, AF_INET6
16 from Milter.utils import parse_addr
17 if True:
18  from multiprocessing import Process as Thread, Queue
19 else:
20  from threading import Thread
21  from Queue import Queue
22 
23 logq = Queue(maxsize=4)
24 
25 class myMilter(Milter.Base):
26 
27  def __init__(self): # A new instance with each new connection.
28  self.id = Milter.uniqueID() # Integer incremented with each call.
29 
30  # each connection runs in its own thread and has its own myMilter
31  # instance. Python code must be thread safe. This is trivial if only stuff
32  # in myMilter instances is referenced.
33  @Milter.noreply
34  def connect(self, IPname, family, hostaddr):
35  # (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
36  # (self, 'ip6.mxout.example.com', AF_INET6,
37  # ('3ffe:80e8:d8::1', 4720, 1, 0) )
38  self.IP = hostaddr[0]
39  self.port = hostaddr[1]
40  if family == AF_INET6:
41  self.flow = hostaddr[2]
42  self.scope = hostaddr[3]
43  else:
44  self.flow = None
45  self.scope = None
46  self.IPname = IPname # Name from a reverse IP lookup
47  self.H = None
48  self.fp = None
49  self.receiver = self.getsymval('j')
50  self.log("connect from %s at %s" % (IPname, hostaddr) )
51 
52  return Milter.CONTINUE
53 
54 
55  ## def hello(self,hostname):
56  def hello(self, heloname):
57  # (self, 'mailout17.dallas.texas.example.com')
58  self.H = heloname
59  self.log("HELO %s" % heloname)
60  if heloname.find('.') < 0: # illegal helo name
61  # NOTE: example only - too many real braindead clients to reject on this
62  self.setreply('550','5.7.1','Sheesh people! Use a proper helo name!')
63  return Milter.REJECT
64 
65  return Milter.CONTINUE
66 
67  ## def envfrom(self,f,*str):
68  def envfrom(self, mailfrom, *str):
69  self.F = mailfrom
70  self.R = [] # list of recipients
71  self.fromparms = Milter.dictfromlist(str) # ESMTP parms
72  self.user = self.getsymval('{auth_authen}') # authenticated user
73  self.log("mail from:", mailfrom, *str)
74  # NOTE: self.fp is only an *internal* copy of message data. You
75  # must use addheader, chgheader, replacebody to change the message
76  # on the MTA.
77  self.fp = StringIO.StringIO()
78  self.canon_from = '@'.join(parse_addr(mailfrom))
79  self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
80  return Milter.CONTINUE
81 
82 
83  ## def envrcpt(self, to, *str):
84  @Milter.noreply
85  def envrcpt(self, to, *str):
86  rcptinfo = to,Milter.dictfromlist(str)
87  self.R.append(rcptinfo)
88 
89  return Milter.CONTINUE
90 
91 
92  @Milter.noreply
93  def header(self, name, hval):
94  self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer
95  return Milter.CONTINUE
96 
97  @Milter.noreply
98  def eoh(self):
99  self.fp.write("\n") # terminate headers
100  return Milter.CONTINUE
101 
102  @Milter.noreply
103  def body(self, chunk):
104  self.fp.write(chunk)
105  return Milter.CONTINUE
106 
107  def eom(self):
108  self.fp.seek(0)
109  msg = email.message_from_file(self.fp)
110  # many milter functions can only be called from eom()
111  # example of adding a Bcc:
112  self.addrcpt('<%s>' % 'spy@example.com')
113  return Milter.ACCEPT
114 
115  def close(self):
116  # always called, even when abort is called. Clean up
117  # any external resources here.
118  return Milter.CONTINUE
119 
120  def abort(self):
121  # client disconnected prematurely
122  return Milter.CONTINUE
123 
124  ## === Support Functions ===
125 
126  def log(self,*msg):
127  logq.put((msg,self.id,time.time()))
128 
129 def background():
130  while True:
131  t = logq.get()
132  if not t: break
133  msg,id,ts = t
134  print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
135  # 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
136  for i in msg: print i,
137  print
138 
139 ## ===
140 
141 def main():
142  bt = Thread(target=background)
143  bt.start()
144  socketname = "/home/stuart/pythonsock"
145  timeout = 600
146  # Register to have the Milter factory create instances of your class:
147  Milter.factory = myMilter
148  flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
149  flags += Milter.ADDRCPT
150  flags += Milter.DELRCPT
151  Milter.set_flags(flags) # tell Sendmail which features we use
152  print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
153  sys.stdout.flush()
154  Milter.runmilter("pythonfilter",socketname,timeout)
155  logq.put(None)
156  bt.join()
157  print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
158 
159 if __name__ == "__main__":
160  main()