Compare commits

..

37 Commits

Author SHA1 Message Date
Stuart D. Gathman 255624ea80 print function 2016-07-26 09:36:23 -04:00
Stuart D. Gathman f5bd952f64 Pull Scott's changes 2016-07-26 07:58:40 -04:00
Stuart D. Gathman 687bebcd45 Merge fix test case from master 2016-07-26 07:56:52 -04:00
Scott Kitterman 5252278804 Fixup test bits missing from the python3 branch. 2016-07-26 01:21:47 -04:00
Stuart D. Gathman e5cff32526 Target python2.7 for master
Conflicts:
	setup.cfg
2016-07-26 01:09:35 -04:00
Stuart D. Gathman 702c6c126f Use unicode literal to join unicode strings.
Conflicts:
	Milter/utils.py
2016-07-26 01:07:13 -04:00
Stuart D. Gathman bf327f95e0 Add section to link projects using pymilter. 2016-07-26 01:04:19 -04:00
Stuart D. Gathman 883b19f131 Fix test case 2016-07-26 01:03:59 -04:00
Stuart D. Gathman 0863bb5602 Fix spurious cleanup error. 2016-07-26 01:03:26 -04:00
Scott Kitterman 67d974638a Import print_function from future so we still work with python2.7 2016-07-25 17:42:19 -04:00
Scott Kitterman 9058f1c2aa sample.py: print is a function in python3 2016-07-25 17:29:29 -04:00
Scott Kitterman 932216e1bf Convert tabs to spaces for sample.py 2016-07-25 17:27:34 -04:00
Stuart D. Gathman 91a70384ed Update README 2016-07-25 10:05:19 -04:00
Stuart Gathman bbd6771a74 Handle missing padding in encoded header 2016-07-25 10:04:55 -04:00
Stuart Gathman 8b36939747 Test case for missing padding. 2016-07-25 10:04:35 -04:00
Stuart Gathman 0c1726614d Link to related packages.
Conflicts:
	doc/mainpage.py
2016-07-25 10:04:06 -04:00
Stuart Gathman 74b8b1ae19 Copy sendmail-devel libmilter api into documention, since milter.org is gone.
Conflicts:
	Milter/__init__.py
	doc/milter.py
	makefile
2016-07-25 10:02:46 -04:00
Stuart Gathman d35ed40edf Fix header_leading_space, update doc version.
Conflicts:
	Doxyfile
	testgrey.py
2016-07-25 09:59:03 -04:00
Stuart Gathman 753c417f31 Fix bug from pyspf - caching server altering case of cached names. 2016-07-25 09:56:11 -04:00
Stuart Gathman 73bd1895cd Add dns name support for iniplist() 2016-07-25 09:55:47 -04:00
Stuart D. Gathman e01b7dabf2 Use python3 for build 2016-07-23 12:19:45 -04:00
Stuart Gathman 1b4903f905 Python3 changes 2013-01-13 04:26:30 +00:00
cvs2svn e1d29fdf6a This commit was manufactured by cvs2svn to create branch 'python3-branch'.
Sprout from master 2012-11-10 03:38:47 UTC Stuart Gathman <stuart@gathman.org> 'Update doc version to 0.9.6'
Cherrypick from bmsi 2005-05-31 18:10:47 UTC Stuart Gathman <stuart@gathman.org> 'Release 0.7.2':
    test/big5
    test/bounce
    test/bounce1
    test/bound
    test/honey
    test/missingboundary
    test/samp1
    test/spam44
    test/spam7
    test/spam8
    test/test1
    test/test8
    test/virus1
    test/virus13
    test/virus2
    test/virus3
    test/virus4
    test/virus5
    test/virus6
    test/virus7
2012-11-10 03:38:48 +00:00
Stuart Gathman 23defb880b Update doc version to 0.9.6 2012-11-10 03:38:47 +00:00
Stuart Gathman 7502c29e47 Use functools.wraps for noreply decorator 2012-08-28 19:42:05 +00:00
Stuart Gathman 594d3ad365 Doc updates. 2012-08-28 06:02:36 +00:00
Stuart Gathman b2e0b2ebc6 Fix CNAME following bug. 2012-08-28 06:02:14 +00:00
Stuart Gathman 04a241f1e9 Ignore leading/trailing whitespace parsing IP6 addresses. 2012-07-13 21:50:52 +00:00
Stuart Gathman 16bfe5d4da Exceptions on unsupported result code for callback decorators. 2012-04-13 20:33:35 +00:00
Stuart Gathman 70d19001c0 Replace redundant callback array with macros. If this doesn't break anything,
macros can be eliminated with code changes.
2012-04-12 23:32:50 +00:00
Stuart Gathman 0d001dd8e9 Support RFC2553 on BSD 2012-04-12 23:08:06 +00:00
Stuart Gathman 8f4a82794c Release 0.9.6 2012-03-03 18:51:56 +00:00
Stuart Gathman de0ec3430d Start new release. 2012-02-26 01:35:58 +00:00
Stuart Gathman c9e32e4b06 throw ValueError when message line contains a single % 2012-02-25 15:53:45 +00:00
Stuart Gathman 83a1762515 New example 2011-11-05 15:51:03 +00:00
Stuart Gathman feb6526cb8 Grace period 2011-11-05 15:50:02 +00:00
Stuart Gathman 3a3add814e Very simple actual milter. 2011-11-05 14:28:56 +00:00
20 changed files with 19041 additions and 179 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ PROJECT_NAME = pymilter
# This could be handy for archiving the generated documentation or # This could be handy for archiving the generated documentation or
# if some version control system is used. # if some version control system is used.
PROJECT_NUMBER = 0.9.5 PROJECT_NUMBER = 1.0
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
# base path where the generated documentation will be put. # base path where the generated documentation will be put.
+84 -23
View File
@@ -8,13 +8,14 @@
# Copyright 2001,2009 Business Management Systems, Inc. # Copyright 2001,2009 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details. # This code is under the GNU General Public License. See COPYING for details.
__version__ = '0.9.5' __version__ = '0.9.7'
import os import os
import milter import milter
import thread import thread
from milter import * from milter import *
from functools import wraps
_seq_lock = thread.allocate_lock() _seq_lock = thread.allocate_lock()
_seq = 0 _seq = 0
@@ -102,7 +103,7 @@ def rejected_recipients(klass):
return enable_protocols(klass,P_RCPT_REJ) return enable_protocols(klass,P_RCPT_REJ)
## Milter leading space on headers. A class decorator that calls ## Milter leading space on headers. A class decorator that calls
# enable_protocols() with the P_HEAD_LEADSPC flag. By default, # enable_protocols() with the P_HDR_LEADSPC flag. By default,
# header continuation lines are collected and joined before getting # header continuation lines are collected and joined before getting
# sent to a milter. Headers modified or added by the milter are # sent to a milter. Headers modified or added by the milter are
# folded by the MTA as necessary according to its own standards. # folded by the MTA as necessary according to its own standards.
@@ -120,7 +121,7 @@ def rejected_recipients(klass):
# @param klass the %milter application class to modify # @param klass the %milter application class to modify
# @return the modified %milter class # @return the modified %milter class
def header_leading_space(klass): def header_leading_space(klass):
return enable_protocols(klass,P_HEAD_LEADSPC) return enable_protocols(klass,P_HDR_LEADSPC)
## Function decorator to disable callback methods. ## Function decorator to disable callback methods.
# If the MTA supports it, tells the MTA not to invoke this callback, # If the MTA supports it, tells the MTA not to invoke this callback,
@@ -137,7 +138,12 @@ def nocallback(func):
except KeyError: except KeyError:
raise ValueError( raise ValueError(
'@nocallback applied to non-optional method: '+func.__name__) '@nocallback applied to non-optional method: '+func.__name__)
return func def wrapper(self,*args):
if func(self,*args) != CONTINUE:
raise RuntimeError('%s return code must be CONTINUE with @nocallback'
% func.__name__)
return CONTINUE
return wrapper
## Function decorator to disable callback reply. ## Function decorator to disable callback reply.
# If the MTA supports it, tells the MTA not to wait for a reply from # If the MTA supports it, tells the MTA not to wait for a reply from
@@ -152,9 +158,14 @@ def noreply(func):
except KeyError: except KeyError:
raise ValueError( raise ValueError(
'@noreply applied to non-optional method: '+func.__name__) '@noreply applied to non-optional method: '+func.__name__)
@wraps(func)
def wrapper(self,*args): def wrapper(self,*args):
rc = func(self,*args) rc = func(self,*args)
if self._protocol & nr_mask: return NOREPLY if self._protocol & nr_mask:
if rc != CONTINUE:
raise RuntimeError('%s return code must be CONTINUE with @noreply'
% func.__name__)
return NOREPLY
return rc return rc
wrapper.milter_protocol = nr_mask wrapper.milter_protocol = nr_mask
return wrapper return wrapper
@@ -247,7 +258,9 @@ class Base(object):
## Defined by subclasses to write log messages. ## Defined by subclasses to write log messages.
def log(self,*msg): pass def log(self,*msg): pass
## Called for each connection to the MTA. ## Called for each connection to the MTA. Called by the
# <a href="milter_api/xxfi_connect.html">
# xxfi_connect</a> callback.
# The <code>hostname</code> provided by the local MTA is either # The <code>hostname</code> provided by the local MTA is either
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available. # the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
# The format of hostaddr depends on the socket family: # The format of hostaddr depends on the socket family:
@@ -260,6 +273,17 @@ class Base(object):
# <dt><code>socket.AF_UNIX</code> # <dt><code>socket.AF_UNIX</code>
# <dd>A string with the socketname # <dd>A string with the socketname
# </dl> # </dl>
# To vary behavior based on what port the client connected to,
# for example skipping blacklist checks for port 587 (which must
# be authenticated), use @link #getsymval getsymval('{daemon_port}') @endlink.
# The <code>{daemon_port}</code> macro must be enabled in sendmail.cf
# <pre>
# O Milter.macros.connect=j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}
# </pre>
# or sendmail.mc
# <pre>
# define(`confMILTER_MACROS_CONNECT', ``j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}'')dnl
# </pre>
# @param hostname the PTR name or bracketed IP of the SMTP client # @param hostname the PTR name or bracketed IP of the SMTP client
# @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>, # @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
# or <code>socket.AF_UNIX</code> # or <code>socket.AF_UNIX</code>
@@ -271,12 +295,26 @@ class Base(object):
# this almost always results in terminating the connection. # this almost always results in terminating the connection.
@nocallback @nocallback
def hello(self,hostname): return CONTINUE def hello(self,hostname): return CONTINUE
## Called when the SMTP client says MAIL FROM. ## Called when the SMTP client says MAIL FROM. Called by the
# <a href="milter_api/xxfi_envfrom.html">
# xxfi_envfrom</a> callback.
# Returning REJECT rejects the message, but not the connection. # Returning REJECT rejects the message, but not the connection.
# The sender is the "envelope" from as defined by
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
# For the From: header (author) defined in
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
# see @link #header the header callback @endlink.
@nocallback @nocallback
def envfrom(self,f,*str): return CONTINUE def envfrom(self,f,*str): return CONTINUE
## Called when the SMTP client says RCPT TO. ## Called when the SMTP client says RCPT TO. Called by the
# <a href="milter_api/xxfi_envrcpt.html">
# xxfi_envrcpt</a> callback.
# Returning REJECT rejects the current recipient, not the entire message. # Returning REJECT rejects the current recipient, not the entire message.
# The recipient is the "envelope" recipient as defined by
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
# For recipients defined in
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
# for example To: or Cc:, see @link #header the header callback @endlink.
@nocallback @nocallback
def envrcpt(self,to,*str): return CONTINUE def envrcpt(self,to,*str): return CONTINUE
## Called when the SMTP client says DATA. ## Called when the SMTP client says DATA.
@@ -310,7 +348,7 @@ class Base(object):
## Called when the connection is closed. ## Called when the connection is closed.
def close(self): return CONTINUE def close(self): return CONTINUE
## Return mask of SMFIP_N.. protocol option bits to clear for this class ## Return mask of SMFIP_N* protocol option bits to clear for this class
# The @@nocallback and @@noreply decorators set the # The @@nocallback and @@noreply decorators set the
# <code>milter_protocol</code> function attribute to the protocol mask bit to # <code>milter_protocol</code> function attribute to the protocol mask bit to
# pass to libmilter, causing that callback or its reply to be skipped. # pass to libmilter, causing that callback or its reply to be skipped.
@@ -335,8 +373,11 @@ class Base(object):
return p return p
## Negotiate milter protocol options. Called by the ## Negotiate milter protocol options. Called by the
# <a href="https://www.milter.org/developers/api/xxfi_negotiate"> # <a href="milter_api/xxfi_negotiate.html">
# xffi_negotiate</a> callback. # xffi_negotiate</a> callback. This is an advanced callback,
# do not override unless you know what you are doing. Most
# negotiation can be done simply by using the supplied
# class and function decorators.
# Options are passed as # Options are passed as
# a list of 4 32-bit ints which can be modified and are passed # a list of 4 32-bit ints which can be modified and are passed
# back to libmilter on return. # back to libmilter on return.
@@ -363,14 +404,33 @@ class Base(object):
## Return the value of an MTA macro. Sendmail macro names ## Return the value of an MTA macro. Sendmail macro names
# are either single chars (e.g. "j") or multiple chars enclosed # are either single chars (e.g. "j") or multiple chars enclosed
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent. # in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
# See <a href="milter_api/smfi_getsymval.html">
# smfi_getsymval</a> for default sendmail macros.
# @param sym the macro name # @param sym the macro name
def getsymval(self,sym): def getsymval(self,sym):
return self._ctx.getsymval(sym) return self._ctx.getsymval(sym)
## Set the SMTP reply code and message. ## Set the SMTP reply code and message.
# If the MTA does not support setmlreply, then only the # If the MTA does not support setmlreply, then only the
# first msg line is used. # first msg line is used. Any '%' in a message line
# must be doubled, or libmilter will silently ignore the setreply.
# Beginning with 0.9.6, we test for that case and throw ValueError to avoid
# head scratching. What will <i>really</i> irritate you, however,
# is that if you carefully double any '%%', your message will be
# sent - but with the '%%' still doubled!
# See <a href="milter_api/smfi_setreply.html">
# smfi_setreply</a> for more information.
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
# @param msg The text part of the SMTP reply. If msg is None,
# an empty message is used.
# @param ml Optional additional message lines.
def setreply(self,rcode,xcode=None,msg=None,*ml): def setreply(self,rcode,xcode=None,msg=None,*ml):
for m in (msg,)+ml:
if 1 in [len(s)&1 for s in R.findall(m)]:
raise ValueError("'%' must be doubled: "+m)
return self._ctx.setreply(rcode,xcode,msg,*ml) return self._ctx.setreply(rcode,xcode,msg,*ml)
## Tell the MTA which macro names will be used. ## Tell the MTA which macro names will be used.
@@ -389,7 +449,7 @@ class Base(object):
# Milter methods which can only be called from eom callback. # Milter methods which can only be called from eom callback.
## Add a mail header field. ## Add a mail header field.
# Calls <a href="https://www.milter.org/developers/api/smfi_addheader"> # Calls <a href="milter_api/smfi_addheader.html">
# smfi_addheader</a>. # smfi_addheader</a>.
# The <code>Milter.ADDHDRS</code> action flag must be set. # The <code>Milter.ADDHDRS</code> action flag must be set.
# #
@@ -403,7 +463,7 @@ class Base(object):
return self._ctx.addheader(field,value,idx) return self._ctx.addheader(field,value,idx)
## Change the value of a mail header field. ## Change the value of a mail header field.
# Calls <a href="https://www.milter.org/developers/api/smfi_chgheader"> # Calls <a href="milter_api/smfi_chgheader.html">
# smfi_chgheader</a>. # smfi_chgheader</a>.
# The <code>Milter.CHGHDRS</code> action flag must be set. # The <code>Milter.CHGHDRS</code> action flag must be set.
# #
@@ -417,7 +477,7 @@ class Base(object):
return self._ctx.chgheader(field,idx,value) return self._ctx.chgheader(field,idx,value)
## Add a recipient to the message. ## Add a recipient to the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt"> # Calls <a href="milter_api/smfi_addrcpt.html">
# smfi_addrcpt</a>. # smfi_addrcpt</a>.
# If no corresponding mail header is added, this is like a Bcc. # If no corresponding mail header is added, this is like a Bcc.
# The syntax of the recipient is the same as used in the SMTP # The syntax of the recipient is the same as used in the SMTP
@@ -437,7 +497,7 @@ class Base(object):
raise DisabledAction("ADDRCPT_PAR") raise DisabledAction("ADDRCPT_PAR")
return self._ctx.addrcpt(rcpt,params) return self._ctx.addrcpt(rcpt,params)
## Delete a recipient from the message. ## Delete a recipient from the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt"> # Calls <a href="milter_api/smfi_delrcpt.html">
# smfi_delrcpt</a>. # smfi_delrcpt</a>.
# The recipient should match one passed to the envrcpt callback. # The recipient should match one passed to the envrcpt callback.
# The <code>Milter.DELRCPT</code> action flag must be set. # The <code>Milter.DELRCPT</code> action flag must be set.
@@ -450,7 +510,7 @@ class Base(object):
return self._ctx.delrcpt(rcpt) return self._ctx.delrcpt(rcpt)
## Replace the message body. ## Replace the message body.
# Calls <a href="https://www.milter.org/developers/api/smfi_replacebody"> # Calls <a href="milter_api/smfi_replacebody.html">
# smfi_replacebody</a>. # smfi_replacebody</a>.
# The entire message body must be replaced. # The entire message body must be replaced.
# Call repeatedly with blocks of data until the entire body is transferred. # Call repeatedly with blocks of data until the entire body is transferred.
@@ -464,7 +524,7 @@ class Base(object):
return self._ctx.replacebody(body) return self._ctx.replacebody(body)
## Change the SMTP envelope sender address. ## Change the SMTP envelope sender address.
# Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom"> # Calls <a href="milter_api/smfi_chgfrom.html">
# smfi_chgfrom</a>. # smfi_chgfrom</a>.
# The syntax of the sender is that same as used in the SMTP # The syntax of the sender is that same as used in the SMTP
# MAIL FROM command (and as delivered to the envfrom callback), # MAIL FROM command (and as delivered to the envfrom callback),
@@ -481,7 +541,7 @@ class Base(object):
return self._ctx.chgfrom(sender,params) return self._ctx.chgfrom(sender,params)
## Quarantine the message. ## Quarantine the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_quarantine"> # Calls <a href="milter_api/smfi_quarantine.html">
# smfi_quarantine</a>. # smfi_quarantine</a>.
# When quarantined, a message goes into the mailq as if to be delivered, # When quarantined, a message goes into the mailq as if to be delivered,
# but delivery is deferred until the message is unquarantined. # but delivery is deferred until the message is unquarantined.
@@ -495,7 +555,7 @@ class Base(object):
return self._ctx.quarantine(reason) return self._ctx.quarantine(reason)
## Tell the MTA to wait a bit longer. ## Tell the MTA to wait a bit longer.
# Calls <a href="https://www.milter.org/developers/api/smfi_progress"> # Calls <a href="milter_api/smfi_progress.html">
# smfi_progress</a>. # smfi_progress</a>.
# Resets timeouts in the MTA that detect a "hung" milter. # Resets timeouts in the MTA that detect a "hung" milter.
def progress(self): def progress(self):
@@ -509,9 +569,9 @@ class Milter(Base):
## Provide simple logging to sys.stdout ## Provide simple logging to sys.stdout
def log(self,*msg): def log(self,*msg):
print 'Milter:', print('Milter:',end=None)
for i in msg: print i, for i in msg: print(i,end=None)
print print()
@noreply @noreply
def connect(self,hostname,family,hostaddr): def connect(self,hostname,family,hostaddr):
@@ -714,4 +774,5 @@ for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'):
__all__ = __all__.keys() __all__ = __all__.keys()
## @example milter-template.py ## @example milter-template.py
## @example milter-nomix.py
# #
+26 -5
View File
@@ -70,15 +70,24 @@ class Session(object):
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF'] pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
post: isinstance(__return__, types.ListType) post: isinstance(__return__, types.ListType)
""" """
if name.endswith('.'): name = name[:-1]
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
return [] # invalid DNS name (too long or empty)
name = name.lower()
result = self.cache.get( (name, qtype) ) result = self.cache.get( (name, qtype) )
cname = None cname = None
if result: return result
cnamek = (name,'CNAME')
cname = self.cache.get( cnamek )
if not result: if cname:
cname = cname[0]
else:
safe2cache = Session.SAFE2CACHE safe2cache = Session.SAFE2CACHE
for k, v in DNSLookup(name, qtype): for k, v in DNSLookup(name, qtype):
if k == (name, 'CNAME'): if k == cnamek:
cname = v cname = v
if (qtype,k[1]) in safe2cache: if k[1] == 'CNAME' or (qtype,k[1]) in safe2cache:
self.cache.setdefault(k, []).append(v) self.cache.setdefault(k, []).append(v)
result = self.cache.get( (name, qtype), []) result = self.cache.get( (name, qtype), [])
if not result and cname: if not result and cname:
@@ -88,11 +97,23 @@ class Session(object):
#return result # if too many == NX_DOMAIN #return result # if too many == NX_DOMAIN
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME) raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
cnames[name] = cname cnames[name] = cname
if cname in cnames: if cname.lower().rstrip('.') in cnames:
raise DNSError, 'CNAME loop' raise DNSError('CNAME loop')
result = self.dns(cname, qtype, cnames=cnames) result = self.dns(cname, qtype, cnames=cnames)
if result:
self.cache[(name,qtype)] = result
return result return result
def dns_txt(self, domainname, enc='ascii'):
"Get a list of TXT records for a domain name."
if domainname:
try:
return [''.join(s.decode(enc) for s in a)
for a in self.dns(domainname, 'TXT')]
except UnicodeEncodeError:
raise DNSError('Non-ascii character in SPF TXT record.')
return []
DNS.DiscoverNameServers() DNS.DiscoverNameServers()
if __name__ == '__main__': if __name__ == '__main__':
+1 -1
View File
@@ -50,7 +50,7 @@ class Greylist(object):
# expired # expired
log.debug('Expired greylist: %s',key) log.debug('Expired greylist: %s',key)
r = Record() r = Record()
elif now < r.firstseen + self.greylist_time: elif now < r.firstseen + self.greylist_time + 5:
# still greylisted # still greylisted
log.debug('Early greylist: %s',key) log.debug('Early greylist: %s',key)
#r = Record() #r = Record()
+32 -3
View File
@@ -6,11 +6,14 @@ import re
import struct import struct
import socket import socket
import email.Errors import email.Errors
import email.base64mime
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from email.Header import decode_header from email.Header import decode_header
from binascii import a2b_base64
#import email.Utils #import email.Utils
import rfc822 import rfc822
dnsre = re.compile(r'^[a-z][-a-z\d.]+$', re.IGNORECASE)
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4) PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
ip4re = re.compile(PAT_IP4+'$') ip4re = re.compile(PAT_IP4+'$')
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$' ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
@@ -49,7 +52,7 @@ if hasattr(socket,'has_ipv6') and socket.has_ipv6:
def inet_ntop(s): def inet_ntop(s):
return socket.inet_ntop(socket.AF_INET6,s) return socket.inet_ntop(socket.AF_INET6,s)
def inet_pton(s): def inet_pton(s):
return socket.inet_pton(socket.AF_INET6,s) return socket.inet_pton(socket.AF_INET6,s.strip())
else: else:
from pyip6 import inet_ntop, inet_pton from pyip6 import inet_ntop, inet_pton
@@ -67,6 +70,12 @@ def iniplist(ipaddr,iplist):
True True
>>> iniplist('192.168.0.45',['192.168.0.*']) >>> iniplist('192.168.0.45',['192.168.0.*'])
True True
>>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
True
>>> iniplist('2607:f8b0:4004:801::',['google.com/40'])
True
>>> iniplist('4.2.2.2',['nothing.example.com'])
False
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48']) >>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
True True
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48']) >>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
@@ -75,8 +84,10 @@ def iniplist(ipaddr,iplist):
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3 ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
""" """
if ip4re.match(ipaddr): if ip4re.match(ipaddr):
fam = socket.AF_INET
ipnum = addr2bin(ipaddr) ipnum = addr2bin(ipaddr)
elif ip6re.match(ipaddr): elif ip6re.match(ipaddr):
fam = socket.AF_INET6
ipnum = bin2long6(inet_pton(ipaddr)) ipnum = bin2long6(inet_pton(ipaddr))
else: else:
raise ValueError('Invalid ip syntax:'+ipaddr) raise ValueError('Invalid ip syntax:'+ipaddr)
@@ -96,6 +107,13 @@ def iniplist(ipaddr,iplist):
n = 128 n = 128
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6): if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
return True return True
elif dnsre.match(p[0]):
try:
sfx = '/'.join(['']+p[1:])
addrlist = [r[4][0]+sfx for r in socket.getaddrinfo(p[0],25,fam)]
if iniplist(ipaddr,addrlist):
return True
except socket.gaierror: pass
elif fnmatchcase(ipaddr,pat): elif fnmatchcase(ipaddr,pat):
return True return True
return False return False
@@ -103,6 +121,7 @@ def iniplist(ipaddr,iplist):
## Split email into Fullname and address. ## Split email into Fullname and address.
# This replaces <code>email.Utils.parseaddr</code> but fixes # This replaces <code>email.Utils.parseaddr</code> but fixes
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>. # some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
# Additional tricky cases are still broken. Patches welcome.
# #
def parseaddr(t): def parseaddr(t):
"""Split email into Fullname and address. """Split email into Fullname and address.
@@ -141,6 +160,16 @@ def parseaddr(t):
return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec)) return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
return res return res
## Fix email.base64mime.decode to add any missing padding
def decode(s, convert_eols=None):
if not s: return s
while len(s) % 4: s += '=' # add missing padding
dec = a2b_base64(s)
if convert_eols:
return dec.replace(CRLF, convert_eols)
return dec
email.base64mime.decode = decode
def parse_addr(t): def parse_addr(t):
"""Split email into user,domain. """Split email into user,domain.
@@ -190,8 +219,8 @@ def parse_header(val):
u.append(unicode(s)) u.append(unicode(s))
else: else:
u.append(unicode(s)) u.append(unicode(s))
u = ''.join(u) u = u''.join(u)
for enc in ('us-ascii','iso-8859-1','utf8'): for enc in ('us-ascii','iso-8859-1','utf-8'):
try: try:
return u.encode(enc) return u.encode(enc)
except UnicodeError: continue except UnicodeError: continue
+4 -27
View File
@@ -11,25 +11,24 @@ any point, tell Sendmail to reject, discard, or accept the message.
Requirements Requirements
------------ ------------
This python milter extension: http://www.bmsi.com/python/milter.html Python milter extension: http://https://pypi.python.org/pypi/pymilter/
Python: http://www.python.org Python: http://www.python.org
Sendmail: http://www.sendmail.org Sendmail: http://www.sendmail.org
NB: From Sendmail's libmilter/README: NB: From Sendmail's libmilter/README:
libmilter requires pthread support in the operating system. Moreover, it libmilter requires pthread support in the operating system. Moreover, it
requires that the library functions it uses are thread safe; which is true requires that the library functions it uses are thread safe; which is true
for the operating systems libmilter has been developed and tested on. On for the operating systems libmilter has been developed and tested on. On
some operating systems this requires special compile time options (e.g., some operating systems this requires special compile time options (e.g.,
not just -pthread). libmilter is currently known to work on (modulo not just -pthread). libmilter is currently known to work on (modulo problems
problems in the pthread support of some specific versions): in the pthread support of some specific versions):
FreeBSD 3.x, 4.x FreeBSD 3.x, 4.x
SunOS 5.x (x >= 5) SunOS 5.x (x >= 5)
AIX 4.3.x AIX 4.3.x
HP UX 11.x HP UX 11.x
Linux (recent versions/distributions) Linux (recent versions/distributions)
OpenBSD
AIX 4.1.5
libmilter is currently not supported on: libmilter is currently not supported on:
@@ -110,28 +109,6 @@ _FFR_MILTER for the cf macros. For example,
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
RedHat 6.2 Notes
----------------
The Redhat 6.2 sendmail RPM does not enable milter. You can obtain a
modified spec file at
http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec
use it to rebuild the Redhat 7.2 SRPM. The RH6.2 SRPM does not have
recent sendmail security patches.
RedHat 7.2 Notes
----------------
The Redhat 7.2 sendmail RPM enables milter in sendmail - but does not include
the headers needed for compiling a milter. You can obtain a modified spec
file with a sendmail-devel package that includes the needed static libraries
and headers at
http://www.bmsi.com/linux/sendmail-rh72.spec
IPv6 Notes IPv6 Notes
---------- ----------
+37 -4
View File
@@ -1,7 +1,7 @@
## @mainpage Writing Milters in Python ## @mainpage Writing Milters in Python
# #
# At the lowest level, the <code>milter</code> module provides a thin wrapper # At the lowest level, the <code>milter</code> module provides a thin wrapper
# around the <a href="https://www.milter.org/developers/api/index"> sendmail # around the <a href="milter_api/index.html"> sendmail
# libmilter API</a>. This API lets you register callbacks for a number of # libmilter API</a>. This API lets you register callbacks for a number of
# events in the process of sendmail receiving a message via SMTP. These # events in the process of sendmail receiving a message via SMTP. These
# events include the initial connection from a MTA, the envelope sender and # events include the initial connection from a MTA, the envelope sender and
@@ -37,10 +37,10 @@
# @section threading # @section threading
# #
# The libmilter library which pymilter wraps # The libmilter library which pymilter wraps
# <a href="https://www.milter.org/developers/overview#SignalHandling">handles # <a href="milter_overview#SignalHandling">handles
# all signals</a> itself, and expects to be called from a single main thread. # all signals</a> itself, and expects to be called from a single main thread.
# It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to # It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to
# <a href="https://www.milter.org/developers/api/smfi_stop">smfi_stop</a> # <a href="milter_api/smfi_stop.html">smfi_stop</a>
# and the last to an internal ABORT. # and the last to an internal ABORT.
# #
# If you use python threads or threading modules, then signal handling gets # If you use python threads or threading modules, then signal handling gets
@@ -50,4 +50,37 @@
# You may find the # You may find the
# <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html"> # <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html">
# multiprocessing</a> module useful. It can be a drop-in # multiprocessing</a> module useful. It can be a drop-in
# replacement for threading as illustrated in @ref milter-template.py. # replacement for threading as illustrated in
# <a href="milter-template_8py-example.html">milter-template.py</a>.
#
# @section Useful python packages for milters
#
# <a href="https://pypi.python.org/pypi/pyspf">pyspf</a> checks the
# SMTP envelope sender (MAIL FROM, passed to the Milter.Base.envfrom callback)
# against a Sender Policy published in DNS by the sending domain. This
# can prevent forgery of the MAIL FROM. SPF is Sender Policy Framework.
#
# <a href="https://launchpad.net/dkimpy">pydkim</a> checks a DKIM signature
# of the email body and headers against a public key published in DNS by
# the signing domain. DKIM is DomainKeys Identified Mail.
#
# The <a href="https://pypi.python.org/pypi/authres/">authres</a> module
# parses and formats the Authentication-Results email header, providing
# a standard place to summarize the results from DKIM, SPF, rDNS, SMTP AUTH,
# and other email authentication methods.
#
# <a href="https://pypi.python.org/pypi/pydspam/">pydspam</a> wraps
# the libdspam API of the <a href="http://dspam.sourceforge.net/">DSPAM</a>
# project.
#
# @section Milters written with pymilter
#
# <a href="https://github.com/croessner/vrfydmn">Verify Domain</a> is a
# Postfix milter that rejects/fixes manipulated From: header
# on a mail host with multiple virtual domains.
#
# <a href="https://pypi.python.org/pypi/milter/">BMS Milter</a> has several
# milters, a big complicated spam filter that integrates multiple
# authentication protocols with pydpsm, and two simple ones: spfmilter.py and
# dkim-milter.py.
#
+36 -20
View File
@@ -20,41 +20,47 @@
# and converts function callbacks to instance method invocations. # and converts function callbacks to instance method invocations.
# #
class milterContext(object): class milterContext(object):
## Calls <a href="https://www.milter.org/developers/api/smfi_getsymval">smfi_getsymval</a>. ## Calls <a href="milter_api/smfi_getsymval.html">smfi_getsymval</a>.
def getsymval(self,sym): pass def getsymval(self,sym): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_setreply"> ## Calls <a href="milter_api/smfi_setreply.html">
# smfi_setreply</a> or # smfi_setreply</a> or
# <a href="https://www.milter.org/developers/api/smfi_setmlreply"> # <a href="milter_api/smfi_setmlreply.html">
# smfi_setmlreply</a>. # smfi_setmlreply</a>.
# @param rcode SMTP response code # @param rcode SMTP response code
# @param xcode extended SMTP response code # @param xcode extended SMTP response code
# @param msg one or more message lines. If the MTA does not support # @param msg one or more message lines. If the MTA does not support
# multiline messages, only the first is used. # multiline messages, only the first is used.
def setreply(self,rcode,xcode,*msg): pass def setreply(self,rcode,xcode,*msg): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_addheader">smfi_addheader</a>. ## Calls <a href="milter_api/smfi_addheader.html">smfi_addheader</a>.
def addheader(self,name,value,idx=-1): pass def addheader(self,name,value,idx=-1): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">smfi_chgheader</a>. ## Calls <a href="milter_api/smfi_chgheader.html">smfi_chgheader</a>.
def chgheader(self,name,idx,value): pass def chgheader(self,name,idx,value): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">smfi_addrcpt</a>. ## Calls <a href="milter_api/smfi_addrcpt.html">smfi_addrcpt</a>.
def addrcpt(self,rcpt,params=None): pass def addrcpt(self,rcpt,params=None): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">smfi_delrcpt</a>. ## Calls <a href="milter_api/smfi_delrcpt.html">smfi_delrcpt</a>.
def delrcpt(self,rcpt): pass def delrcpt(self,rcpt): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">smfi_replacebody</a>. ## Calls <a href="milter_api/smfi_replacebody.html">smfi_replacebody</a>.
def replacebody(self,data): pass def replacebody(self,data): pass
## Attach a Python object to this connection context. ## Attach a Python object to this connection context.
# @return the old value or None # @return the old value or None
def setpriv(self,priv): pass def setpriv(self,priv): pass
## Return the Python object attached to this connection context. ## Return the Python object attached to this connection context.
def getpriv(self): pass def getpriv(self): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">smfi_quarantine</a>. ## Calls <a href="milter_api/smfi_quarantine.html">smfi_quarantine</a>.
def quarantine(self,reason): pass def quarantine(self,reason): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_progress">smfi_progress</a>. ## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>.
def progress(self): pass def progress(self): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">smfi_chgfrom</a>. ## Calls <a href="milter_api/smfi_chgfrom.html">smfi_chgfrom</a>.
def chgfrom(self,sender,param=None): pass def chgfrom(self,sender,param=None): pass
## Tell the MTA which macro values we are interested in for a given stage. ## Tell the MTA which macro values we are interested in for a given stage.
# Of interest only when you need to squeeze a few more bytes of bandwidth. # Of interest only when you need to squeeze a few more bytes of bandwidth.
def setsmlist(self,stage,macrolist): pass # It may only be called from the negotiate callback.
# The protocol stages are
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
# Calls <a href="milter_api/smfi_setsymlist.html">smfi_setsymlist</a>.
# @param stage protocol stage in which the macro list should be used
# @param macrolist a space separated list of macro names
def setsymlist(self,stage,macrolist): pass
class error(Exception): pass class error(Exception): pass
@@ -91,40 +97,50 @@ def set_exception_policy(code): pass
# in the future (perhaps keeping the set functions for compatibility). # in the future (perhaps keeping the set functions for compatibility).
# @param name the milter name by which the MTA finds us # @param name the milter name by which the MTA finds us
# @param negotiate the # @param negotiate the
# <a href="https://www.milter.org/developers/api/xxfi_negotiate"> # <a href="milter_api/xxfi_negotiate.html">
# xxfi_negotiate</a> callback, called to negotiate supported # xxfi_negotiate</a> callback, called to negotiate supported
# actions, callbacks, and protocol steps. # actions, callbacks, and protocol steps.
# @param unknown the # @param unknown the
# <a href="https://www.milter.org/developers/api/xxfi_unknown"> # <a href="milter_api/xxfi_unknown.html">
# xxfi_unknown</a> callback, called when for SMTP commands # xxfi_unknown</a> callback, called when for SMTP commands
# not recognized by the MTA. (Extend SMTP in your milter!) # not recognized by the MTA. (Extend SMTP in your milter!)
# @param data the # @param data the
# <a href="https://www.milter.org/developers/api/xxfi_data"> # <a href="milter_api/xxfi_data.html">
# xxfi_data</a> callback, called when the DATA # xxfi_data</a> callback, called when the DATA
# SMTP command is received. # SMTP command is received.
def register(name,negotiate=None,unknown=None,data=None): pass def register(name,negotiate=None,unknown=None,data=None): pass
## Attempt to create the socket used to communicate with the MTA.
# milter.opensocket() attempts to create the socket specified previously by a
# call to milter.setconn() which will be the interface between MTAs and the
# %milter. This allows the calling application to ensure that the socket can be
# created. If this is not called, milter.main() will do so implicitly.
# Calls <a href="milter_api/smfi_opensocket.html">
# smfi_opensocket</a>. While not documented for libmilter, my experiments
# indicate that you must call register() before calling opensocket().
# @param rmsock Try to remove an existing unix domain socket if true.
def opensocket(rmsock): pass def opensocket(rmsock): pass
## Transfer control to libmilter. ## Transfer control to libmilter.
# Calls <a href="https://www.milter.org/developers/api/smfi_main"> # Calls <a href="milter_api/smfi_main.html">
# smfi_main</a>. # smfi_main</a>.
def main(): pass def main(): pass
## Set the libmilter debugging level. ## Set the libmilter debugging level.
# <a href="https://www.milter.org/developers/api/smfi_setdbg">smfi_setdbg</a> # <a href="milter_api/smfi_setdbg.html">smfi_setdbg</a>
# sets the milter library's internal debugging level to a new level # sets the %milter library's internal debugging level to a new level
# so that code details may be traced. A level of zero turns off debugging. The # so that code details may be traced. A level of zero turns off debugging. The
# greater (more positive) the level the more detailed the debugging. Six is the # greater (more positive) the level the more detailed the debugging. Six is the
# current, highest, useful value. Must be called before calling main(). # current, highest, useful value. Must be called before calling main().
def setdbg(lev): pass def setdbg(lev): pass
## Set timeout for MTA communication. ## Set timeout for MTA communication.
# Calls <a href="https://www.milter.org/developers/api/smfi_settimeout"> # Calls <a href="milter_api/smfi_settimeout.html">
# smfi_settimeout</a>. Must be called before calling main(). # smfi_settimeout</a>. Must be called before calling main().
def settimeout(secs): pass def settimeout(secs): pass
## Set socket backlog. ## Set socket backlog.
# Calls <a href="https://www.milter.org/developers/api/smfi_setbacklog"> # Calls <a href="milter_api/smfi_setbacklog.html">
# smfi_setbacklog</a>. Must be called before calling main(). # smfi_setbacklog</a>. Must be called before calling main().
def setbacklog(n): pass def setbacklog(n): pass
+5 -3
View File
@@ -1,9 +1,11 @@
web: web:
doxygen doxygen
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-devel-* doc/html/milter_api
rsync -ravKk doc/html/ spidey2.bmsi.com:/Public/pymilter
cd doc/html; zip -r ../../doc .
VERSION=0.9.5 VERSION=0.9.6
CVSTAG=pymilter-0_9_5 CVSTAG=pymilter-0_9_6
PKG=pymilter-$(VERSION) PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz SRCTAR=$(PKG).tar.gz
+79
View File
@@ -0,0 +1,79 @@
## A very simple milter to prevent mixing of internal and external mail.
# Internal is defined as using one of a list of internal top level domains.
# This code is open-source on the same terms as Python.
import Milter
import time
import sys
from Milter.utils import parse_addr
internal_tlds = ["corp", "personal"]
## Determine if a hostname is internal or not.
# True if internal, False otherwise
def is_internal(hostname):
components = hostname.split(".")
return components.pop() in internal_tlds:
# Determine if internal and external hosts are mixed based on a list
# of hostnames
def are_mixed(hostnames):
hostnames_mapped = map(is_internal, hostnames)
# Num internals
num_internal_hosts = hostnames_mapped.count(True)
# Num externals
num_external_hosts = hostnames_mapped.count(False)
return num_external_hosts >= 1 and num_internal_hosts >= 1
class NoMixMilter(Milter.Base):
def __init__(self): # A new instance with each new connection.
self.id = Milter.uniqueID() # Integer incremented with each call.
## def envfrom(self,f,*str):
@Milter.noreply
def envfrom(self, mailfrom, *str):
self.mailfrom = mailfrom
self.domains = []
t = parse_addr(mailfrom)
if len(t) > 1:
self.domains.append(t[1])
else:
self.domains.append('local')
self.internal = False
return Milter.CONTINUE
## def envrcpt(self, to, *str):
def envrcpt(self, to, *str):
self.R.append(to)
t = parse_addr(to)
if len(t) > 1:
self.domains.append(t[1])
else:
self.domains.append('local')
if are_mixed(self.domains):
# FIXME: log recipients collected in self.mailfrom and self.R
self.setreply('550','5.7.1','Mixing internal and external TLDs')
return Milter.REJECT
return Milter.CONTINUE
def main():
socketname = "/var/run/nomixsock"
timeout = 600
# Register to have the Milter factory create instances of your class:
Milter.factory = NoMixMilter
print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush()
Milter.runmilter("nomixfilter",socketname,timeout)
logq.put(None)
bt.join()
print "%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__":
main()
+2 -3
View File
@@ -1,6 +1,6 @@
## To roll your own milter, create a class that extends Milter. ## To roll your own milter, create a class that extends Milter.
# See the pymilter project at http://bmsi.com/python/milter.html # See the pymilter project at http://bmsi.com/python/milter.html
# based on Sendmail's milter API http://www.milter.org/milter_api/api.html # based on Sendmail's milter API
# This code is open-source on the same terms as Python. # This code is open-source on the same terms as Python.
## Milter calls methods of your class at milter events. ## Milter calls methods of your class at milter events.
@@ -79,7 +79,7 @@ class myMilter(Milter.Base):
## def envrcpt(self, to, *str): ## def envrcpt(self, to, *str):
@Milter.noreply @Milter.noreply
def envrcpt(self, recipient, *str): def envrcpt(self, to, *str):
rcptinfo = to,Milter.dictfromlist(str) rcptinfo = to,Milter.dictfromlist(str)
self.R.append(rcptinfo) self.R.append(rcptinfo)
@@ -110,7 +110,6 @@ class myMilter(Milter.Base):
self.addrcpt('<%s>' % 'spy@example.com') self.addrcpt('<%s>' % 'spy@example.com')
return Milter.ACCEPT return Milter.ACCEPT
def close(self): def close(self):
# always called, even when abort is called. Clean up # always called, even when abort is called. Clean up
# any external resources here. # any external resources here.
+93 -56
View File
@@ -35,6 +35,16 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $Log$
* Revision 1.31 2012/04/12 23:32:50 customdesigned
* Replace redundant callback array with macros. If this doesn't break anything,
* macros can be eliminated with code changes.
*
* Revision 1.30 2012/04/12 23:08:06 customdesigned
* Support RFC2553 on BSD
*
* Revision 1.29 2011/06/09 15:45:27 customdesigned
* Print callback name for non-int return error.
*
* Revision 1.28 2011/06/08 23:13:48 customdesigned * Revision 1.28 2011/06/08 23:13:48 customdesigned
* Generate special exception when callback return not int. * Generate special exception when callback return not int.
* *
@@ -257,10 +267,10 @@ $ python setup.py help
#define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */ #define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */
/* Now see if it supports the RFC-2553 socket's API spec. Early /* Now see if it supports the RFC-2553 socket's API spec. Early
* IPv6 "prototype" implementations existed before the RFC was * IPv6 "prototype" implementations existed before the RFC was
* published. Unfortunately I know of now good way to do this * published. Unfortunately I know of no good way to do this
* other than with OS-specific tests. * other than with OS-specific tests.
*/ */
#ifdef linux #if defined(__FreeBSD_kernel__) || defined(__linux__)
#define HAVE_IPV6_RFC2553 #define HAVE_IPV6_RFC2553
#include <arpa/inet.h> #include <arpa/inet.h>
#endif #endif
@@ -274,46 +284,54 @@ $ python setup.py help
#endif #endif
#endif #endif
/* Yes, these are static. If you need multiple different callbacks, */ enum callbacks {
/* it's cleaner to use multiple filters, or convert to OO method calls. */ CONNECT,HELO,ENVFROM,ENVRCPT,HEADER,EOH,BODY,EOM,ABORT,CLOSE,
static PyObject *connect_callback = NULL;
static PyObject *helo_callback = NULL;
static PyObject *envfrom_callback = NULL;
static PyObject *envrcpt_callback = NULL;
static PyObject *header_callback = NULL;
static PyObject *eoh_callback = NULL;
static PyObject *body_callback = NULL;
static PyObject *eom_callback = NULL;
static PyObject *abort_callback = NULL;
static PyObject *close_callback = NULL;
#ifdef SMFIS_ALL_OPTS #ifdef SMFIS_ALL_OPTS
static PyObject *unknown_callback = NULL; UNKNOWN,DATA,NEGOTIATE,
static PyObject *data_callback = NULL;
static PyObject *negotiate_callback = NULL;
#endif #endif
NUMCALLBACKS
};
#define connect_callback callback[CONNECT].cb
#define helo_callback callback[HELO].cb
#define envfrom_callback callback[ENVFROM].cb
#define envrcpt_callback callback[ENVRCPT].cb
#define header_callback callback[HEADER].cb
#define eoh_callback callback[EOH].cb
#define body_callback callback[BODY].cb
#define eom_callback callback[EOM].cb
#define abort_callback callback[ABORT].cb
#define close_callback callback[CLOSE].cb
#define unknown_callback callback[UNKNOWN].cb
#define data_callback callback[DATA].cb
#define negotiate_callback callback[NEGOTIATE].cb
/* Yes, these are static. If you need multiple different callbacks,
it's cleaner to use multiple filters, or convert to OO method calls. */
static struct MilterCallback { static struct MilterCallback {
PyObject **cbp; PyObject *cb;
const char *name; const char *name;
} callback_names[] = { } callback[NUMCALLBACKS+1] = {
{ &connect_callback,"connect" }, { NULL ,"connect" },
{ &helo_callback,"helo" }, { NULL ,"helo" },
{ &envfrom_callback,"envfrom" }, { NULL ,"envfrom" },
{ &envrcpt_callback,"envrcpt" }, { NULL ,"envrcpt" },
{ &header_callback,"header" }, { NULL ,"header" },
{ &eoh_callback,"eoh" }, { NULL ,"eoh" },
{ &body_callback,"body" }, { NULL ,"body" },
{ &eom_callback,"eom" }, { NULL ,"eom" },
{ &abort_callback,"abort" }, { NULL ,"abort" },
{ &close_callback,"close" }, { NULL ,"close" },
#ifdef SMFIS_ALL_OPTS #ifdef SMFIS_ALL_OPTS
{ &unknown_callback,"unknown" }, { NULL ,"unknown" },
{ &data_callback,"data" }, { NULL ,"data" },
{ &negotiate_callback,"negotiate" }, { NULL ,"negotiate" },
#endif #endif
{ NULL , NULL } { NULL , NULL }
}; };
staticforward struct smfiDesc description; /* forward declaration */ static struct smfiDesc description; /* forward declaration */
static PyObject *MilterError; static PyObject *MilterError;
/* The interpreter instance that called milter.main */ /* The interpreter instance that called milter.main */
@@ -325,7 +343,7 @@ typedef struct {
static milter_Diag diag; static milter_Diag diag;
staticforward PyTypeObject milter_ContextType; static PyTypeObject milter_ContextType;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
@@ -662,13 +680,13 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
result = PyEval_CallObject(cb, arglist); result = PyEval_CallObject(cb, arglist);
Py_DECREF(arglist); Py_DECREF(arglist);
if (result == NULL) return _report_exception(self); if (result == NULL) return _report_exception(self);
if (!PyInt_Check(result)) { if (!PyLong_Check(result)) {
const struct MilterCallback *p; const struct MilterCallback *p;
const char *cbname = "milter"; const char *cbname = "milter";
char buf[40]; char buf[40];
Py_DECREF(result); Py_DECREF(result);
for (p = callback_names; p->cbp; ++p) { for (p = callback; p->name; ++p) {
if (cb == *p->cbp) { if (cb == p->cb) {
cbname = p->name; cbname = p->name;
break; break;
} }
@@ -677,7 +695,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
PyErr_SetString(MilterError,buf); PyErr_SetString(MilterError,buf);
return _report_exception(self); return _report_exception(self);
} }
retval = PyInt_AS_LONG(result); retval = PyLong_AS_LONG(result);
Py_DECREF(result); Py_DECREF(result);
_release_thread(self->t); _release_thread(self->t);
return retval; return retval;
@@ -694,7 +712,7 @@ makeipaddr(struct sockaddr_in *addr) {
sprintf(buf, "%d.%d.%d.%d", sprintf(buf, "%d.%d.%d.%d",
(int) (x>>24) & 0xff, (int) (x>>16) & 0xff, (int) (x>>24) & 0xff, (int) (x>>16) & 0xff,
(int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff); (int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff);
return PyString_FromString(buf); return PyUnicode_FromString(buf);
} }
#ifdef HAVE_IPV6_SUPPORT #ifdef HAVE_IPV6_SUPPORT
@@ -702,8 +720,8 @@ static PyObject *
makeip6addr(struct sockaddr_in6 *addr) { makeip6addr(struct sockaddr_in6 *addr) {
char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */ char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */
const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf); const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf);
if (s) return PyString_FromString(s); if (s) return PyUnicode_FromString(s);
return PyString_FromString("inet6:unknown"); return PyUnicode_FromString("inet6:unknown");
} }
#endif #endif
@@ -794,7 +812,7 @@ generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) {
for (i=0;i<count;i++) { for (i=0;i<count;i++) {
/* There's some error checking performed in do_mkvalue() for a string */ /* There's some error checking performed in do_mkvalue() for a string */
/* that's not currently done here - it probably should be */ /* that's not currently done here - it probably should be */
PyObject *o = PyString_FromStringAndSize(argv[i], strlen(argv[i])); PyObject *o = PyUnicode_FromStringAndSize(argv[i], strlen(argv[i]));
if (o == NULL) { /* out of memory */ if (o == NULL) { /* out of memory */
Py_DECREF(arglist); Py_DECREF(arglist);
return _report_exception(self); return _report_exception(self);
@@ -925,7 +943,7 @@ milter_wrap_negotiate(SMFICTX *ctx,
int i; int i;
for (i = 0; i < 4; ++i) { for (i = 0; i < 4; ++i) {
*pa[i] = (i <= len) *pa[i] = (i <= len)
? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i)) ? PyLong_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
: fa[i]; : fa[i];
} }
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
@@ -1513,11 +1531,6 @@ static PyMethodDef context_methods[] = {
{ NULL, NULL } { NULL, NULL }
}; };
static PyObject *
milter_Context_getattr(PyObject *self, char *name) {
return Py_FindMethod(context_methods, self, name);
}
static struct smfiDesc description = { /* Set some reasonable defaults */ static struct smfiDesc description = { /* Set some reasonable defaults */
"pythonfilter", "pythonfilter",
SMFI_VERSION, SMFI_VERSION,
@@ -1566,14 +1579,13 @@ static PyMethodDef milter_methods[] = {
}; };
static PyTypeObject milter_ContextType = { static PyTypeObject milter_ContextType = {
PyObject_HEAD_INIT(&PyType_Type) PyVarObject_HEAD_INIT(&PyType_Type,0)
0,
"milterContext", "milterContext",
sizeof(milter_ContextObject), sizeof(milter_ContextObject),
0, 0,
milter_Context_dealloc, /* tp_dealloc */ milter_Context_dealloc, /* tp_dealloc */
0, /* tp_print */ 0, /* tp_print */
milter_Context_getattr, /* tp_getattr */ 0, /* tp_getattr */
0, /* tp_setattr */ 0, /* tp_setattr */
0, /* tp_compare */ 0, /* tp_compare */
0, /* tp_repr */ 0, /* tp_repr */
@@ -1587,6 +1599,13 @@ static PyTypeObject milter_ContextType = {
0, /* tp_setattro */ 0, /* tp_setattro */
0, /* tp_as_buffer */ 0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */ Py_TPFLAGS_DEFAULT, /* tp_flags */
NULL, /* Documentation string */
0, /* call function for all accessible objects */
0, /* delete references to contained objects */
0, /* rich comparisons */
0, /* weak reference enabler */
0, 0, /* Iterators */
context_methods, /* Attribute descriptor and subclassing stuff */
}; };
static char milter_documentation[] = static char milter_documentation[] =
@@ -1596,17 +1615,27 @@ Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
See <sendmailsource>/libmilter/README for details on setting it up.\n"; See <sendmailsource>/libmilter/README for details on setting it up.\n";
static void setitem(PyObject *d,const char *name,long val) { static void setitem(PyObject *d,const char *name,long val) {
PyObject *v = PyInt_FromLong(val); PyObject *v = PyLong_FromLong(val);
PyDict_SetItemString(d,name,v); PyDict_SetItemString(d,name,v);
Py_DECREF(v); Py_DECREF(v);
} }
void static struct PyModuleDef moduledef = {
initmilter(void) { PyModuleDef_HEAD_INIT,
"milter", /* m_name */
milter_documentation,/* m_doc */
-1, /* m_size */
milter_methods, /* m_methods */
NULL, /* m_reload */
NULL, /* m_traverse */
NULL, /* m_clear */
NULL, /* m_free */
};
PyMODINIT_FUNC PyInit_milter(void) {
PyObject *m, *d; PyObject *m, *d;
m = Py_InitModule4("milter", milter_methods, milter_documentation, m = PyModule_Create(&moduledef);
(PyObject*)NULL, PYTHON_API_VERSION);
d = PyModule_GetDict(m); d = PyModule_GetDict(m);
MilterError = PyErr_NewException("milter.error", NULL, NULL); MilterError = PyErr_NewException("milter.error", NULL, NULL);
PyDict_SetItemString(d,"error", MilterError); PyDict_SetItemString(d,"error", MilterError);
@@ -1633,6 +1662,13 @@ initmilter(void) {
#endif #endif
#ifdef SMFIF_SETSMLIST #ifdef SMFIF_SETSMLIST
setitem(d,"SETSMLIST",SMFIF_SETSMLIST); setitem(d,"SETSMLIST",SMFIF_SETSMLIST);
setitem(d,"M_CONNECT",SMFIM_CONNECT);/* connect */
setitem(d,"M_HELO",SMFIM_HELO); /* HELO/EHLO */
setitem(d,"M_ENVFROM",SMFIM_ENVFROM);/* MAIL From */
setitem(d,"M_ENVRCPT",SMFIM_ENVRCPT);/* RCPT To */
setitem(d,"M_DATA",SMFIM_DATA); /* DATA */
setitem(d,"M_EOM",SMFIM_EOM); /* end of message (final dot) */
setitem(d,"M_EOH",SMFIM_EOH); /* end of header */
#endif #endif
#ifdef SMFIS_ALL_OPTS #ifdef SMFIS_ALL_OPTS
setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ); setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ);
@@ -1665,4 +1701,5 @@ initmilter(void) {
setitem(d,"DISCARD", SMFIS_DISCARD); setitem(d,"DISCARD", SMFIS_DISCARD);
setitem(d,"ACCEPT", SMFIS_ACCEPT); setitem(d,"ACCEPT", SMFIS_ACCEPT);
setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL); setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL);
return m;
} }
+4 -2
View File
@@ -1,4 +1,7 @@
# $Log$ # $Log$
# Revision 1.7 2009/06/13 21:15:12 customdesigned
# Doxygen updates.
#
# Revision 1.6 2009/06/09 03:13:13 customdesigned # Revision 1.6 2009/06/09 03:13:13 customdesigned
# More doxygen docs. # More doxygen docs.
# #
@@ -165,15 +168,14 @@ class MimeMessage(Message):
""" """
def __init__(self,fp=None,seekable=1): def __init__(self,fp=None,seekable=1):
Message.__init__(self) Message.__init__(self)
self.headerchange = None
self.submsg = None self.submsg = None
self.modified = False self.modified = False
## @var headerchange ## @var headerchange
# Provide a headerchange event for integration with Milter. # Provide a headerchange event for integration with Milter.
# The headerchange attribute can be assigned a function to be called when # The headerchange attribute can be assigned a function to be called when
# changing headers. The signature is: # changing headers. The signature is:
# headerchange(msg,name,value) -> None # headerchange(msg,name,value) -> None
self.headerchange = None
def get_param(self, param, failobj=None, header='content-type', unquote=True): def get_param(self, param, failobj=None, header='content-type', unquote=True):
val = Message.get_param(self,param,failobj,header,unquote) val = Message.get_param(self,param,failobj,header,unquote)
+10 -2
View File
@@ -6,7 +6,7 @@
Summary: Python interface to sendmail milter API Summary: Python interface to sendmail milter API
Name: %{pythonbase}-pymilter Name: %{pythonbase}-pymilter
Version: 0.9.5 Version: 0.9.6
Release: 1%{dist} Release: 1%{dist}
Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
License: GPLv2+ License: GPLv2+
@@ -75,7 +75,15 @@ chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
%changelog %changelog
* Wed Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.5-1 * Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
- Remove redundant table in miltermodule
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1
- Raise ValueError on unescaped '%' passed to setreply
- Grace time at end of Greylist window
* Fri Aug 19 2011 Stuart Gathman <stuart@bmsi.com> 0.9.5-1
- Print milter.error for invalid callback return type. - Print milter.error for invalid callback return type.
(Since stacktrace is empty, the TypeError exception is confusing.) (Since stacktrace is empty, the TypeError exception is confusing.)
- Fix milter-template.py - Fix milter-template.py
+9 -7
View File
@@ -1,4 +1,4 @@
from __future__ import print_function
# A simple milter. # A simple milter.
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
@@ -21,9 +21,9 @@ class sampleMilter(Milter.Milter):
"Milter to replace attachments poisonous to Windows with a WARNING message." "Milter to replace attachments poisonous to Windows with a WARNING message."
def log(self,*msg): def log(self,*msg):
print "%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id), print("%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id), end=' ')
for i in msg: print i, for i in msg: print(i, end=' ')
print print()
def __init__(self): def __init__(self):
self.tempname = None self.tempname = None
@@ -35,7 +35,9 @@ class sampleMilter(Milter.Milter):
# multiple messages can be received on a single connection # multiple messages can be received on a single connection
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
# of each message. # of each message.
@Milter.noreply
def envfrom(self,f,*str): def envfrom(self,f,*str):
"start of MAIL transaction"
self.log("mail from",f,str) self.log("mail from",f,str)
self.fp = StringIO.StringIO() self.fp = StringIO.StringIO()
self.tempname = None self.tempname = None
@@ -169,13 +171,13 @@ if __name__ == "__main__":
socketname = os.getenv("HOME") + "/pythonsock" socketname = os.getenv("HOME") + "/pythonsock"
Milter.factory = sampleMilter Milter.factory = sampleMilter
Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS) Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS)
print """To use this with sendmail, add the following to sendmail.cf: print("""To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=pythonfilter O InputMailFilters=pythonfilter
Xpythonfilter, S=local:%s Xpythonfilter, S=local:%s
See the sendmail README for libmilter. See the sendmail README for libmilter.
sample milter startup""" % socketname sample milter startup""" % socketname)
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,240) Milter.runmilter("pythonfilter",socketname,240)
print "sample milter shutdown" print("sample milter shutdown")
+3 -3
View File
@@ -1,5 +1,5 @@
[bdist_rpm] [bdist_rpm]
python=python2.6 python=python3
doc_files=README NEWS TODO doc_files=README NEWS TODO COPYING CREDITS
packager=Stuart D. Gathman <stuart@bmsi.com> packager=Stuart D. Gathman <stuart@gathman.org>
release=1 release=1
+1 -1
View File
@@ -13,7 +13,7 @@ libs = ["milter"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian libdirs = ["/usr/lib/libmilter"] # needed for Debian
# NOTE: importing Milter to obtain version fails when milter.so not built # NOTE: importing Milter to obtain version fails when milter.so not built
setup(name = "pymilter", version = '0.9.5', setup(name = "pymilter", version = '0.9.7',
description="Python interface to sendmail milter API", description="Python interface to sendmail milter API",
long_description="""\ long_description="""\
This is a python extension module to enable python scripts to This is a python extension module to enable python scripts to
+18587
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -1,4 +1,7 @@
# $Log$ # $Log$
# Revision 1.5 2011/06/09 17:27:42 customdesigned
# Documentation updates.
#
# Revision 1.4 2005/07/20 14:49:44 customdesigned # Revision 1.4 2005/07/20 14:49:44 customdesigned
# Handle corrupt and empty ZIP files. # Handle corrupt and empty ZIP files.
# #
@@ -170,7 +173,7 @@ class MimeTestCase(unittest.TestCase):
msg = mime.message_from_file(open('test/'+fname,'r')) msg = mime.message_from_file(open('test/'+fname,'r'))
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.failIf(msg.ismodified()) self.failIf(msg.ismodified())
msg = mime.message_from_file(open('test/tmpytgcE5.fail','r')) msg = mime.message_from_file(open('test/test2','r'))
rc = mime.check_attachments(msg,self._chk_attach) rc = mime.check_attachments(msg,self._chk_attach)
self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF") self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
self.assertEquals(rc,Milter.CONTINUE) self.assertEquals(rc,Milter.CONTINUE)
@@ -198,4 +201,4 @@ if __name__ == '__main__':
fp = open(fname,'r') fp = open(fname,'r')
msg = mime.message_from_file(fp) msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
print msg.as_string() print(msg.as_string())
+6
View File
@@ -11,6 +11,7 @@ class AddrCacheTestCase(unittest.TestCase):
self.fname = 'test.dat' self.fname = 'test.dat'
def tearDown(self): def tearDown(self):
if os.path.exists(self.fname):
os.remove(self.fname) os.remove(self.fname)
def testAdd(self): def testAdd(self):
@@ -38,6 +39,11 @@ class AddrCacheTestCase(unittest.TestCase):
cache.load(self.fname,30) cache.load(self.fname,30)
self.failUnless('spammer.com' in cache) self.failUnless('spammer.com' in cache)
def testParseHeader(self):
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
h = Milter.utils.parse_header(s)
self.assertEqual(h,'Last Few Coldplay Album Artworks Available\x00')
def suite(): def suite():
s = unittest.makeSuite(AddrCacheTestCase,'test') s = unittest.makeSuite(AddrCacheTestCase,'test')
s.addTest(doctest.DocTestSuite(Milter.utils)) s.addTest(doctest.DocTestSuite(Milter.utils))