Compare commits

..

1 Commits

Author SHA1 Message Date
cvs2svn 07503a8dea This commit was manufactured by cvs2svn to create tag 'pymilter-0_9_2'.
Sprout from master 2009-06-04 22:17:40 UTC Stuart Gathman <stuart@gathman.org> 'Release 0.9.2-3'
Cherrypick from bmsi 2005-05-31 18:23:49 UTC Stuart Gathman <stuart@gathman.org> 'Development changes since 0.7.2':
    sample.py
    test/amazon
    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
    testsample.py
2009-06-04 22:17:41 +00:00
35 changed files with 1166 additions and 22263 deletions
+2 -4
View File
@@ -1,5 +1,5 @@
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that
kludge and added threading and context objects to it, wrote a proper OO kludge and added threading and context objects to it, wrote a proper OO
wrapper (Milter.py) that handles attachments, did lots of testing, packaged wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a it with distutils, and generally transformed it from a quick hack to a
@@ -7,8 +7,6 @@ real, usable Python extension.
Other contributors (in random order): Other contributors (in random order):
Daniel Troeder
for pointing out a typo in @noreply
arkanes@irc.freenode.net arkanes@irc.freenode.net
for suggesting a class method to compute and cache protocol masks for suggesting a class method to compute and cache protocol masks
habnabit@habnabit.org habnabit@habnabit.org
@@ -43,4 +41,4 @@ Business Management Systems - http://www.bmsi.com
for hosting the website, and providing paying clients who need milter service for hosting the website, and providing paying clients who need milter service
so I can work on it as part of my day job. so I can work on it as part of my day job.
If I have left anybody out, send me a reminder: stuart@gathman.org If I have left anybody out, send me a reminder: stuart@bmsi.com
-1522
View File
File diff suppressed because it is too large Load Diff
+54 -479
View File
@@ -1,28 +1,22 @@
## @package Milter # Author: Stuart D. Gathman <stuart@bmsi.com>
# A thin OO wrapper for the milter module. # Copyright 2001 Business Management Systems, Inc.
#
# Clients generally subclass Milter.Base and define callback
# methods.
#
# @author Stuart D. Gathman <stuart@bmsi.com>
# 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.8' # A thin OO wrapper for the milter module
__version__ = '0.9.2'
import os import os
import re
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
def uniqueID(): def uniqueID():
"""Return a unique sequence number (incremented on each call). """Return a sequence number unique to this process.
""" """
global _seq global _seq
_seq_lock.acquire() _seq_lock.acquire()
@@ -30,7 +24,6 @@ def uniqueID():
_seq_lock.release() _seq_lock.release()
return seqno return seqno
## @private
OPTIONAL_CALLBACKS = { OPTIONAL_CALLBACKS = {
'connect':(P_NR_CONN,P_NOCONNECT), 'connect':(P_NR_CONN,P_NOCONNECT),
'hello':(P_NR_HELO,P_NOHELO), 'hello':(P_NR_HELO,P_NOHELO),
@@ -43,325 +36,72 @@ OPTIONAL_CALLBACKS = {
'header':(P_NR_HDR,P_NOHDRS) 'header':(P_NR_HDR,P_NOHDRS)
} }
## @private
R = re.compile(r'%+')
## @private
def decode_mask(bits,names):
t = [ (s,getattr(milter,s)) for s in names]
nms = [s for s,m in t if bits & m]
for s,m in t: bits &= ~m
if bits: nms += hex(bits)
return nms
## Class decorator to enable optional protocol steps.
# P_SKIP is enabled by default when supported, but
# applications may wish to enable P_HDR_LEADSPC
# to send and receive the leading space of header continuation
# lines unchanged, and/or P_RCPT_REJ to have recipients
# detected as invalid by the MTA passed to the envcrpt callback.
#
# Applications may want to check whether the protocol is actually
# supported by the MTA in use. Base._protocol
# is a bitmask of protocol options negotiated. So,
# for instance, if <code>self._protocol & Milter.P_RCPT_REJ</code>
# is true, then that feature was successfully negotiated with the MTA
# and the application will see recipients the MTA has flagged as invalid.
#
# Sample use:
# <pre>
# class myMilter(Milter.Base):
# def envrcpt(self,to,*params):
# return Milter.CONTINUE
# myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ)
# </pre>
# @since 0.9.3
# @param klass the %milter application class to modify
# @param mask a bitmask of protocol steps to enable
# @return the modified %milter class
def enable_protocols(klass,mask):
klass._protocol_mask = klass.protocol_mask() & ~mask
return klass
## Milter rejected recipients. A class decorator that calls
# enable_protocols() with the P_RCPT_REJ flag. By default, the MTA
# does not pass recipients that it knows are invalid on to the milter.
# This decorator enables a %milter app to see all recipients if supported
# by the MTA. Use like this with python-2.6 and later:
# <pre>
# @@Milter.rejected_recipients
# class myMilter(Milter.Base):
# def envrcpt(self,to,*params):
# return Milter.CONTINUE
# </pre>
# @since 0.9.5
# @param klass the %milter application class to modify
# @return the modified %milter class
def rejected_recipients(klass):
return enable_protocols(klass,P_RCPT_REJ)
## Milter leading space on headers. A class decorator that calls
# enable_protocols() with the P_HEAD_LEADSPC flag. By default,
# header continuation lines are collected and joined before getting
# sent to a milter. Headers modified or added by the milter are
# folded by the MTA as necessary according to its own standards.
# With this flag, header continuation lines are preserved
# with their newlines and leading space. In addition, header folding
# done by the milter is preserved as well.
# Use like this with python-2.6 and later:
# <pre>
# @@Milter.header_leading_space
# class myMilter(Milter.Base):
# def header(self,hname,value):
# return Milter.CONTINUE
# </pre>
# @since 0.9.5
# @param klass the %milter application class to modify
# @return the modified %milter class
def header_leading_space(klass):
return enable_protocols(klass,P_HEAD_LEADSPC)
## Function decorator to disable callback methods.
# If the MTA supports it, tells the MTA not to invoke this callback,
# increasing efficiency. All the callbacks (except negotiate)
# are disabled in Milter.Base, and overriding them reenables the
# callback. An application may need to use @@nocallback when it extends
# another %milter and wants to disable a callback again.
# The disabled method should still return Milter.CONTINUE, in case the MTA does
# not support protocol negotiation, and for when called from a test harness.
# @since 0.9.2
def nocallback(func): def nocallback(func):
try: try:
func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][1] func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][1]
except KeyError: except KeyError:
raise ValueError( raise ValueError(
'@nocallback applied to non-optional method: '+func.__name__) '@nocallback applied to non-optional method: '+func.__name__)
def wrapper(self,*args): return func
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.
# If the MTA supports it, tells the MTA not to wait for a reply from
# this callback, and assume CONTINUE. The method should still return
# CONTINUE in case the MTA does not support protocol negotiation.
# The decorator arranges to change the return code to NOREPLY
# when supported by the MTA.
# @since 0.9.2
def noreply(func): def noreply(func):
try: try:
nr_mask = OPTIONAL_CALLBACKS[func.__name__][0] nr_mask = OPTIONAL_CALLBACKS[func.__name__][0]
except KeyError: except KeyErro:
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: if self._protocol & nr_mask: return NOREPLY
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
## Disabled action exception.
# set_flags() can tell the MTA that this application will not use certain
# features (such as CHGFROM). This can also be negotiated for each
# connection in the negotiate callback. If the application then calls
# the feature anyway via an instance method, this exception is
# thrown.
# @since 0.9.2
class DisabledAction(RuntimeError): class DisabledAction(RuntimeError):
pass pass
## A do "nothing" Milter base class representing an SMTP connection. # A do nothing Milter base class from which python milters should derive
# # unless they are using the milter C module directly.
# Python milters should derive from this class
# unless they are using the low level milter module directly.
#
# Most of the methods are either "actions" or "callbacks". Callbacks
# are invoked by the MTA at certain points in the SMTP protocol. For
# instance when the HELO command is seen, the MTA calls the helo
# callback before returning a response code. All callbacks must
# return one of these constants: CONTINUE, TEMPFAIL, REJECT, ACCEPT,
# DISCARD, SKIP. The NOREPLY response is supplied automatically by
# the @@noreply decorator if negotiation with the MTA is successful.
# @@noreply and @@nocallback methods should return CONTINUE for two reasons:
# the MTA may not support negotiation, and the class may be running in a test
# harness.
#
# Optional callbacks are disabled with the @@nocallback decorator, and
# automatically reenabled when overridden. Disabled callbacks should
# still return CONTINUE for testing and MTAs that do not support
# negotiation.
# Each SMTP connection to the MTA calls the factory method you provide to
# create an instance derived from this class. This is typically the
# constructor for a class derived from Base. The _setctx() method attaches
# the instance to the low level milter.milterContext object. When the SMTP
# connection terminates, the close callback is called, the low level connection
# object is destroyed, and this normally causes instances of this class to be
# garbage collected as well. The close() method should release any global
# resources held by instances.
# @since 0.9.2
class Base(object): class Base(object):
"The core class interface to the %milter module." "The core class interface to the milter module."
## Attach this Milter to the low level milter.milterContext object.
def _setctx(self,ctx): def _setctx(self,ctx):
## The low level @ref milter.milterContext object.
self._ctx = ctx self._ctx = ctx
## A bitmask of actions this connection has negotiated to use.
# By default, all actions are enabled. High throughput milters
# may want to disable unused actions to increase efficiency.
# Some optional actions may be disabled by calling milter.set_flags(), or
# by overriding the negotiate callback. The bits include:
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
# CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST</code>.
# The <code>Milter.CURR_ACTS</code> bitmask is all actions
# known when the milter module was compiled.
# Application code can also inspect this field to determine
# which actions are available. This is especially useful in
# generic library code designed to work in multiple milters.
# @since 0.9.2
#
self._actions = CURR_ACTS # all actions enabled by default self._actions = CURR_ACTS # all actions enabled by default
## A bitmask of protocol options this connection has negotiated.
# An application may inspect this
# variable to determine which protocol steps are supported. Options
# of interest to applications: the SKIP result code is allowed
# only if the P_SKIP bit is set, rejected recipients are passed to the
# %milter application only if the P_RCPT_REJ bit is set, and
# header values are sent and received with leading spaces (in the
# continuation lines) intact if the P_HDR_LEADSPC bit is set (so
# that the application can customize indenting).
#
# The P_N* bits should be negotiated via the @@noreply and @@nocallback
# method decorators, and P_RCPT_REJ, P_HDR_LEADSPC should
# be enabled using the enable_protocols class decorator.
#
# The bits include: <code>
# P_RCPT_REJ P_NR_CONN P_NR_HELO P_NR_MAIL P_NR_RCPT P_NR_DATA P_NR_UNKN
# P_NR_EOH P_NR_BODY P_NR_HDR P_NOCONNECT P_NOHELO P_NOMAIL P_NORCPT
# P_NODATA P_NOUNKNOWN P_NOEOH P_NOBODY P_NOHDRS P_HDR_LEADSPC P_SKIP
# </code> (all under the Milter namespace).
# @since 0.9.2
self._protocol = 0 # no protocol options by default self._protocol = 0 # no protocol options by default
if ctx: if ctx:
ctx.setpriv(self) ctx.setpriv(self)
## 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 by the
# <a href="https://www.milter.org/developers/api/xxfi_connect">
# xxfi_connect</a> callback.
# 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 format of hostaddr depends on the socket family:
# <dl>
# <dt><code>socket.AF_INET</code>
# <dd>A tuple of (IP as string in dotted quad form, integer port)
# <dt><code>socket.AF_INET6</code>
# <dd>A tuple of (IP as a string in standard representation,
# integer port, integer flow info, integer scope id)
# <dt><code>socket.AF_UNIX</code>
# <dd>A string with the socketname
# </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 family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
# or <code>socket.AF_UNIX</code>
# @param hostaddr a tuple or string with peer IP or socketname
@nocallback @nocallback
def connect(self,hostname,family,hostaddr): return CONTINUE def connect(self,hostname,family,hostaddr): return CONTINUE
## Called when the SMTP client says HELO.
# Returning REJECT prevents progress until a valid HELO is provided;
# 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 by the
# <a href="https://www.milter.org/developers/api/xxfi_envfrom">
# xxfi_envfrom</a> callback.
# 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 by the
# <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
# xxfi_envrcpt</a> callback.
# 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.
# Returning REJECT rejects the message without wasting bandwidth
# on the unwanted message.
# @since 0.9.2
@nocallback @nocallback
def data(self): return CONTINUE def data(self): return CONTINUE
## Called for each header field in the message body.
@nocallback @nocallback
def header(self,field,value): return CONTINUE def header(self,field,value): return CONTINUE
## Called at the blank line that terminates the header fields.
@nocallback @nocallback
def eoh(self): return CONTINUE def eoh(self): return CONTINUE
## Called to supply the body of the message to the Milter by chunks.
# @param blk a block of message bytes
@nocallback @nocallback
def body(self,blk): return CONTINUE def body(self,unused): return CONTINUE
## Called when the SMTP client issues an unknown command.
# @param cmd the unknown command
# @since 0.9.2
@nocallback @nocallback
def unknown(self,cmd): return CONTINUE def unknown(self,cmd): return CONTINUE
## Called at the end of the message body.
# Most of the message manipulation actions can only take place from
# the eom callback.
def eom(self): return CONTINUE def eom(self): return CONTINUE
## Called when the connection is abnormally terminated.
# The close callback is still called also.
def abort(self): return CONTINUE def abort(self): return CONTINUE
## 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
# <code>milter_protocol</code> function attribute to the protocol mask bit to
# pass to libmilter, causing that callback or its reply to be skipped.
# Overriding a method creates a new function object, so that
# <code>milter_protocol</code> defaults to 0.
# Libmilter passes the protocol bits that the current MTA knows
# how to skip. We clear the ones we don't want to skip.
# The negation is somewhat mind bending, but it is simple.
# @since 0.9.2
@classmethod @classmethod
def protocol_mask(klass): def protocol_mask(klass):
try: try:
return klass._protocol_mask return klass._protocol_mask
except AttributeError: except AttributeError:
p = P_RCPT_REJ | P_HDR_LEADSPC # turn these new features off by default p = 0
for func,(nr,nc) in OPTIONAL_CALLBACKS.items(): for func,(nr,nc) in OPTIONAL_CALLBACKS.items():
func = getattr(klass,func) func = getattr(klass,func)
ca = getattr(func,'milter_protocol',0) ca = getattr(func,'milter_protocol',0)
@@ -370,25 +110,13 @@ class Base(object):
klass._protocol_mask = p klass._protocol_mask = p
return p return p
## Negotiate milter protocol options. Called by the
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
# 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
# a list of 4 32-bit ints which can be modified and are passed
# back to libmilter on return.
# Default negotiation sets P_NO* and P_NR* for callbacks # Default negotiation sets P_NO* and P_NR* for callbacks
# marked @@nocallback and @@noreply respectively, leaves all # marked @nocallback and @noreply respectively
# actions enabled, and enables Milter.SKIP. The @@enable_protocols
# class decorator can customize which protocol steps are implemented.
# @param opts a modifiable list of 4 ints with negotiated options
# @since 0.9.2
def negotiate(self,opts): def negotiate(self,opts):
try: try:
self._actions,p,f1,f2 = opts self._actions,p,f1,f2 = opts
opts[1] = self._protocol = p & ~self.protocol_mask() opts[1] = self._protocol = \
p & ~self.protocol_mask() & ~P_RCPT_REJ & ~P_HDR_LEADSPC
opts[2] = 0 opts[2] = 0
opts[3] = 0 opts[3] = 0
#self.log("Negotiated:",opts) #self.log("Negotiated:",opts)
@@ -398,186 +126,61 @@ class Base(object):
return CONTINUE return CONTINUE
# Milter methods which can be invoked from most callbacks # Milter methods which can be invoked from most callbacks
## Return the value of an MTA macro. Sendmail macro names
# are either single chars (e.g. "j") or multiple chars enclosed
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
# See <a href="https://www.milter.org/developers/api/smfi_getsymval">
# smfi_getsymval</a> for default sendmail macros.
# @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. # If sendmail 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="https://www.milter.org/developers/api/smfi_setreply">
# 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. # may only be called from negotiate callback
# This information can reduce the size of messages received from sendmail, def setsmlist(self,stage,macros):
# and hence could reduce bandwidth between sendmail and your milter where if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST")
# that is a factor. The <code>Milter.SETSYMLIST</code> action flag must be if type(macros) in (list,tuple):
# set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, macros = ' '.join(macros)
# M_DATA, M_EOM, M_EOH. return self._ctx.setsmlist(stage,macros)
#
# May only be called from negotiate callback.
# @since 0.9.8, previous version was misspelled!
# @param stage the protocol stage to set to macro list for,
# one of the M_* constants defined in Milter
# @param macros space separated and/or lists of strings
def setsymlist(self,stage,*macros):
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
a = []
for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(' ')
except: pass
a += m
return self._ctx.setsmlist(stage,' '.join(a))
# 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.
# Calls <a href="https://www.milter.org/developers/api/smfi_addheader">
# smfi_addheader</a>.
# The <code>Milter.ADDHDRS</code> action flag must be set.
#
# May be called from eom callback only.
# @param field the header field name
# @param value the header field value
# @param idx header field index from the top of the message to insert at
# @throws DisabledAction if ADDHDRS is not enabled
def addheader(self,field,value,idx=-1): def addheader(self,field,value,idx=-1):
if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS") if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS")
return self._ctx.addheader(field,value,idx) return self._ctx.addheader(field,value,idx)
## Change the value of a mail header field.
# Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">
# smfi_chgheader</a>.
# The <code>Milter.CHGHDRS</code> action flag must be set.
#
# May be called from eom callback only.
# @param field the name of the field to change
# @param idx index of the field to change when there are multiple instances
# @param value the new value of the field
# @throws DisabledAction if CHGHDRS is not enabled
def chgheader(self,field,idx,value): def chgheader(self,field,idx,value):
if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS") if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS")
return self._ctx.chgheader(field,idx,value) return self._ctx.chgheader(field,idx,value)
## Add a recipient to the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">
# smfi_addrcpt</a>.
# 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
# RCPT TO command (and as delivered to the envrcpt callback), for example
# "self.addrcpt('<foo@example.com>')".
# The <code>Milter.ADDRCPT</code> action flag must be set.
# If the optional <code>params</code> argument is used, then
# the <code>Milter.ADDRCPT_PAR</code> action flag must be set.
#
# May be called from eom callback only.
# @param rcpt the message recipient
# @param params an optional list of ESMTP parameters
# @throws DisabledAction if ADDRCPT or ADDRCPT_PAR is not enabled
def addrcpt(self,rcpt,params=None): def addrcpt(self,rcpt,params=None):
if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT") if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT")
if params and not self._actions & ADDRCPT_PAR:
raise DisabledAction("ADDRCPT_PAR")
return self._ctx.addrcpt(rcpt,params) return self._ctx.addrcpt(rcpt,params)
## Delete a recipient from the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">
# smfi_delrcpt</a>.
# The recipient should match one passed to the envrcpt callback.
# The <code>Milter.DELRCPT</code> action flag must be set.
#
# May be called from eom callback only.
# @param rcpt the message recipient to delete
# @throws DisabledAction if DELRCPT is not enabled
def delrcpt(self,rcpt): def delrcpt(self,rcpt):
if not self._actions & DELRCPT: raise DisabledAction("DELRCPT") if not self._actions & DELRCPT: raise DisabledAction("DELRCPT")
return self._ctx.delrcpt(rcpt) return self._ctx.delrcpt(rcpt)
## Replace the message body.
# Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">
# smfi_replacebody</a>.
# The entire message body must be replaced.
# Call repeatedly with blocks of data until the entire body is transferred.
# The <code>Milter.MODBODY</code> action flag must be set.
#
# May be called from eom callback only.
# @param body a chunk of body data
# @throws DisabledAction if MODBODY is not enabled
def replacebody(self,body): def replacebody(self,body):
if not self._actions & MODBODY: raise DisabledAction("MODBODY") if not self._actions & MODBODY: raise DisabledAction("MODBODY")
return self._ctx.replacebody(body) return self._ctx.replacebody(body)
## Change the SMTP envelope sender address.
# Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">
# smfi_chgfrom</a>.
# The syntax of the sender is that same as used in the SMTP
# MAIL FROM command (and as delivered to the envfrom callback),
# for example <code>self.chgfrom('<bar@example.com>')</code>.
# The <code>Milter.CHGFROM</code> action flag must be set.
#
# May be called from eom callback only.
# @since 0.9.1
# @param sender the new sender address
# @param params an optional list of ESMTP parameters
# @throws DisabledAction if CHGFROM is not enabled
def chgfrom(self,sender,params=None): def chgfrom(self,sender,params=None):
if not self._actions & CHGFROM: raise DisabledAction("CHGFROM") if not self._actions & CHGFROM: raise DisabledAction("CHGFROM")
return self._ctx.chgfrom(sender,params) return self._ctx.chgfrom(sender,params)
## Quarantine the message.
# Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">
# 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.
# The <code>Milter.QUARANTINE</code> action flag must be set.
#
# May be called from eom callback only.
# @param reason a string describing the reason for quarantine
# @throws DisabledAction if QUARANTINE is not enabled
def quarantine(self,reason): def quarantine(self,reason):
if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE") if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE")
return self._ctx.quarantine(reason) return self._ctx.quarantine(reason)
## Tell the MTA to wait a bit longer.
# Calls <a href="https://www.milter.org/developers/api/smfi_progress">
# smfi_progress</a>.
# Resets timeouts in the MTA that detect a "hung" milter.
def progress(self): def progress(self):
return self._ctx.progress() return self._ctx.progress()
## A logging but otherwise do nothing Milter base class. # A logging but otherwise do nothing Milter base class included
# This is included for compatibility with previous versions of pymilter. # for compatibility with previous versions of pymilter.
# The logging callbacks are marked @@noreply.
class Milter(Base): class Milter(Base):
"A simple class interface to the milter module." "A simple class interface to the milter module."
## Provide simple logging to sys.stdout
def log(self,*msg): def log(self,*msg):
print 'Milter:', print 'Milter:',
for i in msg: print i, for i in msg: print i,
@@ -637,25 +240,13 @@ class Milter(Base):
self.log("close") self.log("close")
return CONTINUE return CONTINUE
## The milter connection factory
# This factory method is called for each connection to create the
# python object that tracks the connection. It should return
# an object derived from Milter.Base.
#
# Note that since python is dynamic, this variable can be changed while
# the milter is running: for instance, to a new subclass based on a
# change in configuration.
factory = Milter factory = Milter
## @private
# @brief Connect context to connection instance and return enabled callbacks.
def negotiate_callback(ctx,opts): def negotiate_callback(ctx,opts):
m = factory() m = factory()
m._setctx(ctx) m._setctx(ctx)
return m.negotiate(opts) return m.negotiate(opts)
## @private
# @brief Connect context if needed and invoke connect method.
def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN): def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
m = ctx.getpriv() m = ctx.getpriv()
if not m: if not m:
@@ -665,8 +256,6 @@ def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
m._setctx(ctx) m._setctx(ctx)
return m.connect(hostname,family,hostaddr) return m.connect(hostname,family,hostaddr)
## @private
# @brief Disconnect milterContext and call close method.
def close_callback(ctx): def close_callback(ctx):
m = ctx.getpriv() m = ctx.getpriv()
if not m: return CONTINUE if not m: return CONTINUE
@@ -676,10 +265,8 @@ def close_callback(ctx):
m._setctx(None) # release milterContext m._setctx(None) # release milterContext
return rc return rc
## Convert ESMTP parameters with values to a keyword dictionary.
# @deprecated You probably want Milter.param2dict instead.
def dictfromlist(args): def dictfromlist(args):
"Convert ESMTP parms with values to keyword dictionary." "Convert ESMTP parm list to keyword dictionary."
kw = {} kw = {}
for s in args: for s in args:
pos = s.find('=') pos = s.find('=')
@@ -687,18 +274,6 @@ def dictfromlist(args):
kw[s[:pos].upper()] = s[pos+1:] kw[s[:pos].upper()] = s[pos+1:]
return kw return kw
## Convert ESMTP parm list to keyword dictionary.
# Params with no value are set to None in the dictionary.
# @since 0.9.3
# @param str list of param strings of the form "NAME" or "NAME=VALUE"
# @return a dictionary of ESMTP param names and values
def param2dict(str):
"Convert ESMTP parm list to keyword dictionary."
pairs = [x.split('=',1) for x in str]
for e in pairs:
if len(e) < 2: e.append(None)
return dict([(k.upper(),v) for k,v in pairs])
def envcallback(c,args): def envcallback(c,args):
"""Call function c with ESMTP parms converted to keyword parameters. """Call function c with ESMTP parms converted to keyword parameters.
Can be used in the envfrom and/or envrcpt callbacks to process Can be used in the envfrom and/or envrcpt callbacks to process
@@ -713,12 +288,28 @@ def envcallback(c,args):
pargs.append(s) pargs.append(s)
return c(*pargs,**kw) return c(*pargs,**kw)
## Run the %milter. def runmilter(name,socketname,timeout = 0):
# @param name the name of the %milter known to the MTA # This bit is here on the assumption that you will be starting this filter
# @param socketname the socket to be passed to milter.setconn() # before sendmail. If sendmail is not running and the socket already exists,
# @param timeout the time in secs the MTA should wait for a response before # libmilter will throw a warning. If sendmail is running, this is still
# considering this %milter dead # safe if there are no messages currently being processed. It's safer to
def runmilter(name,socketname,timeout = 0,rmsock=True): # shutdown sendmail, kill the filter process, restart the filter, and then
# restart sendmail.
pos = socketname.find(':')
if pos > 1:
s = socketname[:pos]
fname = socketname[pos+1:]
else:
s = "unix"
fname = socketname
if s == "unix" or s == "local":
print "Removing %s" % fname
try:
os.unlink(fname)
except os.error, x:
import errno
if x.errno != errno.ENOENT:
raise milter.error(x)
# The default flags set include everything # The default flags set include everything
# milter.set_flags(milter.ADDHDRS) # milter.set_flags(milter.ADDHDRS)
@@ -739,24 +330,12 @@ def runmilter(name,socketname,timeout = 0,rmsock=True):
milter.setconn(socketname) milter.setconn(socketname)
if timeout > 0: milter.settimeout(timeout) if timeout > 0: milter.settimeout(timeout)
# disable negotiate callback if runtime version < (1,0,1)
ncb = negotiate_callback
if milter.getversion() < (1,0,1):
ncb = None
# The name *must* match the X line in sendmail.cf (supposedly) # The name *must* match the X line in sendmail.cf (supposedly)
milter.register(name, milter.register(name,
data=lambda ctx: ctx.getpriv().data(), data=lambda ctx: ctx.getpriv().data(),
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd), unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
negotiate=ncb negotiate=negotiate_callback
) )
# We remove the socket here by default on the assumption that you will be
# starting this filter before sendmail. If sendmail is not running and the
# socket already exists, libmilter will throw a warning. If sendmail is
# running, this is still safe if there are no messages currently being
# processed. It's safer to shutdown sendmail, kill the filter process,
# restart the filter, and then restart sendmail.
milter.opensocket(rmsock)
start_seq = _seq start_seq = _seq
try: try:
milter.main() milter.main()
@@ -769,7 +348,3 @@ __all__ = globals().copy()
for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'): for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'):
del __all__[priv] del __all__[priv]
__all__ = __all__.keys() __all__ = __all__.keys()
## @example milter-template.py
## @example milter-nomix.py
#
+9 -12
View File
@@ -10,9 +10,6 @@
# CBV results. # CBV results.
# #
# $Log$ # $Log$
# Revision 1.9 2008/05/08 21:35:57 customdesigned
# Allow explicitly whitelisted email from banned_users.
#
# Revision 1.8 2007/09/03 16:18:45 customdesigned # Revision 1.8 2007/09/03 16:18:45 customdesigned
# Delete unparseable timestamps when loading address cache. These have # Delete unparseable timestamps when loading address cache. These have
# arisen because of failure to parse MAIL FROM properly. Will have to # arisen because of failure to parse MAIL FROM properly. Will have to
@@ -75,8 +72,8 @@ class AddrCache(object):
except OSError: except OSError:
fp = () fp = ()
for ln in fp: for ln in fp:
try: try:
rcpt,ts = ln.strip().split(None,1) rcpt,ts = ln.strip().split(None,1)
try: try:
l = time.strptime(ts,AddrCache.time_format) l = time.strptime(ts,AddrCache.time_format)
t = time.mktime(l) t = time.mktime(l)
@@ -87,11 +84,11 @@ class AddrCache(object):
except: # unparsable timestamp - likely garbage except: # unparsable timestamp - likely garbage
changed = True changed = True
continue continue
except: # manual entry (no timestamp) except: # manual entry (no timestamp)
cache[ln.strip().lower()] = (now,None) cache[ln.strip().lower()] = (now,None)
wfp.write(ln) wfp.write(ln)
if changed: if changed:
lock.commit(self.fname+'.old') lock.commit(self.fname+'.old')
else: else:
lock.unlock() lock.unlock()
except IOError: except IOError:
@@ -129,13 +126,13 @@ class AddrCache(object):
ts,res = self.cache[lsender] ts,res = self.cache[lsender]
too_old = time.time() - self.age*24*60*60 # max age in days too_old = time.time() - self.age*24*60*60 # max age in days
if not ts or ts > too_old: if not ts or ts > too_old:
return res return res
del self.cache[lsender] del self.cache[lsender]
raise KeyError, sender raise KeyError, sender
except KeyError,x: except KeyError,x:
try: try:
user,host = sender.split('@',1) user,host = sender.split('@',1)
return self.__getitem__(host) return self.__getitem__(host)
except ValueError: except ValueError:
raise x raise x
+13 -15
View File
@@ -1,5 +1,4 @@
from ConfigParser import ConfigParser from ConfigParser import ConfigParser
import os.path
class MilterConfigParser(ConfigParser): class MilterConfigParser(ConfigParser):
@@ -21,7 +20,7 @@ class MilterConfigParser(ConfigParser):
return [q.strip() for q in self.get(sect,opt).split(',')] return [q.strip() for q in self.get(sect,opt).split(',')]
return [] return []
def getaddrset(self,sect,opt,dir=''): def getaddrset(self,sect,opt):
if not self.has_option(sect,opt): if not self.has_option(sect,opt):
return {} return {}
s = self.get(sect,opt) s = self.get(sect,opt)
@@ -30,14 +29,13 @@ class MilterConfigParser(ConfigParser):
q = q.strip() q = q.strip()
if q.startswith('file:'): if q.startswith('file:'):
domain = q[5:].lower() domain = q[5:].lower()
fname = os.path.join(dir,domain) d[domain] = d.setdefault(domain,[]) + open(domain,'r').read().split()
d[domain] = d.setdefault(domain,[]) + open(fname,'r').read().split()
else: else:
user,domain = q.split('@') user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user) d.setdefault(domain.lower(),[]).append(user)
return d return d
def getaddrdict(self,sect,opt,dir=''): def getaddrdict(self,sect,opt):
if not self.has_option(sect,opt): if not self.has_option(sect,opt):
return {} return {}
d = {} d = {}
@@ -45,14 +43,14 @@ class MilterConfigParser(ConfigParser):
q = q.strip() q = q.strip()
if self.has_option(sect,q): if self.has_option(sect,q):
l = self.get(sect,q) l = self.get(sect,q)
for addr in l.split(','): for addr in l.split(','):
addr = addr.strip() addr = addr.strip()
if addr.startswith('file:'): if addr.startswith('file:'):
fname = os.path.join(dir,addr[5:]) fname = addr[5:]
for a in open(fname,'r').read().split(): for a in open(fname,'r').read().split():
d[a] = q d[a] = q
else: else:
d[addr] = q d[addr] = q
return d return d
def getdefault(self,sect,opt,default=None): def getdefault(self,sect,opt,default=None):
+20 -53
View File
@@ -1,22 +1,12 @@
## @package Milter.dns # provide a higher level interface to pydns
# Provide a higher level interface to pydns.
import DNS import DNS
from DNS import DNSError from DNS import DNSError
MAX_CNAME = 10 MAX_CNAME = 10
## Lookup DNS records by label and RR type.
# The response can include records of other types that the DNS
# server thinks we might need.
# @param name the DNS label to lookup
# @param qtype the name of the DNS RR type to lookup
# @return a list of ((name,type),data) tuples
def DNSLookup(name, qtype): def DNSLookup(name, qtype):
try: try:
# To be thread safe, we create a fresh DnsRequest with
# each call. It would be more efficient to reuse
# a req object stored in a Session.
req = DNS.DnsRequest(name, qtype=qtype) req = DNS.DnsRequest(name, qtype=qtype)
resp = req.req() resp = req.req()
#resp.show() #resp.show()
@@ -34,28 +24,25 @@ class Session(object):
def __init__(self): def __init__(self):
self.cache = {} self.cache = {}
## Additional DNS RRs we can safely cache.
# We have to be careful which additional DNS RRs we cache. For # We have to be careful which additional DNS RRs we cache. For
# instance, PTR records are controlled by the connecting IP, and they # instance, PTR records are controlled by the connecting IP, and they
# could poison our local cache with bogus A and MX records. # could poison our local cache with bogus A and MX records.
# Each entry is a tuple of (query_type,rr_type). So for instance,
# the entry ('MX','A') says it is safe (for milter purposes) to cache
# any 'A' RRs found in an 'MX' query.
SAFE2CACHE = frozenset((
('MX','MX'), ('MX','A'),
('CNAME','CNAME'), ('CNAME','A'),
('A','A'),
('AAAA','AAAA'),
('PTR','PTR'),
('NS','NS'), ('NS','A'),
('TXT','TXT'),
('SPF','SPF')
))
## Cached DNS lookup. SAFE2CACHE = {
# @param name the DNS label to query ('MX','A'): None,
# @param qtype the query type, e.g. 'A' ('MX','MX'): None,
# @param cnames tracks CNAMES already followed in recursive calls ('CNAME','A'): None,
('CNAME','CNAME'): None,
('A','A'): None,
('AAAA','AAAA'): None,
('PTR','PTR'): None,
('NS','NS'): None,
('NS','A'): None,
('TXT','TXT'): None,
('SPF','SPF'): None
}
def dns(self, name, qtype, cnames=None): def dns(self, name, qtype, cnames=None):
"""DNS query. """DNS query.
@@ -70,23 +57,15 @@ 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)
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 cname: if not result:
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 == cnamek: if k == (name, 'CNAME'):
cname = v cname = v
if k[1] == 'CNAME' or (qtype,k[1]) in safe2cache: if (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:
@@ -97,22 +76,10 @@ class Session(object):
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 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__':
+15 -80
View File
@@ -5,24 +5,6 @@
# Send DSNs, do call back verification, # Send DSNs, do call back verification,
# and generate DSN messages from a template # and generate DSN messages from a template
# $Log$ # $Log$
# Revision 1.22 2011/03/18 20:41:31 customdesigned
# Python2.6 SMTP.close() fails when instance never connected.
#
# Revision 1.21 2011/03/03 05:11:58 customdesigned
# Release 0.9.4
#
# Revision 1.20 2010/10/11 00:29:47 customdesigned
# Handle multiple recipients. For CBV or auto whitelist of multiple emails.
#
# Revision 1.19 2009/07/02 19:41:12 customdesigned
# Handle @ in localpart.
#
# Revision 1.18 2009/06/10 18:01:59 customdesigned
# Doxygen updates
#
# Revision 1.17 2009/05/20 20:08:44 customdesigned
# Support non-DSN CBV (non-empty MAIL FROM)
#
# Revision 1.16 2007/09/25 01:24:59 customdesigned # Revision 1.16 2007/09/25 01:24:59 customdesigned
# Allow arbitrary object, not just spf.query like, to provide data for create_msg # Allow arbitrary object, not just spf.query like, to provide data for create_msg
# #
@@ -44,31 +26,7 @@
# Revision 1.10 2006/05/24 20:56:35 customdesigned # Revision 1.10 2006/05/24 20:56:35 customdesigned
# Remove default templates. Scrub test. # Remove default templates. Scrub test.
# #
## @package Milter.dsn
# Support DSNs and CallBackValidations (CBV).
#
# A Delivery Status Notification (bounce) is sent to the envelope
# sender (original MAIL FROM) with a null MAIL FROM (<>) to notify the
# original sender # of delays or problems with delivery. A Callback Validation
# starts the DSN process, but stops before issuing the DATA command. The
# purpose is to check whether the envelope recipient is accepted (and is
# therefore a valid email). The null MAIL FROM tells the remote
# MTA to never reply according to RFC2821 (but some braindead MTAs
# reply anyway, of course).
#
# Milters should cache CBV results and should avoid sending DSNs
# unless the sender is authenticated somehow (e.g. SPF Pass). However,
# when email is quarantined, and is not known to be a forgery, sending a DSN
# is better than silently disappearing, and a DSN is better than sending
# a normal message as notification - because MAIL FROM signing schemes
# can reject bounces of forged emails. Whatever you do, don't copy those
# assinine commercial filters that send a normal message to notify you
# that some virus is forging your email.
#
# <b>DSNs should *only* be sent to MAIL FROM addresses.</b> Never send
# a DSN or use a null MAIL FROM with an email address obtained from
# anywhere else.
#
import smtplib import smtplib
import socket import socket
from email.Message import Message from email.Message import Message
@@ -76,25 +34,12 @@ import Milter
import time import time
import dns import dns
## Send DSN.
# Try the published MX names in order, rejecting obviously bogus entries
# (like <code>localhost</code>).
# @param mailfrom the original sender we are notifying or validating
# @param receiver the HELO name of the MTA we are sending the DSN on behalf of.
# Be sure to send from an IP that matches the HELO.
# @param msg the DSN message in RFC2822 format, or None for CBV.
# @param timeout total seconds to wait for a response from an MX
# @param session Milter.dns.Session object from current incoming mail
# session to reuse its cache, or None to create a fresh one.
# @param ourfrom set to a valid email to send a normal notification from, or
# to validate emails not obtained from MAIL FROM.
# @return None on success or (status_code,msg) on failure.
def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''): def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
"""Send DSN. If msg is None, do callback verification. """Send DSN. If msg is None, do callback verification.
Mailfrom is original sender we are sending DSN or CBV to. Mailfrom is original sender we are sending DSN or CBV to.
Receiver is the MTA sending the DSN. Receiver is the MTA sending the DSN.
Return None for success or (code,msg) for failure.""" Return None for success or (code,msg) for failure."""
user,domain = mailfrom.rsplit('@',1) user,domain = mailfrom.split('@')
if not session: session = dns.Session() if not session: session = dns.Session()
try: try:
mxlist = session.dns(domain,'MX') mxlist = session.dns(domain,'MX')
@@ -117,34 +62,24 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
if a[0] == receiver: if a[0] == receiver:
return (553,'Fraudulent MX for %s: %s' % (domain,host)) return (553,'Fraudulent MX for %s: %s' % (domain,host))
if not (200 <= code <= 299): if not (200 <= code <= 299):
raise smtplib.SMTPHeloError(code, resp) raise smtplib.SMTPHeloError(code, resp)
if msg: if msg:
try: try:
smtp.sendmail('<%s>'%ourfrom,mailfrom,msg) smtp.sendmail('<%s>'%ourfrom,mailfrom,msg)
except smtplib.SMTPSenderRefused: except smtplib.SMTPSenderRefused:
# does not accept DSN, try postmaster (at the risk of mail loops) # does not accept DSN, try postmaster (at the risk of mail loops)
smtp.sendmail('<postmaster@%s>'%receiver,mailfrom,msg) smtp.sendmail('<postmaster@%s>'%receiver,mailfrom,msg)
else: # CBV else: # CBV
code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom) code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom)
if code != 250: if code != 250:
raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom) raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom)
if isinstance(mailfrom,basestring): code,resp = smtp.rcpt(mailfrom)
mailfrom = [mailfrom] if code not in (250,251):
badrcpts = {} return (code,resp) # permanent error
for rcpt in mailfrom: smtp.quit()
code,resp = smtp.rcpt(rcpt)
if code not in (250,251):
badrcpts[rcpt] = (code,resp)# permanent error
smtp.quit()
if len(badrcpts) == 1:
return badrcpts.values()[0] # permanent error
if badrcpts:
return badrcpts
return None # success return None # success
except smtplib.SMTPRecipientsRefused,x: except smtplib.SMTPRecipientsRefused,x:
if len(x.recipients) == 1: return x.recipients[mailfrom] # permanent error
return x.recipients.values()[0] # permanent error
return x.recipients
except smtplib.SMTPSenderRefused,x: except smtplib.SMTPSenderRefused,x:
return x.args[:2] # does not accept DSN return x.args[:2] # does not accept DSN
except smtplib.SMTPDataError,x: except smtplib.SMTPDataError,x:
@@ -155,7 +90,7 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
pass # MX didn't accept connections, try next one pass # MX didn't accept connections, try next one
except socket.timeout: except socket.timeout:
pass # MX too slow, try next one pass # MX too slow, try next one
if hasattr(smtp,'sock'): smtp.close() smtp.close()
if time.time() > toolate: if time.time() > toolate:
return (450,'No MX response within %f minutes'%(timeout/60.0)) return (450,'No MX response within %f minutes'%(timeout/60.0))
return (450,'No MX servers available') # temp error return (450,'No MX servers available') # temp error
+4 -5
View File
@@ -48,10 +48,9 @@ def is_dynip(host,addr):
True True
""" """
if host.startswith('[') and host.endswith(']'): if host.startswith('[') and host.endswith(']'):
return True # no ptr return True
if addr: if addr:
if host.find(addr) >= 0: return True if host.find(addr) >= 0: return True
if addr.find(':') >= 0: return False # IP6
a = addr.split('.') a = addr.split('.')
ia = map(int,a) ia = map(int,a)
h = host h = host
@@ -68,8 +67,8 @@ def is_dynip(host,addr):
if ia[2:] in (g[:2],g[-2:]): return True if ia[2:] in (g[:2],g[-2:]): return True
for m in ip3.finditer(host): for m in ip3.finditer(host):
if int(m.group()) == ia[3]: if int(m.group()) == ia[3]:
h = host[:m.start()] + '<3>' + host[m.end():] h = host[:m.start()] + '<3>' + host[m.end():]
break break
if rehmac.search(h): return True if rehmac.search(h): return True
if host.find(''.join(a[:3])) >= 0: return True if host.find(''.join(a[:3])) >= 0: return True
if host.find(''.join(a[1:])) >= 0: return True if host.find(''.join(a[1:])) >= 0: return True
@@ -86,7 +85,7 @@ if __name__ == '__main__':
if a[3:5] == ['connect','from']: if a[3:5] == ['connect','from']:
host = a[5] host = a[5]
if host.startswith('[') and host.endswith(']'): if host.startswith('[') and host.endswith(']'):
continue # no PTR continue # no PTR
ip = a[7][2:-2] ip = a[7][2:-2]
if ip in seen: continue if ip in seen: continue
seen.add(ip) seen.add(ip)
+9 -55
View File
@@ -18,19 +18,13 @@ def quoteAddress(s):
class Record(object): class Record(object):
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' ) __slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
def __init__(self,timeinc=0): def __init__(self):
now = time.time() + timeinc now = time.time()
self.firstseen = now self.firstseen = now
self.lastseen = now self.lastseen = now
self.cnt = 0 self.cnt = 0
self.umis = None self.umis = None
def __str__(self):
return "Grey[%s:%s:%s:%d]" % (
time.ctime(self.firstseen),time.ctime(self.lastseen),
self.umis,self.cnt
)
class Greylist(object): class Greylist(object):
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36): def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
@@ -40,38 +34,8 @@ class Greylist(object):
self.greylist_retain = grey_retain * 24 * 3600 # days self.greylist_retain = grey_retain * 24 * 3600 # days
self.dbp = shelve.open(dbname,'c',protocol=2) self.dbp = shelve.open(dbname,'c',protocol=2)
self.lock = thread.allocate_lock() self.lock = thread.allocate_lock()
def export_csv(self,fp,timeinc=0):
"Export records to csv."
import csv
dbp = self.dbp
w = csv.writer(fp)
now = time.time() + timeinc
for key, r in dbp.iteritems():
if now > r.lastseen + self.greylist_retain: continue
ip,sender,recipient = key.rsplit(':',2)
w.writerow([ip,sender,recipient,r.firstseen,r.lastseen,r.cnt,r.umis])
def clean(self,timeinc=0):
"Delete records past the retention limit."
now = time.time() + timeinc
cnt = 0
dbp = self.dbp
for key, r in dbp.iteritems():
#print key,r,time.ctime(now)
if now > r.lastseen + self.greylist_retain:
self.lock.acquire()
try:
r = dbp[key]
now = time.time() + timeinc
if now > r.lastseen + self.greylist_retain:
del dbp[key]
cnt += 1
finally:
self.lock.release()
return cnt
def check(self,ip,sender,recipient,timeinc=0): def check(self,ip,sender,recipient):
"Return number of allowed messages for greylist triple." "Return number of allowed messages for greylist triple."
sender = quoteAddress(sender) sender = quoteAddress(sender)
recipient = quoteAddress(recipient) recipient = quoteAddress(recipient)
@@ -81,15 +45,15 @@ class Greylist(object):
dbp = self.dbp dbp = self.dbp
try: try:
r = dbp[key] r = dbp[key]
now = time.time() + timeinc now = time.time()
if now > r.lastseen + self.greylist_retain: if now > r.lastseen + self.greylist_retain:
# expired # expired
log.debug('Expired greylist: %s',key) log.debug('Expired greylist: %s',key)
r = Record(timeinc) r = Record()
elif now < r.firstseen + self.greylist_time + 5: elif now < r.firstseen + self.greylist_time:
# still greylisted # still greylisted
log.debug('Early greylist: %s',key) log.debug('Early greylist: %s',key)
#r = Record(timeinc) #r = Record()
r.lastseen = now r.lastseen = now
elif r.cnt or now < r.firstseen + self.greylist_expire: elif r.cnt or now < r.firstseen + self.greylist_expire:
# in greylist window or active # in greylist window or active
@@ -99,22 +63,12 @@ class Greylist(object):
else: else:
# passed greylist window # passed greylist window
log.debug('Late greylist: %s',key) log.debug('Late greylist: %s',key)
r = Record(timeinc) r = Record()
dbp[key] = r dbp[key] = r
except: except:
r = Record(timeinc) r = Record()
dbp[key] = r dbp[key] = r
dbp.sync() dbp.sync()
finally: finally:
self.lock.release() self.lock.release()
return r.cnt return r.cnt
def close(self):
self.dbp.close()
if __name__ == '__main__':
import sys
g = Greylist(sys.argv[1],5,24,36)
try:
g.export_csv(sys.stdout)
finally: g.close()
-106
View File
@@ -1,106 +0,0 @@
import time
import logging
import urllib
import sqlite3
import thread
from datetime import datetime
log = logging.getLogger('milter.greylist')
_db_lock = thread.allocate_lock()
class Greylist(object):
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
self.ignoreLastByte = False
self.greylist_time = grey_time * 60 # minutes
self.greylist_expire = grey_expire * 3600 # hours
self.greylist_retain = grey_retain * 24 * 3600 # days
self.conn = sqlite3.connect(dbname)
self.conn.row_factory = sqlite3.Row
try:
self.conn.execute('''create table greylist(
ip text , sender text, recipient text,
firstseen timestamp, lastseen timestamp, cnt integer, umis text,
primary key (ip,sender,recipient))''')
except: pass
def import_csv(self,fp):
import csv
rdr = csv.reader(fp)
cur = self.conn.execute('begin immediate')
try:
for r in rdr:
cur.execute('''insert into
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
values(?,?,?,?,?,?,?)''', r)
self.conn.commit()
finally:
cur.close();
def clean(self,timeinc=0):
"Delete records past the retention limit."
now = time.time() + timeinc - self.greylist_retain
cur = self.conn.cursor()
try:
cur.execute('delete from greylist where lastseen < ?',(now,))
cnt = cur.rowcount
self.conn.commit()
finally: cur.close()
return cnt
def check(self,ip,sender,recipient,timeinc=0):
"Return number of allowed messages for greylist triple."
_db_lock.acquire()
cur = self.conn.execute('begin immediate')
try:
cur.execute('''select firstseen,lastseen,cnt,umis from greylist where
ip=? and sender=? and recipient=?''',(ip,sender,recipient))
r = cur.fetchone()
now = time.time() + timeinc
cnt = 0
if not r:
cur.execute('''insert into
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
values(?,?,?,?,?,?,?)''', (ip,sender,recipient,now,now,0,None))
elif now > r['lastseen'] + self.greylist_retain:
# expired
log.debug('Expired greylist: %s:%s:%s',ip,sender,recipient)
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
where ip=? and sender=? and recipient=?''',
(now,now,0,None,ip,sender,recipient))
elif now < r['firstseen'] + self.greylist_time + 5:
# still greylisted
log.debug('Early greylist: %s:%s:%s',ip,sender,recipient)
#r = Record()
cur.execute('''update greylist set lastseen=?
where ip=? and sender=? and recipient=?''',
(now,ip,sender,recipient))
elif r['cnt'] or now < r['firstseen'] + self.greylist_expire:
# in greylist window or active
cnt = r['cnt'] + 1
cur.execute('''update greylist set lastseen=?,cnt=?
where ip=? and sender=? and recipient=?''',
(now,cnt,ip,sender,recipient))
log.debug('Active greylist(%d): %s:%s:%s',cnt,ip,sender,recipient)
else:
# passed greylist window
log.debug('Late greylist: %s:%s:%s',ip,sender,recipient)
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
where ip=? and sender=? and recipient=?''',
(now,now,0,None,ip,sender,recipient))
self.conn.commit()
finally:
cur.close()
_db_lock.release()
return cnt
def close(self):
self.conn.close()
if __name__ == '__main__':
import sys
g = Greylist(sys.argv[1])
try:
g.import_csv(sys.stdin)
finally: g.close()
+3 -3
View File
@@ -31,8 +31,8 @@ class PLock(object):
os.chown(self.lockname,-1,st.st_gid) os.chown(self.lockname,-1,st.st_gid)
except: except:
if strict_perms: if strict_perms:
self.unlock() self.unlock()
raise raise
return self.fp return self.fp
def wlock(self,lockname=None): def wlock(self,lockname=None):
@@ -51,7 +51,7 @@ class PLock(object):
self.fp = None self.fp = None
if backname: if backname:
try: try:
os.remove(backname) os.remove(backname)
except OSError: pass except OSError: pass
os.link(self.basename,backname) os.link(self.basename,backname)
os.rename(self.lockname,self.basename) os.rename(self.lockname,self.basename)
-117
View File
@@ -1,117 +0,0 @@
"""Pure Python IP6 parsing and formatting
Copyright (c) 2006 Stuart Gathman <stuart@bmsi.com>
This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form.
"""
import struct
#from spf import RE_IP4
import re
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
RE_IP4 = re.compile(PAT_IP4+'$')
def inet_ntop(s):
"""
Convert ip6 address to standard hex notation.
Examples:
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0xFFFF,0x0102,0x0304))
'::FFFF:1.2.3.4'
>>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0,0,0,0x0102,0x0304))
'1234:5678::102:304'
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0x1234,0x5678,0,0x0102,0x0304))
'::1234:5678:0:102:304'
>>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0x0102,0x0304,0,0,0))
'1234:5678:0:102:304::'
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0,0,0))
'::'
"""
# convert to 8 words
a = struct.unpack("!HHHHHHHH",s)
n = (0,0,0,0,0,0,0,0) # null ip6
if a == n: return '::'
# check for ip4 mapped
if a[:5] == (0,0,0,0,0) and a[5] in (0,0xFFFF):
ip4 = '.'.join([str(i) for i in struct.unpack("!BBBB",s[12:])])
if a[5]:
return "::FFFF:" + ip4
return "::" + ip4
# find index of longest sequence of 0
for l in (7,6,5,4,3,2,1):
e = n[:l]
for i in range(9-l):
if a[i:i+l] == e:
if i == 0:
return ':'+':%x'*(8-l) % a[l:]
if i == 8 - l:
return '%x:'*(8-l) % a[:-l] + ':'
return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:]
return "%x:%x:%x:%x:%x:%x:%x:%x" % a
def inet_pton(p):
"""
Convert ip6 standard hex notation to ip6 address.
Examples:
>>> struct.unpack('!HHHHHHHH',inet_pton('::'))
(0, 0, 0, 0, 0, 0, 0, 0)
>>> struct.unpack('!HHHHHHHH',inet_pton('::1234'))
(0, 0, 0, 0, 0, 0, 0, 4660)
>>> struct.unpack('!HHHHHHHH',inet_pton('1234::'))
(4660, 0, 0, 0, 0, 0, 0, 0)
>>> struct.unpack('!HHHHHHHH',inet_pton('1234::5678'))
(4660, 0, 0, 0, 0, 0, 0, 22136)
>>> struct.unpack('!HHHHHHHH',inet_pton('::FFFF:1.2.3.4'))
(0, 0, 0, 0, 0, 65535, 258, 772)
>>> struct.unpack('!HHHHHHHH',inet_pton('1.2.3.4'))
(0, 0, 0, 0, 0, 65535, 258, 772)
>>> try: inet_pton('::1.2.3.4.5')
... except ValueError,x: print x
::1.2.3.4.5
"""
if p == '::':
return '\0'*16
s = p
m = RE_IP4.search(s)
try:
if m:
pos = m.start()
ip4 = [int(i) for i in s[pos:].split('.')]
if not pos:
return struct.pack('!QLBBBB',0,65535,*ip4)
s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4)
a = s.split('::')
if len(a) == 2:
l,r = a
if not l:
r = r.split(':')
return struct.pack('!HHHHHHHH',
*[0]*(8-len(r)) + [int(s,16) for s in r])
if not r:
l = l.split(':')
return struct.pack('!HHHHHHHH',
*[int(s,16) for s in l] + [0]*(8-len(l)))
l = l.split(':')
r = r.split(':')
return struct.pack('!HHHHHHHH',
*[int(s,16) for s in l] + [0]*(8-len(l)-len(r))
+ [int(s,16) for s in r])
if len(a) == 1:
return struct.pack('!HHHHHHHH',
*[int(s,16) for s in a[0].split(':')])
except ValueError: pass
raise ValueError,p
-192
View File
@@ -1,192 +0,0 @@
## @package Milter.test
# A test framework for milters
import rfc822
import StringIO
import Milter
Milter.NOREPLY = Milter.CONTINUE
## Test mixin for unit testing %milter applications.
# This mixin overrides many Milter.MilterBase methods
# with stub versions that simply record what was done.
# @since 0.9.8
class TestBase(object):
def __init__(self,logfile='test/milter.log'):
self._protocol = 0
self.logfp = open(logfile,"a")
## List of recipients deleted
self._delrcpt = []
## List of recipients added
self._addrcpt = []
## Macros defined
self._macros = { }
## The message body.
self._body = None
## True if the %milter replaced the message body.
self._bodyreplaced = False
## True if the %milter changed any headers.
self._headerschanged = False
## Reply codes and messages set by the %milter
self._reply = None
## The rfc822 message object for the current email being fed to the %milter.
self._msg = None
self._symlist = [ None, None, None, None, None, None, None ]
def log(self,*msg):
for i in msg: print >>self.logfp, i,
print >>self.logfp
## Set a macro value.
# These are retrieved by the %milter with getsymval.
# @param name the macro name, as passed to getsymval
# @param val the macro value
def setsymval(self,name,val):
self._macros[name] = val
def getsymval(self,name):
# FIXME: track stage, and use _symlist
return self._macros.get(name,'')
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self._bodyreplaced = True
else:
raise IOError,"replacebody not called from eom()"
# FIXME: rfc822 indexing does not really reflect the way chg/add header
# work for a %milter
def chgheader(self,field,idx,value):
if not self._body:
raise IOError,"chgheader not called from eom()"
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
if value == '':
del self._msg[field]
else:
self._msg[field] = value
self._headerschanged = True
def addheader(self,field,value,idx=-1):
if not self._body:
raise IOError,"addheader not called from eom()"
self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value
self._headerschanged = True
def delrcpt(self,rcpt):
if not self._body:
raise IOError,"delrcpt not called from eom()"
self._delrcpt.append(rcpt)
def addrcpt(self,rcpt):
if not self._body:
raise IOError,"addrcpt not called from eom()"
self._addrcpt.append(rcpt)
## Save the reply codes and messages in self._reply.
def setreply(self,rcode,xcode,*msg):
self._reply = (rcode,xcode) + msg
def setsymlist(self,stage,macros):
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
# not used yet, but just for grins we save the data
a = []
for m in macros:
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(' ')
except: pass
a += m
self._symlist[stage] = set(a)
## Feed a file like object to the %milter. Calls envfrom, envrcpt for
# each recipient, header for each header field, body for each body
# block, and finally eom. A return code from the %milter other than
# CONTINUE returns immediately with that return code.
#
# This is a convenience method, a test could invoke the callbacks
# in sequence on its own - and for some complex tests, this may
# be necessary.
# @param fp the file with rfc2822 message stream
# @param sender the MAIL FROM
# @param rcpt RCPT TO - additional recipients may follow
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
self._body = None
self._bodyreplaced = False
self._headerschanged = False
self._reply = None
msg = rfc822.Message(fp)
rc = self.envfrom('<%s>'%sender)
if rc != Milter.CONTINUE: return rc
for rcpt in (rcpt,) + rcpts:
rc = self.envrcpt('<%s>'%rcpt)
if rc != Milter.CONTINUE: return rc
line = None
for h in msg.headers:
if h[:1].isspace():
line = line + h
continue
if not line:
line = h
continue
s = line.split(': ',1)
if len(s) > 1: val = s[1].strip()
else: val = ''
rc = self.header(s[0],val)
if rc != Milter.CONTINUE: return rc
line = h
if line:
s = line.split(': ',1)
rc = self.header(s[0],s[1])
if rc != Milter.CONTINUE: return rc
rc = self.eoh()
if rc != Milter.CONTINUE: return rc
while 1:
buf = fp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = StringIO.StringIO()
rc = self.eom()
if self._bodyreplaced:
body = self._body.getvalue()
else:
msg.rewindbody()
body = msg.fp.read()
self._body = StringIO.StringIO()
self._body.writelines(msg.headers)
self._body.write('\n')
self._body.write(body)
return rc
## Feed an email contained in a file to the %milter.
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
# @param sender MAIL FROM
# @param rcpts RCPT TO, multiple recipients may be supplied
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
with open('test/'+fname,'r') as fp:
return self.feedFile(fp,sender,*rcpts)
## Call the connect and helo callbacks.
# The helo callback is not called if connect does not return CONTINUE.
# @param host the hostname passed to the connect callback
# @param helo the hostname passed to the helo callback
# @param ip the IP address passed to the connect callback
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
self._body = None
self._bodyreplaced = False
opts = [ Milter.CURR_ACTS,~0,0,0 ]
rc = self.negotiate(opts)
rc = super(TestBase,self).connect(host,1,(ip,1234))
if rc != Milter.CONTINUE:
self.close()
return rc
rc = self.hello(helo)
if rc != Milter.CONTINUE:
self.close()
return rc
+14 -91
View File
@@ -1,7 +1,3 @@
## @package Milter.utils
# Miscellaneous functions.
#
import re import re
import struct import struct
import socket import socket
@@ -11,53 +7,17 @@ from email.Header import decode_header
#import email.Utils #import email.Utils
import rfc822 import rfc822
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4) ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$')
ip4re = re.compile(PAT_IP4+'$')
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
'|::(?:%(hex4)s:){5}%(ls32)s$'
'|(?:%(hex4)s)?::(?:%(hex4)s:){4}%(ls32)s$'
'|(?:(?:%(hex4)s:){0,1}%(hex4)s)?::(?:%(hex4)s:){3}%(ls32)s$'
'|(?:(?:%(hex4)s:){0,2}%(hex4)s)?::(?:%(hex4)s:){2}%(ls32)s$'
'|(?:(?:%(hex4)s:){0,3}%(hex4)s)?::%(hex4)s:%(ls32)s$'
'|(?:(?:%(hex4)s:){0,4}%(hex4)s)?::%(ls32)s$'
'|(?:(?:%(hex4)s:){0,5}%(hex4)s)?::%(hex4)s$'
'|(?:(?:%(hex4)s:){0,6}%(hex4)s)?::$'
% {
'ls32': r'(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|%s)'%PAT_IP4,
'hex4': r'[0-9a-f]{1,4}'
}, re.IGNORECASE)
# from spf.py # from spf.py
def addr2bin(s): def addr2bin(str):
"""Convert a string IPv4 address into an unsigned integer.""" "Convert a string IPv4 address into an unsigned integer."
if s.find(':') >= 0: return struct.unpack("!L", socket.inet_aton(str))[0]
try:
return bin2long6(inet_pton(s))
except:
raise socket.error("Invalid IP6 address: "+s)
try:
return struct.unpack("!L", socket.inet_aton(s))[0]
except socket.error:
raise socket.error("Invalid IP4 address: "+s)
def bin2long6(s):
"""Convert binary IP6 address into an unsigned Python long integer."""
h, l = struct.unpack("!QQ", s)
return h << 64 | l
if hasattr(socket,'has_ipv6') and socket.has_ipv6:
def inet_ntop(s):
return socket.inet_ntop(socket.AF_INET6,s)
def inet_pton(s):
return socket.inet_pton(socket.AF_INET6,s.strip())
else:
from pyip6 import inet_ntop, inet_pton
MASK = 0xFFFFFFFFL MASK = 0xFFFFFFFFL
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
def cidr(i,n,mask=MASK): def cidr(i,n):
return ~(mask >> n) & mask & i return ~(MASK >> n) & MASK & i
def iniplist(ipaddr,iplist): def iniplist(ipaddr,iplist):
"""Return whether ip is in cidr list """Return whether ip is in cidr list
@@ -67,43 +27,21 @@ 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('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
True
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
Traceback (most recent call last):
...
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
""" """
if ip4re.match(ipaddr): ipnum = addr2bin(ipaddr)
ipnum = addr2bin(ipaddr)
elif ip6re.match(ipaddr):
ipnum = bin2long6(inet_pton(ipaddr))
else:
raise ValueError('Invalid ip syntax:'+ipaddr)
for pat in iplist: for pat in iplist:
p = pat.split('/',1) p = pat.split('/',1)
if ip4re.match(p[0]): if ip4re.match(p[0]):
if len(p) > 1: if len(p) > 1:
n = int(p[1]) n = int(p[1])
else: else:
n = 32 n = 32
if cidr(addr2bin(p[0]),n) == cidr(ipnum,n): if cidr(addr2bin(p[0]),n) == cidr(ipnum,n):
return True return True
elif ip6re.match(p[0]):
if len(p) > 1:
n = int(p[1])
else:
n = 128
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
return True
elif fnmatchcase(ipaddr,pat): elif fnmatchcase(ipaddr,pat):
return True return True
return False return False
## Split email into Fullname and address.
# This replaces <code>email.Utils.parseaddr</code> but fixes
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
#
def parseaddr(t): def parseaddr(t):
"""Split email into Fullname and address. """Split email into Fullname and address.
@@ -153,27 +91,13 @@ def parse_addr(t):
['user@bar', 'example.com'] ['user@bar', 'example.com']
>>> parse_addr('foo') >>> parse_addr('foo')
['foo'] ['foo']
>>> parse_addr('@mx.example.com:user@example.com')
['user', 'example.com']
>>> parse_addr('@user@example.com')
['@user', 'example.com']
""" """
if t.startswith('<') and t.endswith('>'): t = t[1:-1] if t.startswith('<') and t.endswith('>'): t = t[1:-1]
if t.startswith('"'): if t.startswith('"'):
if t.endswith('"'): return [t[1:-1]] if t.endswith('"'): return [t[1:-1]]
pos = t.find('"@') pos = t.find('"@')
if pos > 0: return [t[1:pos],t[pos+2:]] if pos > 0: return [t[1:pos],t[pos+2:]]
if t.startswith('@'): return t.split('@')
try: t = t.split(':',1)[1]
except IndexError: pass
return t.rsplit('@',1)
## Decode headers gratuitously encoded to hide the content.
# Spammers often encode headers to obscure the content from
# spam filters. This function decodes gratuitously encoded
# headers.
# @param val the raw header value
# @return the decoded value or the original raw value
def parse_header(val): def parse_header(val):
"""Decode headers gratuitously encoded to hide the content. """Decode headers gratuitously encoded to hide the content.
@@ -185,18 +109,17 @@ def parse_header(val):
for s,enc in h: for s,enc in h:
if enc: if enc:
try: try:
u.append(unicode(s,enc,'replace')) u.append(unicode(s,enc))
except LookupError: except LookupError:
u.append(unicode(s)) u.append(unicode(s))
else: else:
u.append(unicode(s)) u.append(unicode(s))
u = ''.join(u) u = ''.join(u)
for enc in ('us-ascii','iso-8859-1','utf8'): for enc in ('us-ascii','iso-8859-1','utf8'):
try: try:
return u.encode(enc) return u.encode(enc)
except UnicodeError: continue except UnicodeError: continue
except UnicodeDecodeError: pass except UnicodeDecodeError: pass
except LookupError: pass except LookupError: pass
except ValueError: pass
except email.Errors.HeaderParseError: pass except email.Errors.HeaderParseError: pass
return val return val
+5 -2
View File
@@ -69,7 +69,8 @@ Not-so-quick Installation
First install Sendmail. Make sure you read libmilter/README in the Sendmail First install Sendmail. Make sure you read libmilter/README in the Sendmail
source directory, and make sure you enable libmilter before you build. The source directory, and make sure you enable libmilter before you build. The
8.11 series had libmilter marked as FFR (For Future Release); 8.12 8.11 series had libmilter marked as FFR (For Future Release); 8.12
officially supports libmilter, but it's still not built by default. officially
supports libmilter, but it's still not built by default.
Install Python, and enable threading in Modules/Setup. Install Python, and enable threading in Modules/Setup.
@@ -135,6 +136,8 @@ http://www.bmsi.com/linux/sendmail-rh72.spec
IPv6 Notes IPv6 Notes
---------- ----------
IPv6 is still experimental.
The IPv6 protocol is supported if your operation system supports it The IPv6 protocol is supported if your operation system supports it
and if sendmail was compiled with IPv6 support. To determine if your and if sendmail was compiled with IPv6 support. To determine if your
sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6 sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6
@@ -192,7 +195,7 @@ Authors
------- -------
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that and dirty python to use it. Stuart D. Gathman (stuart@bmsi.com) took that
kludge and added threading and context objects to it, wrote a proper OO kludge and added threading and context objects to it, wrote a proper OO
wrapper (Milter.py) that handles attachments, did lots of testing, packaged wrapper (Milter.py) that handles attachments, did lots of testing, packaged
it with distutils, and generally transformed it from a quick hack to a it with distutils, and generally transformed it from a quick hack to a
+4
View File
@@ -1,2 +1,6 @@
Support smfi_negotiate and auto negotiate only those callbacks for which
Milter.Milter methods have been overridden. (Python should be able to
do that.)
Lookup exact RFC syntax of real name / email and make Lookup exact RFC syntax of real name / email and make
Milter.utils.parse_addr() pass all unit tests. Milter.utils.parse_addr() pass all unit tests.
-56
View File
@@ -1,56 +0,0 @@
## @mainpage Writing Milters in Python
#
# 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
# 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 include the initial connection from a MTA, the envelope sender and
# recipients, the top level mail headers, and the message body. There are
# options to mangle all of these components of the message as it passes through
# the %milter.
#
# At the next level, the <code>Milter</code> module (note the case difference)
# provides a Python friendly object oriented wrapper for the low level API. To
# use the Milter module, an application registers a 'factory' to create an
# object for each connection from a MTA to sendmail. These connection objects
# must provide methods corresponding to the libmilter event callbacks.
#
# Each callback method returns a code to tell sendmail whether to proceed with
# processing the message. This is a big advantage of milters over other mail
# filtering systems. Unwanted mail can be stopped in its tracks at the
# earliest possible point. The callback return codes are
# milter.CONTINUE, milter.REJECT, milter.DISCARD, milter.ACCEPT,
# milter.TEMPFAIL, milter.SKIP, milter.NOREPLY.
#
# The Milter.Base class provides default implementations for
# event methods that do nothing, and also provides wrappers for the libmilter
# methods to mutate the message. It automatically negotiates with MTA
# which protocol steps need to be processed by the %milter, based on
# which callback methods are overridden.
#
# The Milter.Milter class provides an alternate default
# implementation that logs the main milter callbacks, but otherwise does
# nothing. It is provided for compatibility.
#
# The mime module provides a wrapper for the Python email package
# that fixes some bugs, and simplifies modifying selected parts of a MIME
# message.
#
# @section threading
#
# The libmilter library which pymilter wraps
# <a href="https://www.milter.org/developers/overview#SignalHandling">handles
# 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
# <a href="https://www.milter.org/developers/api/smfi_stop">smfi_stop</a>
# and the last to an internal ABORT.
#
# If you use python threads or threading modules, then signal handling gets
# confused. Threads may still be useful, but you may need to provide an
# alternate means of causing graceful shutdown.
#
# You may find the
# <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html">
# multiprocessing</a> module useful. It can be a drop-in
# replacement for threading as illustrated in
# <a href="milter-template_8py-example.html">milter-template.py</a>.
-251
View File
@@ -1,251 +0,0 @@
# Document miltermodule for Doxygen
#
## @package milter
#
# A thin wrapper around libmilter. Most users will not import
# milter directly, but will instead import Milter and subclass
# Milter.Base. This module gives you ultimate low level control
# from python.
#
## Continue processing the current connection, message, or recipient.
CONTINUE = 0
## For a connection-oriented routine, reject this connection;
# call Milter.Base.close(). For a message-oriented routine, except
# Milter.Base.eom() or Milter.Base.abort(), reject this message. For a
# recipient-oriented routine, reject the current recipient (but continue
# processing the current message).
REJECT = 1
## For a message- or recipient-oriented routine, accept this message, but
# silently discard it. SMFIS_DISCARD should not be returned by a
# connection-oriented routine.
DISCARD = 2
## For a connection-oriented routine, accept this connection without further
# filter processing; call Milter.Base.close(). For a message- or
# recipient-oriented routine, accept this message without further filtering.
ACCEPT = 3
## Return a temporary failure, i.e., the corresponding SMTP command will return
# an appropriate 4xx status code. For a message-oriented routine, except
# Milter.Base.envfrom(), fail for this message. For a connection-oriented
# routine, fail for this connection; call Milter.Base.close(). For a recipient-oriented
# routine, only
# fail for the current recipient; continue message processing.
TEMPFAIL = 4
## Skip further callbacks of the same type in this transaction.
# Currently this return value is only allowed in Milter.Base.body(). It can be
# used if a %milter has received sufficiently many body chunks to make a
# decision, but still wants to invoke message modification functions that are
# only allowed to be called from Milter.Base.eom(). Note: the %milter must
# negotiate this behavior with the MTA, i.e., it must check whether the
# protocol action SMFIP_SKIP is available and if so, the %milter must request
# it.
SKIP = 5
## Do not send a reply back to the MTA.
# The %milter must negotiate this behavior with the MTA, i.e., it must check
# whether the appropriate protocol action P_NR_* is available and if so,
# the %milter must request it. If you set the P_NR_* protocol action for a
# callback, that callback must always reply with NOREPLY. Using any other
# reply code is a violation of the API. If in some cases your callback may
# return another value (e.g., due to some resource shortages), then you must
# not set P_NR_* and you must use CONTINUE as the default return
# code. (Alternatively you can try to delay reporting the problem to a later
# callback for which P_NR_* is not set.)
#
# This is negotiated and returned automatically by the Milter.noreply
# function decorator.
NOREPLY = 6
## Hold context for a %milter connection.
# Each connection to sendmail creates a new <code>SMFICTX</code> struct within
# libmilter. The milter module in turn creates a milterContext
# tied to the <code>SMFICTX</code> struct via <code>smfi_setpriv</code>
# to hold a PyThreadState and a user defined Python object for the connection.
#
# Most application interaction with libmilter takes places via
# the milterContext object for the connection. It is passed to
# callback functions as the first parameter.
#
# The <code>Milter</code> module creates a python class for each connection,
# and converts function callbacks to instance method invocations.
#
class milterContext(object):
## Calls <a href="https://www.milter.org/developers/api/smfi_getsymval">smfi_getsymval</a>.
def getsymval(self,sym): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_setreply">
# smfi_setreply</a> or
# <a href="https://www.milter.org/developers/api/smfi_setmlreply">
# smfi_setmlreply</a>.
# @param rcode SMTP response code
# @param xcode extended SMTP response code
# @param msg one or more message lines. If the MTA does not support
# multiline messages, only the first is used.
def setreply(self,rcode,xcode,*msg): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_addheader">smfi_addheader</a>.
def addheader(self,name,value,idx=-1): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">smfi_chgheader</a>.
def chgheader(self,name,idx,value): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">smfi_addrcpt</a>.
def addrcpt(self,rcpt,params=None): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">smfi_delrcpt</a>.
def delrcpt(self,rcpt): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">smfi_replacebody</a>.
def replacebody(self,data): pass
## Attach a Python object to this connection context.
# @return the old value or None
def setpriv(self,priv): pass
## Return the Python object attached to this connection context.
def getpriv(self): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">smfi_quarantine</a>.
def quarantine(self,reason): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_progress">smfi_progress</a>.
def progress(self): pass
## Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">smfi_chgfrom</a>.
def chgfrom(self,sender,param=None): pass
## 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.
# 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="https://www.milter.org/developers/api/smfi_setsymlist">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
## Enable optional %milter actions.
# Certain %milter actions need to be enabled before calling main()
# or they throw an exception. Pymilter enables them all by
# default (since 0.9.2), but you may wish to disable unneeded
# actions as an optimization.
# @param flags Bit or mask of optional actions to enable
def set_flags(flags): pass
def set_connect_callback(cb): pass
def set_helo_callback(cb): pass
def set_envfrom_callback(cb): pass
def set_envrcpt_callback(cb): pass
def set_header_callback(cb): pass
def set_eoh_callback(cb): pass
def set_body_callback(cb): pass
def set_abort_callback(cb): pass
def set_close_callback(cb): pass
## Sets the return code for untrapped Python exceptions during a callback.
# The default is TEMPFAIL. You should not depend on this handler. Your
# application should have its own top level exception handler for each
# callback. You can then choose your own reply message, log the stack track
# were you please, and so on. However, if you miss one, this last ditch
# handler will print a standard stack trace to sys.stderr, and return to
# sendmail.
# @param code one of #TEMPFAIL,#REJECT,#CONTINUE, or since 1.0, #ACCEPT
def set_exception_policy(code): pass
## Register python %milter with libmilter.
# The name we pass is used to identify the %milter in the MTA configuration.
# Callback functions must be set using the set_*_callback() functions before
# registering the %milter.
# Three additional callbacks are specified as keyword parameters. These
# were added by recent versions of libmilter. The keyword parameters is
# a nicer way to do it, I think, since it makes clear that you have to do
# it before registering. I may move all the callbacks in the future (perhaps
# keeping the set functions for compatibility). Note that Milter.Base
# automatically maps all callbacks to member functions, and negotiates which
# member functions are actually overridden by an application class.
# @param name the %milter name by which the MTA finds us
# @param negotiate the
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
# xxfi_negotiate</a> callback, called to negotiate supported
# actions, callbacks, and protocol steps.
# @param unknown the
# <a href="https://www.milter.org/developers/api/xxfi_unknown">
# xxfi_unknown</a> callback, called when for SMTP commands
# not recognized by the MTA. (Extend SMTP in your milter!)
# @param data the
# <a href="https://www.milter.org/developers/api/xxfi_data">
# xxfi_data</a> callback, called when the DATA
# SMTP command is received.
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="https://www.milter.org/developers/api/smfi_opensocket">
# 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
## Transfer control to libmilter.
# Calls <a href="https://www.milter.org/developers/api/smfi_main">
# smfi_main</a>.
def main(): pass
## Set the libmilter debugging level.
# <a href="https://www.milter.org/developers/api/smfi_setdbg">smfi_setdbg</a>
# 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
# greater (more positive) the level the more detailed the debugging. Six is the
# current, highest, useful value. Must be called before calling main().
def setdbg(lev): pass
## Set timeout for MTA communication.
# Calls <a href="https://www.milter.org/developers/api/smfi_settimeout">
# smfi_settimeout</a>. Must be called before calling main().
def settimeout(secs): pass
## Set socket backlog.
# Calls <a href="https://www.milter.org/developers/api/smfi_setbacklog">
# smfi_setbacklog</a>. Must be called before calling main().
def setbacklog(n): pass
## Set the socket used to communicate with the MTA.
# The MTA can communicate with the milter by means of a
# unix, inet, or inet6 socket. By default, a unix domain socket
# is used. It must not exist,
# and sendmail will throw warnings if, eg, the file is under a
# group or world writable directory. milter.setconn() will not fail with
# an invalid socket - this will be detected only when calling milter.main()
# or milter.opensocket().
# @param s the socket address in proto:address format
# <pre>
# milter.setconn('unix:/var/run/pythonfilter') # a named pipe
# milter.setconn('local:/var/run/pythonfilter') # a named pipe
# milter.setconn('inet:8800') # listen on ANY interface
# milter.setconn('inet:7871@@publichost') # listen on a specific interface
# milter.setconn('inet6:8020')
# milter.setconn('inet6:8020@[2001:db8:1234::1]') # listen on specific IP
# </pre>
def setconn(s): pass
## Stop the %milter gracefully.
def stop(): pass
## Retrieve diagnostic info.
# Return a tuple with diagnostic info gathered by the milter module.
# The first two fields are counts of milterContext objects created
# and deleted. Additional fields may be added later.
# @return a tuple of diagnostic data
def getdiag(): pass
## Retrieve the runtime libmilter version.
# Return the runtime libmilter version. This can be different
# from the compile time version when sendmail or libmilter is upgraded
# after pymilter is compiled.
# @return a tuple of <code>(major,minor,patchlevel)</code>
def getversion(): pass
## The compile time libmilter version.
# Python code might need to deal with pymilter compiled
# against various versions of libmilter. This module constant
# contains the contents of the <code>SMFI_VERSION</code> macro when
# the milter module was compiled.
VERSION = 0x1000001
-16
View File
@@ -1,16 +0,0 @@
web:
doxygen
cd doc/html; zip -r ../../doc .
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
VERSION=1.0
CVSTAG=pymilter-1_0
PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz
$(SRCTAR):
cvs export -r$(CVSTAG) -d $(PKG) pymilter
tar cvfz $(PKG).tar.gz $(PKG)
rm -r $(PKG)
cvstar: $(SRCTAR)
-79
View File
@@ -1,79 +0,0 @@
## 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()
+10 -32
View File
@@ -11,18 +11,11 @@ import Milter
import StringIO import StringIO
import time import time
import email import email
import sys
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
from Milter.utils import parse_addr from Milter import parse_addr
if True:
from multiprocessing import Process as Thread, Queue
else:
from threading import Thread
from Queue import Queue
logq = Queue(maxsize=4)
class myMilter(Milter.Base): class myMilter(Milter.Milter):
def __init__(self): # A new instance with each new connection. def __init__(self): # A new instance with each new connection.
self.id = Milter.uniqueID() # Integer incremented with each call. self.id = Milter.uniqueID() # Integer incremented with each call.
@@ -30,7 +23,6 @@ class myMilter(Milter.Base):
# each connection runs in its own thread and has its own myMilter # each connection runs in its own thread and has its own myMilter
# instance. Python code must be thread safe. This is trivial if only stuff # instance. Python code must be thread safe. This is trivial if only stuff
# in myMilter instances is referenced. # in myMilter instances is referenced.
@Milter.noreply
def connect(self, IPname, family, hostaddr): def connect(self, IPname, family, hostaddr):
# (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) ) # (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
# (self, 'ip6.mxout.example.com', AF_INET6, # (self, 'ip6.mxout.example.com', AF_INET6,
@@ -71,9 +63,6 @@ class myMilter(Milter.Base):
self.fromparms = Milter.dictfromlist(str) # ESMTP parms self.fromparms = Milter.dictfromlist(str) # ESMTP parms
self.user = self.getsymval('{auth_authen}') # authenticated user self.user = self.getsymval('{auth_authen}') # authenticated user
self.log("mail from:", mailfrom, *str) self.log("mail from:", mailfrom, *str)
# NOTE: self.fp is only an *internal* copy of message data. You
# must use addheader, chgheader, replacebody to change the message
# on the MTA.
self.fp = StringIO.StringIO() self.fp = StringIO.StringIO()
self.canon_from = '@'.join(parse_addr(mailfrom)) self.canon_from = '@'.join(parse_addr(mailfrom))
self.fp.write('From %s %s\n' % (self.canon_from,time.ctime())) self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
@@ -81,37 +70,38 @@ class myMilter(Milter.Base):
## def envrcpt(self, to, *str): ## def envrcpt(self, to, *str):
@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)
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply
def header(self, name, hval): def header(self, name, hval):
self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply
def eoh(self): def eoh(self):
self.fp.write("\n") # terminate headers self.fp.write("\n") # terminate headers
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply
def body(self, chunk): def body(self, chunk):
self.fp.write(chunk) self.fp.write(chunk)
return Milter.CONTINUE return Milter.CONTINUE
def eom(self): def eom(self):
self.fp.seek(0) self.fp.seek(0)
msg = email.message_from_file(self.fp) msg = email.message_from_file(self.fp)
self.setreply('250','2.5.1','Grokked by pymilter')
# many milter functions can only be called from eom() # many milter functions can only be called from eom()
# example of adding a Bcc: # example of adding a Bcc:
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.
@@ -124,25 +114,15 @@ class myMilter(Milter.Base):
## === Support Functions === ## === Support Functions ===
def log(self,*msg): def log(self,*msg):
logq.put((msg,self.id,time.time())) print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S'),self.id),
def background():
while True:
t = logq.get()
if not t: break
msg,id,ts = t
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ... # 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print i, for i in msg: print i,
print print
## === ## ===
def main(): def main():
bt = Thread(target=background)
bt.start()
socketname = "/home/stuart/pythonsock"
timeout = 600
# Register to have the Milter factory create instances of your class: # Register to have the Milter factory create instances of your class:
Milter.factory = myMilter Milter.factory = myMilter
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
@@ -152,8 +132,6 @@ def main():
print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S') print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,timeout) Milter.runmilter("pythonfilter",socketname,timeout)
logq.put(None)
bt.join()
print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S') print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__": if __name__ == "__main__":
+68 -200
View File
@@ -35,46 +35,6 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$ * $Log$
* Revision 1.35 2013/03/14 22:11:25 customdesigned
* Release 0.9.8
*
* Revision 1.34 2013/03/09 05:42:14 customdesigned
* Make TestBase members private, fix getsymlist misspelling.
*
* Revision 1.33 2013/03/09 00:25:23 customdesigned
* Better untrapped exception message. const char for doc comments.
*
* Revision 1.32 2013/01/13 01:46:16 customdesigned
* Doc updates.
*
* 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
* Generate special exception when callback return not int.
*
* Revision 1.27 2009/07/28 21:45:54 customdesigned
* Add getversion() to return runtime version.
*
* Revision 1.26 2009/07/28 21:08:20 customdesigned
* Increment del count.
*
* Revision 1.25 2009/07/28 20:58:55 customdesigned
* getdiag method
*
* Revision 1.24 2009/06/09 01:54:44 customdesigned
* Forgot to initialize optional parameter.
*
* Revision 1.23 2009/05/29 20:44:58 customdesigned
* Typo SMFIP_NO constants.
*
* Revision 1.22 2009/05/29 19:53:36 customdesigned * Revision 1.22 2009/05/29 19:53:36 customdesigned
* Typo SMFIS_ALL_OPTS * Typo SMFIS_ALL_OPTS
* *
@@ -279,10 +239,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 no good way to do this * published. Unfortunately I know of now good way to do this
* other than with OS-specific tests. * other than with OS-specific tests.
*/ */
#if defined(__FreeBSD_kernel__) || defined(__linux__) #ifdef linux
#define HAVE_IPV6_RFC2553 #define HAVE_IPV6_RFC2553
#include <arpa/inet.h> #include <arpa/inet.h>
#endif #endif
@@ -296,64 +256,25 @@ $ python setup.py help
#endif #endif
#endif #endif
enum callbacks {
CONNECT,HELO,ENVFROM,ENVRCPT,HEADER,EOH,BODY,EOM,ABORT,CLOSE,
#ifdef SMFIS_ALL_OPTS
UNKNOWN,DATA,NEGOTIATE,
#endif
NUMCALLBACKS
};
#define connect_callback callback[CONNECT].cb /* Yes, these are static. If you need multiple different callbacks, */
#define helo_callback callback[HELO].cb /* it's cleaner to use multiple filters, or convert to OO method calls. */
#define envfrom_callback callback[ENVFROM].cb static PyObject *connect_callback = NULL;
#define envrcpt_callback callback[ENVRCPT].cb static PyObject *helo_callback = NULL;
#define header_callback callback[HEADER].cb static PyObject *envfrom_callback = NULL;
#define eoh_callback callback[EOH].cb static PyObject *envrcpt_callback = NULL;
#define body_callback callback[BODY].cb static PyObject *header_callback = NULL;
#define eom_callback callback[EOM].cb static PyObject *eoh_callback = NULL;
#define abort_callback callback[ABORT].cb static PyObject *body_callback = NULL;
#define close_callback callback[CLOSE].cb static PyObject *eom_callback = NULL;
#define unknown_callback callback[UNKNOWN].cb static PyObject *abort_callback = NULL;
#define data_callback callback[DATA].cb static PyObject *close_callback = NULL;
#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 {
PyObject *cb;
const char *name;
} callback[NUMCALLBACKS+1] = {
{ NULL ,"connect" },
{ NULL ,"helo" },
{ NULL ,"envfrom" },
{ NULL ,"envrcpt" },
{ NULL ,"header" },
{ NULL ,"eoh" },
{ NULL ,"body" },
{ NULL ,"eom" },
{ NULL ,"abort" },
{ NULL ,"close" },
#ifdef SMFIS_ALL_OPTS
{ NULL ,"unknown" },
{ NULL ,"data" },
{ NULL ,"negotiate" },
#endif
{ NULL , NULL }
};
staticforward struct smfiDesc description; /* forward declaration */ staticforward 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 */
static PyInterpreterState *interp; static PyInterpreterState *interp;
typedef struct {
unsigned int contextNew;
unsigned int contextDel;
} milter_Diag;
static milter_Diag diag;
staticforward PyTypeObject milter_ContextType; staticforward PyTypeObject milter_ContextType;
@@ -392,7 +313,6 @@ _get_context(SMFICTX *ctx) {
PyThreadState_Delete(t); PyThreadState_Delete(t);
return NULL; return NULL;
} }
++diag.contextNew;
self->t = t; self->t = t;
self->ctx = ctx; self->ctx = ctx;
Py_INCREF(Py_None); Py_INCREF(Py_None);
@@ -431,7 +351,6 @@ milter_Context_dealloc(PyObject *s) {
} }
Py_DECREF(self->priv); Py_DECREF(self->priv);
PyObject_DEL(self); PyObject_DEL(self);
++diag.contextDel;
} }
/* Throw an exception if an smfi call failed, otherwise return PyNone. */ /* Throw an exception if an smfi call failed, otherwise return PyNone. */
@@ -452,7 +371,7 @@ _thread_return(PyThreadState *t,int val,char *errstr) {
return _generic_return(val,errstr); return _generic_return(val,errstr);
} }
static const char milter_set_flags__doc__[] = static char milter_set_flags__doc__[] =
"set_flags(int) -> None\n\ "set_flags(int) -> None\n\
Set flags for filter capabilities; OR of one or more of:\n\ Set flags for filter capabilities; OR of one or more of:\n\
ADDHDRS - filter may add headers\n\ ADDHDRS - filter may add headers\n\
@@ -493,7 +412,7 @@ generic_set_callback(PyObject *args,char *t,PyObject **cb) {
return Py_None; return Py_None;
} }
static const char milter_set_connect_callback__doc__[] = static char milter_set_connect_callback__doc__[] =
"set_connect_callback(Function) -> None\n\ "set_connect_callback(Function) -> None\n\
Sets the Python function invoked when a connection is made to sendmail.\n\ Sets the Python function invoked when a connection is made to sendmail.\n\
Function takes args (ctx, hostname, integer, hostaddr) -> int\n\ Function takes args (ctx, hostname, integer, hostaddr) -> int\n\
@@ -520,7 +439,7 @@ milter_set_connect_callback(PyObject *self, PyObject *args) {
"O:set_connect_callback", &connect_callback); "O:set_connect_callback", &connect_callback);
} }
static const char milter_set_helo_callback__doc__[] = static char milter_set_helo_callback__doc__[] =
"set_helo_callback(Function) -> None\n\ "set_helo_callback(Function) -> None\n\
Sets the Python function invoked upon SMTP HELO.\n\ Sets the Python function invoked upon SMTP HELO.\n\
Function takes args (ctx, hostname) -> int\n\ Function takes args (ctx, hostname) -> int\n\
@@ -531,7 +450,7 @@ milter_set_helo_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_helo_callback", &helo_callback); return generic_set_callback(args, "O:set_helo_callback", &helo_callback);
} }
static const char milter_set_envfrom_callback__doc__[] = static char milter_set_envfrom_callback__doc__[] =
"set_envfrom_callback(Function) -> None\n\ "set_envfrom_callback(Function) -> None\n\
Sets the Python function invoked on envelope from.\n\ Sets the Python function invoked on envelope from.\n\
Function takes args (ctx, from, *str) -> int\n\ Function takes args (ctx, from, *str) -> int\n\
@@ -544,7 +463,7 @@ milter_set_envfrom_callback(PyObject *self, PyObject *args) {
&envfrom_callback); &envfrom_callback);
} }
static const char milter_set_envrcpt_callback__doc__[] = static char milter_set_envrcpt_callback__doc__[] =
"set_envrcpt_callback(Function) -> None\n\ "set_envrcpt_callback(Function) -> None\n\
Sets the Python function invoked on each envelope recipient.\n\ Sets the Python function invoked on each envelope recipient.\n\
Function takes args (ctx, rcpt, *str) -> int\n\ Function takes args (ctx, rcpt, *str) -> int\n\
@@ -557,7 +476,7 @@ milter_set_envrcpt_callback(PyObject *self, PyObject *args) {
&envrcpt_callback); &envrcpt_callback);
} }
static const char milter_set_header_callback__doc__[] = static char milter_set_header_callback__doc__[] =
"set_header_callback(Function) -> None\n\ "set_header_callback(Function) -> None\n\
Sets the Python function invoked on each message header.\n\ Sets the Python function invoked on each message header.\n\
Function takes args (ctx, field, value) ->int\n\ Function takes args (ctx, field, value) ->int\n\
@@ -570,7 +489,7 @@ milter_set_header_callback(PyObject *self, PyObject *args) {
&header_callback); &header_callback);
} }
static const char milter_set_eoh_callback__doc__[] = static char milter_set_eoh_callback__doc__[] =
"set_eoh_callback(Function) -> None\n\ "set_eoh_callback(Function) -> None\n\
Sets the Python function invoked at end of header.\n\ Sets the Python function invoked at end of header.\n\
Function takes args (ctx) -> int"; Function takes args (ctx) -> int";
@@ -580,7 +499,7 @@ milter_set_eoh_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback); return generic_set_callback(args, "O:set_eoh_callback", &eoh_callback);
} }
static const char milter_set_body_callback__doc__[] = static char milter_set_body_callback__doc__[] =
"set_body_callback(Function) -> None\n\ "set_body_callback(Function) -> None\n\
Sets the Python function invoked for each body chunk. There may\n\ Sets the Python function invoked for each body chunk. There may\n\
be multiple body chunks passed to the filter. End-of-lines are\n\ be multiple body chunks passed to the filter. End-of-lines are\n\
@@ -593,7 +512,7 @@ milter_set_body_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_body_callback", &body_callback); return generic_set_callback(args, "O:set_body_callback", &body_callback);
} }
static const char milter_set_eom_callback__doc__[] = static char milter_set_eom_callback__doc__[] =
"set_eom_callback(Function) -> None\n\ "set_eom_callback(Function) -> None\n\
Sets the Python function invoked at end of message.\n\ Sets the Python function invoked at end of message.\n\
This routine is the only place where special operations\n\ This routine is the only place where special operations\n\
@@ -606,7 +525,7 @@ milter_set_eom_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_eom_callback", &eom_callback); return generic_set_callback(args, "O:set_eom_callback", &eom_callback);
} }
static const char milter_set_abort_callback__doc__[] = static char milter_set_abort_callback__doc__[] =
"set_abort_callback(Function) -> None\n\ "set_abort_callback(Function) -> None\n\
Sets the Python function invoked if message is aborted\n\ Sets the Python function invoked if message is aborted\n\
outside of the control of the filter, for example,\n\ outside of the control of the filter, for example,\n\
@@ -620,7 +539,7 @@ milter_set_abort_callback(PyObject *self, PyObject *args) {
return generic_set_callback(args, "O:set_abort_callback", &abort_callback); return generic_set_callback(args, "O:set_abort_callback", &abort_callback);
} }
static const char milter_set_close_callback__doc__[] = static char milter_set_close_callback__doc__[] =
"set_close_callback(Function) -> None\n\ "set_close_callback(Function) -> None\n\
Sets the Python function invoked at end of the connection. This\n\ Sets the Python function invoked at end of the connection. This\n\
is called on close even if the previous mail transaction was aborted.\n\ is called on close even if the previous mail transaction was aborted.\n\
@@ -633,7 +552,7 @@ milter_set_close_callback(PyObject *self, PyObject *args) {
static int exception_policy = SMFIS_TEMPFAIL; static int exception_policy = SMFIS_TEMPFAIL;
static const char milter_set_exception_policy__doc__[] = static char milter_set_exception_policy__doc__[] =
"set_exception_policy(i) -> None\n\ "set_exception_policy(i) -> None\n\
Sets the policy for untrapped Python exceptions during a callback.\n\ Sets the policy for untrapped Python exceptions during a callback.\n\
Must be one of TEMPFAIL,REJECT,CONTINUE"; Must be one of TEMPFAIL,REJECT,CONTINUE";
@@ -644,8 +563,7 @@ milter_set_exception_policy(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i)) if (!PyArg_ParseTuple(args, "i:set_exception_policy", &i))
return NULL; return NULL;
switch (i) { switch (i) {
case SMFIS_REJECT: case SMFIS_TEMPFAIL: case SMFIS_REJECT: case SMFIS_TEMPFAIL: case SMFIS_CONTINUE:
case SMFIS_CONTINUE: case SMFIS_ACCEPT:
exception_policy = i; exception_policy = i;
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
@@ -660,30 +578,23 @@ _release_thread(PyThreadState *t) {
PyEval_ReleaseThread(t); PyEval_ReleaseThread(t);
} }
/** Report and clear any python exception before returning to libmilter. /** Report and clear any python exception before returning to libmilter.
The interpreter is locked when we are called, and we unlock it. */ The interpreter is locked when we are called, and we unlock it. */
static int _report_exception(milter_ContextObject *self) { static int _report_exception(milter_ContextObject *self) {
char untrapped_msg[80];
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
sprintf(untrapped_msg,"pymilter: untrapped exception in %.40s",
description.xxfi_name);
PyErr_Print(); PyErr_Print();
PyErr_Clear(); /* must clear since not returning to python */ PyErr_Clear(); /* must clear since not returning to python */
_release_thread(self->t); _release_thread(self->t);
switch (exception_policy) { switch (exception_policy) {
case SMFIS_REJECT: case SMFIS_REJECT:
smfi_setreply(self->ctx, "554", "5.3.0", untrapped_msg); smfi_setreply(self->ctx, "554", "5.3.0", "Filter failure");
return SMFIS_REJECT; return SMFIS_REJECT;
case SMFIS_TEMPFAIL: case SMFIS_TEMPFAIL:
smfi_setreply(self->ctx, "451", "4.3.0", untrapped_msg); smfi_setreply(self->ctx, "451", "4.3.0", "Filter failure");
return SMFIS_TEMPFAIL; return SMFIS_TEMPFAIL;
} }
return exception_policy; return SMFIS_CONTINUE;
} }
/* This should never happen, _report_exception is only called when
* the caller has already detected a python exception. If it
* does somehow happen, pretend nothing is wrong... */
_release_thread(self->t); _release_thread(self->t);
return SMFIS_CONTINUE; return SMFIS_CONTINUE;
} }
@@ -700,23 +611,9 @@ _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)) { retval = PyInt_AsLong(result);
const struct MilterCallback *p;
const char *cbname = "milter";
char buf[40];
Py_DECREF(result);
for (p = callback; p->name; ++p) {
if (cb == p->cb) {
cbname = p->name;
break;
}
}
sprintf(buf,"The %s callback must return int",cbname);
PyErr_SetString(MilterError,buf);
return _report_exception(self);
}
retval = PyInt_AS_LONG(result);
Py_DECREF(result); Py_DECREF(result);
if (PyErr_Occurred()) return _report_exception(self);
_release_thread(self->t); _release_thread(self->t);
return retval; return retval;
} }
@@ -905,6 +802,10 @@ milter_wrap_abort(SMFICTX *ctx) {
} }
#ifdef SMFIS_ALL_OPTS #ifdef SMFIS_ALL_OPTS
static PyObject *unknown_callback = NULL;
static PyObject *data_callback = NULL;
static PyObject *negotiate_callback = NULL;
static int static int
milter_wrap_unknown(SMFICTX *ctx, const char *cmd) { milter_wrap_unknown(SMFICTX *ctx, const char *cmd) {
PyObject *arglist; PyObject *arglist;
@@ -1011,7 +912,7 @@ milter_wrap_close(SMFICTX *ctx) {
return r; return r;
} }
static const char milter_register__doc__[] = static char milter_register__doc__[] =
"register(name,unknown=,data=,negotiate=) -> None\n\ "register(name,unknown=,data=,negotiate=) -> None\n\
Registers the milter name with current callbacks, and flags.\n\ Registers the milter name with current callbacks, and flags.\n\
Required before main() is called."; Required before main() is called.";
@@ -1056,7 +957,7 @@ milter_register(PyObject *self, PyObject *args, PyObject *kwds) {
return _generic_return(smfi_register(description), "cannot register"); return _generic_return(smfi_register(description), "cannot register");
} }
static const char milter_opensocket__doc__[] = static char milter_opensocket__doc__[] =
"opensocket(rmsock) -> None\n\ "opensocket(rmsock) -> None\n\
Attempts to create and open the socket provided with setconn.\n\ Attempts to create and open the socket provided with setconn.\n\
Removes the socket first if rmsock is True."; Removes the socket first if rmsock is True.";
@@ -1069,7 +970,7 @@ milter_opensocket(PyObject *self, PyObject *args) {
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket"); return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
} }
static const char milter_main__doc__[] = static char milter_main__doc__[] =
"main() -> None\n\ "main() -> None\n\
Main milter routine. Set any callbacks, and flags desired, then call\n\ Main milter routine. Set any callbacks, and flags desired, then call\n\
setconn(), then call register(name), and finally call main()."; setconn(), then call register(name), and finally call main().";
@@ -1093,7 +994,7 @@ milter_main(PyObject *self, PyObject *args) {
return o; return o;
} }
static const char milter_setdbg__doc__[] = static char milter_setdbg__doc__[] =
"setdbg(int) -> None\n\ "setdbg(int) -> None\n\
Sets debug level in sendmail/libmilter source. Dubious usefulness."; Sets debug level in sendmail/libmilter source. Dubious usefulness.";
@@ -1104,7 +1005,7 @@ milter_setdbg(PyObject *self, PyObject *args) {
return _generic_return(smfi_setdbg(val), "cannot set debug value"); return _generic_return(smfi_setdbg(val), "cannot set debug value");
} }
static const char milter_setbacklog__doc__[] = static char milter_setbacklog__doc__[] =
"setbacklog(int) -> None\n\ "setbacklog(int) -> None\n\
Set the TCP connection queue size for the milter socket."; Set the TCP connection queue size for the milter socket.";
@@ -1116,7 +1017,7 @@ milter_setbacklog(PyObject *self, PyObject *args) {
return _generic_return(smfi_setbacklog(val), "cannot set backlog"); return _generic_return(smfi_setbacklog(val), "cannot set backlog");
} }
static const char milter_settimeout__doc__[] = static char milter_settimeout__doc__[] =
"settimeout(int) -> None\n\ "settimeout(int) -> None\n\
Set the time (in seconds) that sendmail will wait before\n\ Set the time (in seconds) that sendmail will wait before\n\
considering this filter dead."; considering this filter dead.";
@@ -1129,7 +1030,7 @@ milter_settimeout(PyObject *self, PyObject *args) {
return _generic_return(smfi_settimeout(val), "cannot set timeout"); return _generic_return(smfi_settimeout(val), "cannot set timeout");
} }
static const char milter_setconn__doc__[] = static char milter_setconn__doc__[] =
"setconn(filename) -> None\n\ "setconn(filename) -> None\n\
Sets the pathname to the unix, inet, or inet6 socket that\n\ Sets the pathname to the unix, inet, or inet6 socket that\n\
sendmail will use to communicate with this filter. By default,\n\ sendmail will use to communicate with this filter. By default,\n\
@@ -1149,7 +1050,7 @@ milter_setconn(PyObject *self, PyObject *args) {
return _generic_return(smfi_setconn(str), "cannot set connection"); return _generic_return(smfi_setconn(str), "cannot set connection");
} }
static const char milter_stop__doc__[] = static char milter_stop__doc__[] =
"stop() -> None\n\ "stop() -> None\n\
This function appears to be a controlled method to tell sendmail to\n\ This function appears to be a controlled method to tell sendmail to\n\
stop using this filter. It will close the socket."; stop using this filter. It will close the socket.";
@@ -1162,31 +1063,7 @@ milter_stop(PyObject *self, PyObject *args) {
return _thread_return(t,smfi_stop(), "cannot stop"); return _thread_return(t,smfi_stop(), "cannot stop");
} }
static const char milter_getdiag__doc__[] = static char milter_getsymval__doc__[] =
"getdiag() -> tuple\n\
Return a tuple of diagnostic data. The first two items are context new\n\
count and context del count. The rest are yet to be defined.";
static PyObject *
milter_getdiag(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, ":getdiag")) return NULL;
return Py_BuildValue("(kk)", diag.contextNew,diag.contextDel);
}
static const char milter_getversion__doc__[] =
"getversion() -> tuple\n\
Return runtime libmilter version as a tuple of major,minor,patchlevel.";
static PyObject *
milter_getversion(PyObject *self, PyObject *args) {
unsigned int major, minor, patch;
if (!PyArg_ParseTuple(args, ":getversion")) return NULL;
if (smfi_version(&major,&minor,&patch) != MI_SUCCESS) {
PyErr_SetString(MilterError, "smfi_version failed");
return NULL;
}
return Py_BuildValue("(kkk)", major,minor,patch);
}
static const char milter_getsymval__doc__[] =
"getsymval(String) -> String\n\ "getsymval(String) -> String\n\
Returns a symbol's value. Context-dependent, and unclear from the dox."; Returns a symbol's value. Context-dependent, and unclear from the dox.";
@@ -1201,7 +1078,7 @@ milter_getsymval(PyObject *self, PyObject *args) {
return Py_BuildValue("s", smfi_getsymval(ctx, str)); return Py_BuildValue("s", smfi_getsymval(ctx, str));
} }
static const char milter_setreply__doc__[] = static char milter_setreply__doc__[] =
"setreply(rcode, xcode, message) -> None\n\ "setreply(rcode, xcode, message) -> None\n\
Sets the specific reply code to be used in response\n\ Sets the specific reply code to be used in response\n\
to the active command.\n\ to the active command.\n\
@@ -1265,7 +1142,7 @@ milter_setreply(PyObject *self, PyObject *args) {
"cannot set reply"); "cannot set reply");
} }
static const char milter_addheader__doc__[] = static char milter_addheader__doc__[] =
"addheader(field, value, idx=-1) -> None\n\ "addheader(field, value, idx=-1) -> None\n\
Add a header to the message. This header is not passed to other\n\ Add a header to the message. This header is not passed to other\n\
filters. It is not checked for standards compliance;\n\ filters. It is not checked for standards compliance;\n\
@@ -1302,7 +1179,7 @@ milter_addheader(PyObject *self, PyObject *args) {
} }
#ifdef SMFIF_CHGFROM #ifdef SMFIF_CHGFROM
static const char milter_chgfrom__doc__[] = static char milter_chgfrom__doc__[] =
"chgfrom(sender,params) -> None\n\ "chgfrom(sender,params) -> None\n\
Change the envelope sender (MAIL From) of the current message.\n\ Change the envelope sender (MAIL From) of the current message.\n\
A filter which calls smfi_chgfrom must have set the CHGFROM flag\n\ A filter which calls smfi_chgfrom must have set the CHGFROM flag\n\
@@ -1311,7 +1188,7 @@ This function can only be called from the EOM callback.";
static PyObject * static PyObject *
milter_chgfrom(PyObject *self, PyObject *args) { milter_chgfrom(PyObject *self, PyObject *args) {
char *sender; char *sender;
char *params = NULL; char *params;
SMFICTX *ctx; SMFICTX *ctx;
PyThreadState *t; PyThreadState *t;
@@ -1325,7 +1202,7 @@ milter_chgfrom(PyObject *self, PyObject *args) {
} }
#endif #endif
static const char milter_chgheader__doc__[] = static char milter_chgheader__doc__[] =
"chgheader(field, int, value) -> None\n\ "chgheader(field, int, value) -> None\n\
Change/delete a header in the message. \n\ Change/delete a header in the message. \n\
It is not checked for standards compliance; the mail filter\n\ It is not checked for standards compliance; the mail filter\n\
@@ -1353,7 +1230,7 @@ milter_chgheader(PyObject *self, PyObject *args) {
"cannot change header"); "cannot change header");
} }
static const char milter_addrcpt__doc__[] = static char milter_addrcpt__doc__[] =
"addrcpt(string,params=None) -> None\n\ "addrcpt(string,params=None) -> None\n\
Add a recipient to the envelope. It must be in the same format\n\ Add a recipient to the envelope. It must be in the same format\n\
as is passed to the envrcpt callback in the first tuple element.\n\ as is passed to the envrcpt callback in the first tuple element.\n\
@@ -1383,7 +1260,7 @@ milter_addrcpt(PyObject *self, PyObject *args) {
return _thread_return(t,rc, "cannot add recipient"); return _thread_return(t,rc, "cannot add recipient");
} }
static const char milter_delrcpt__doc__[] = static char milter_delrcpt__doc__[] =
"delrcpt(string) -> None\n\ "delrcpt(string) -> None\n\
Delete a recipient from the envelope.\n\ Delete a recipient from the envelope.\n\
This function can only be called from the EOM callback."; This function can only be called from the EOM callback.";
@@ -1401,7 +1278,7 @@ milter_delrcpt(PyObject *self, PyObject *args) {
return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient"); return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient");
} }
static const char milter_replacebody__doc__[] = static char milter_replacebody__doc__[] =
"replacebody(string) -> None\n\ "replacebody(string) -> None\n\
Replace the body of the message. This routine may be called multiple\n\ Replace the body of the message. This routine may be called multiple\n\
times if the body is longer than convenient to send in one call. End of\n\ times if the body is longer than convenient to send in one call. End of\n\
@@ -1423,7 +1300,7 @@ milter_replacebody(PyObject *self, PyObject *args) {
(unsigned char *)bodyp, bodylen), "cannot replace message body"); (unsigned char *)bodyp, bodylen), "cannot replace message body");
} }
static const char milter_setpriv__doc__[] = static char milter_setpriv__doc__[] =
"setpriv(object) -> object\n\ "setpriv(object) -> object\n\
Associates any Python object with this context, and returns\n\ Associates any Python object with this context, and returns\n\
the old value or None. Use this to\n\ the old value or None. Use this to\n\
@@ -1449,7 +1326,7 @@ milter_setpriv(PyObject *self, PyObject *args) {
return old; return old;
} }
static const char milter_getpriv__doc__[] = static char milter_getpriv__doc__[] =
"getpriv() -> None\n\ "getpriv() -> None\n\
Returns the Python object associated with the current context (if any).\n\ Returns the Python object associated with the current context (if any).\n\
Use this in conjunction with setpriv to keep track of data in a thread-safe\n\ Use this in conjunction with setpriv to keep track of data in a thread-safe\n\
@@ -1467,7 +1344,7 @@ milter_getpriv(PyObject *self, PyObject *args) {
} }
#ifdef SMFIF_QUARANTINE #ifdef SMFIF_QUARANTINE
static const char milter_quarantine__doc__[] = static char milter_quarantine__doc__[] =
"quarantine(string) -> None\n\ "quarantine(string) -> None\n\
Place the message in quarantine. A string with a description of the reason\n\ Place the message in quarantine. A string with a description of the reason\n\
is the only argument."; is the only argument.";
@@ -1488,7 +1365,7 @@ milter_quarantine(PyObject *self, PyObject *args) {
#endif #endif
#ifdef SMFIR_PROGRESS #ifdef SMFIR_PROGRESS
static const char milter_progress__doc__[] = static char milter_progress__doc__[] =
"progress() -> None\n\ "progress() -> None\n\
Notify the MTA that we are working on a message so it will reset timeouts."; Notify the MTA that we are working on a message so it will reset timeouts.";
@@ -1505,23 +1382,23 @@ milter_progress(PyObject *self, PyObject *args) {
} }
#endif #endif
#ifdef SMFIF_SETSYMLIST #ifdef SMFIF_SETSMLIST
static const char milter_setsymlist__doc__[] = static char milter_setsmlist__doc__[] =
"setsymlist(stage,macrolist) -> None\n\ "setsmlist(stage,macrolist) -> None\n\
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";
static PyObject * static PyObject *
milter_setsymlist(PyObject *self, PyObject *args) { milter_setsmlist(PyObject *self, PyObject *args) {
SMFICTX *ctx; SMFICTX *ctx;
PyThreadState *t; PyThreadState *t;
int stage = 0; int stage = 0;
char *smlist = 0; char *smlist = 0;
if (!PyArg_ParseTuple(args, "is:setsymlist",&stage, &smlist)) return NULL; if (!PyArg_ParseTuple(args, "is:setsmlist",&stage, &smlist)) return NULL;
ctx = _find_context(self); ctx = _find_context(self);
if (ctx == NULL) return NULL; if (ctx == NULL) return NULL;
t = PyEval_SaveThread(); t = PyEval_SaveThread();
return _thread_return(t,smfi_setsymlist(ctx,stage,smlist), return _thread_return(t,smfi_setsmlist(ctx,stage,smlist),
"cannot set macro list"); "cannot set macro list");
} }
#endif #endif
@@ -1545,8 +1422,8 @@ static PyMethodDef context_methods[] = {
#ifdef SMFIF_CHGFROM #ifdef SMFIF_CHGFROM
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__}, { "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
#endif #endif
#ifdef SMFIF_SETSYMLIST #ifdef SMFIF_SETSMLIST
{ "setsymlist", milter_setsymlist, METH_VARARGS, milter_setsymlist__doc__}, { "setsmlist", milter_setsmlist, METH_VARARGS, milter_setsmlist__doc__},
#endif #endif
{ NULL, NULL } { NULL, NULL }
}; };
@@ -1598,8 +1475,6 @@ static PyMethodDef milter_methods[] = {
{ "setbacklog", milter_setbacklog, METH_VARARGS, milter_setbacklog__doc__}, { "setbacklog", milter_setbacklog, METH_VARARGS, milter_setbacklog__doc__},
{ "setconn", milter_setconn, METH_VARARGS, milter_setconn__doc__}, { "setconn", milter_setconn, METH_VARARGS, milter_setconn__doc__},
{ "stop", milter_stop, METH_VARARGS, milter_stop__doc__}, { "stop", milter_stop, METH_VARARGS, milter_stop__doc__},
{ "getdiag", milter_getdiag, METH_VARARGS, milter_getdiag__doc__},
{ "getversion", milter_getversion, METH_VARARGS, milter_getversion__doc__},
{ NULL, NULL } { NULL, NULL }
}; };
@@ -1627,7 +1502,7 @@ static PyTypeObject milter_ContextType = {
Py_TPFLAGS_DEFAULT, /* tp_flags */ Py_TPFLAGS_DEFAULT, /* tp_flags */
}; };
static const char milter_documentation[] = static char milter_documentation[] =
"This module interfaces with Sendmail's libmilter functionality,\n\ "This module interfaces with Sendmail's libmilter functionality,\n\
allowing one to write email filters directly in Python.\n\ allowing one to write email filters directly in Python.\n\
Libmilter is currently marked FFR, and needs to be explicitly installed.\n\ Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
@@ -1669,15 +1544,8 @@ initmilter(void) {
#ifdef SMFIF_CHGFROM #ifdef SMFIF_CHGFROM
setitem(d,"CHGFROM",SMFIF_CHGFROM); setitem(d,"CHGFROM",SMFIF_CHGFROM);
#endif #endif
#ifdef SMFIF_SETSYMLIST #ifdef SMFIF_SETSMLIST
setitem(d,"SETSYMLIST",SMFIF_SETSYMLIST); 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);
+57 -79
View File
@@ -1,16 +1,4 @@
# $Log$ # $Log$
# Revision 1.8 2011/11/05 15:51:03 customdesigned
# New example
#
# Revision 1.7 2009/06/13 21:15:12 customdesigned
# Doxygen updates.
#
# Revision 1.6 2009/06/09 03:13:13 customdesigned
# More doxygen docs.
#
# Revision 1.5 2005/07/20 14:49:43 customdesigned
# Handle corrupt and empty ZIP files.
#
# Revision 1.4 2005/06/17 01:49:39 customdesigned # Revision 1.4 2005/06/17 01:49:39 customdesigned
# Handle zip within zip. # Handle zip within zip.
# #
@@ -82,12 +70,8 @@
# with old milter code. # with old milter code.
# #
## @package mime # This module provides a "defang" function to replace naughty attachments
# This module provides a "defang" function to replace naughty attachments. # with a warning message.
#
# We also provide workarounds for bugs in the email module that comes
# with python. The "bugs" fixed mostly come up only with malformed
# messages - but that is what you have when dealing with spam.
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. # Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
@@ -109,8 +93,6 @@ from email import Errors
from types import ListType,StringType from types import ListType,StringType
## Return a list of filenames in a zip file.
# Embedded zip files are recursively expanded.
def zipnames(txt): def zipnames(txt):
fp = StringIO.StringIO(txt) fp = StringIO.StringIO(txt)
zipf = zipfile.ZipFile(fp,'r') zipf = zipfile.ZipFile(fp,'r')
@@ -121,8 +103,6 @@ def zipnames(txt):
names += zipnames(zipf.read(nm)) names += zipnames(zipf.read(nm))
return names return names
## Fix multipart handling in email.Generator.
#
class MimeGenerator(Generator): class MimeGenerator(Generator):
def _dispatch(self, msg): def _dispatch(self, msg):
# Get the Content-Type: for the message, then try to dispatch to # Get the Content-Type: for the message, then try to dispatch to
@@ -130,24 +110,24 @@ class MimeGenerator(Generator):
# full MIME type, then dispatch to self._handle_<maintype>(). If # full MIME type, then dispatch to self._handle_<maintype>(). If
# that's missing too, then dispatch to self._writeBody(). # that's missing too, then dispatch to self._writeBody().
main = msg.get_content_maintype() main = msg.get_content_maintype()
if msg.is_multipart() and main.lower() != 'multipart': if msg.is_multipart() and main.lower() != 'multipart':
self._handle_multipart(msg) self._handle_multipart(msg)
else: else:
Generator._dispatch(self,msg) Generator._dispatch(self,msg)
def unquote(s): def unquote(s):
"""Remove quotes from a string.""" """Remove quotes from a string."""
if len(s) > 1: if len(s) > 1:
if s.startswith('"'): if s.startswith('"'):
if s.endswith('"'): if s.endswith('"'):
s = s[1:-1] s = s[1:-1]
else: # remove garbage after trailing quote else: # remove garbage after trailing quote
try: s = s[1:s[1:].index('"')+1] try: s = s[1:s[1:].index('"')+1]
except: except:
return s return s
return s.replace('\\\\', '\\').replace('\\"', '"') return s.replace('\\\\', '\\').replace('\\"', '"')
if s.startswith('<') and s.endswith('>'): if s.startswith('<') and s.endswith('>'):
return s[1:-1] return s[1:-1]
return s return s
from types import TupleType from types import TupleType
@@ -162,23 +142,21 @@ def _unquotevalue(value):
from email.Message import _parseparam from email.Message import _parseparam
## Enhance email.Message # Enhance email.Message
# # - Provide a headerchange event for integration with Milter
# Tracks modifications to headers of body or any part independently. # Headerchange attribute can be assigned a function to be called when
# changing headers. The signature is:
# headerchange(msg,name,value) -> None
# - Track modifications to headers of body or any part independently
class MimeMessage(Message): class MimeMessage(Message):
"""Version of email.Message.Message compatible with old mime module """Version of email.Message.Message compatible with old mime module
""" """
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
# Provide a headerchange event for integration with Milter.
# The headerchange attribute can be assigned a function to be called when
# changing headers. The signature is:
# 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)
@@ -205,21 +183,21 @@ class MimeMessage(Message):
for attr,val in self._get_params_preserve([],'content-type'): for attr,val in self._get_params_preserve([],'content-type'):
if isinstance(val, TupleType): if isinstance(val, TupleType):
# It's an RFC 2231 encoded parameter # It's an RFC 2231 encoded parameter
newvalue = _unquotevalue(val) newvalue = _unquotevalue(val)
if val[0]: if val[0]:
val = unicode(newvalue[2], newvalue[0]) val = unicode(newvalue[2], newvalue[0])
else: else:
val = unicode(newvalue[2]) val = unicode(newvalue[2])
else: else:
val = _unquotevalue(val.strip()) val = _unquotevalue(val.strip())
names.append((attr,val)) names.append((attr,val))
names += [("filename",self.get_filename())] names += [("filename",self.get_filename())]
if scan_zip: if scan_zip:
for key,name in tuple(names): # copy by converting to tuple for key,name in tuple(names): # copy by converting to tuple
if name and name.lower().endswith('.zip'): if name and name.lower().endswith('.zip'):
txt = self.get_payload(decode=True) txt = self.get_payload(decode=True)
if txt.strip(): if txt.strip():
names += zipnames(txt) names += zipnames(txt)
return names return names
def ismodified(self): def ismodified(self):
@@ -290,13 +268,13 @@ class MimeMessage(Message):
if t == 'message/rfc822' or t.startswith('multipart/'): if t == 'message/rfc822' or t.startswith('multipart/'):
if not self.submsg: if not self.submsg:
txt = self.get_payload() txt = self.get_payload()
if type(txt) == str: if type(txt) == str:
txt = self.get_payload(decode=True) txt = self.get_payload(decode=True)
self.submsg = email.message_from_string(txt,MimeMessage) self.submsg = email.message_from_string(txt,MimeMessage)
for part in self.submsg.walk(): for part in self.submsg.walk():
part.modified = False part.modified = False
else: else:
self.submsg = txt[0] self.submsg = txt[0]
return self.submsg return self.submsg
return None return None
@@ -336,7 +314,7 @@ def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
if badname: if badname:
if key == 'zipname': if key == 'zipname':
badname = msg.get_filename() badname = msg.get_filename()
break break
else: else:
return Milter.CONTINUE return Milter.CONTINUE
except zipfile.BadZipfile: except zipfile.BadZipfile:
@@ -383,7 +361,7 @@ class _defang:
return rc return rc
def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True, def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True,
scan_zip=False): scan_zip=False):
"""Compatible entry point. """Compatible entry point.
Replace all attachments with dangerous names.""" Replace all attachments with dangerous names."""
self._savname = savname self._savname = savname
@@ -453,25 +431,25 @@ class SGMLFilter(sgmllib.SGMLParser):
n = len(rawdata) n = len(rawdata)
j = i + 2 j = i + 2
while j < n: while j < n:
c = rawdata[j] c = rawdata[j]
if c == ">": if c == ">":
# end of declaration syntax # end of declaration syntax
self.handle_special(rawdata[i+2:j]) self.handle_special(rawdata[i+2:j])
return j + 1 return j + 1
if c in "\"'": if c in "\"'":
m = declstringlit.match(rawdata, j) m = declstringlit.match(rawdata, j)
if not m: if not m:
# incomplete or an error? # incomplete or an error?
return -1 return -1
j = m.end() j = m.end()
elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
m = declname.match(rawdata, j) m = declname.match(rawdata, j)
if not m: if not m:
# incomplete or an error? # incomplete or an error?
return -1 return -1
j = m.end() j = m.end()
else: else:
j += 1 j += 1
# end of buffer between tokens # end of buffer between tokens
return -1 return -1
@@ -500,7 +478,7 @@ def check_html(msg,savname=None):
if msgtype == 'application/octet-stream': if msgtype == 'application/octet-stream':
for (attr,name) in msg.getnames(): for (attr,name) in msg.getnames():
if name and name.lower().endswith(".htm"): if name and name.lower().endswith(".htm"):
msgtype = 'text/html' msgtype = 'text/html'
if msgtype == 'text/html': if msgtype == 'text/html':
out = StringIO.StringIO() out = StringIO.StringIO()
htmlfilter = HTMLScriptFilter(out) htmlfilter = HTMLScriptFilter(out)
+44 -83
View File
@@ -1,25 +1,32 @@
%define __python python2.6 # EL 3,4,5 supported, set to 0 for Fedora
%define pythonbase python %define el4 1
%define dist .el4
%if 0%{?el3} || 0%{?el4}
%define __python python2.4
%endif
%define libdir %{_libdir}/pymilter %define libdir %{_libdir}/pymilter
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")} %{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
%define pythonbase %(basename %{__python})
Summary: Python interface to sendmail milter API Summary: Python interface to sendmail milter API
Name: %{pythonbase}-pymilter Name: pymilter
Version: 1.0 Version: 0.9.2
Release: 1%{dist} Release: 3%{dist}
Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz Source: http://downloads.sourceforge.net/pymilter/%{name}-%{version}.tar.gz
Source1: pymilter.te
License: GPLv2+ License: GPLv2+
Group: Development/Libraries Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
Url: http://www.bmsi.com/python/milter.html Url: http://www.bmsi.com/python/milter.html
# python-2.6.4 gets RuntimeError: not holding the import lock Requires: %{pythonbase} >= 2.4, sendmail >= 8.13
Requires: %{pythonbase} >= 2.6.5, sendmail >= 8.13 %if 0%{?el3} || 0%{?el4}
# Need python2.6 specific pydns, not the version for system python # Need python2.4 specific pydns, not the version for system python
Requires: %{pythonbase}-pydns Requires: pydns
%else
# Needed for callbacks, not a core function but highly useful for milters # Needed for callbacks, not a core function but highly useful for milters
BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13 Requires: python-pydns
%endif
BuildRequires: ed, %{pythonbase}-devel >= 2.4, sendmail-devel >= 8.13
%description %description
This is a python extension module to enable python scripts to This is a python extension module to enable python scripts to
@@ -27,23 +34,11 @@ attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, sending modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV. DSNs, and doing CBV.
%package selinux
Summary: SELinux policy module for pymilter
Group: System Environment/Base
Requires: policycoreutils, selinux-policy, %{name}
BuildRequires: policycoreutils, checkpolicy
%description selinux
SELinux policy module for using pymilter with sendmail with selinux enforcing
%prep %prep
%setup -q -n pymilter-%{version} %setup -q
cp %{SOURCE1} pymilter.te
%build %build
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
checkmodule -m -M -o pymilter.mod pymilter.te
semodule_package -o pymilter.pp -m pymilter.mod
%install %install
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
@@ -51,11 +46,31 @@ rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
mkdir -p $RPM_BUILD_ROOT%{libdir} mkdir -p $RPM_BUILD_ROOT%{libdir}
cp start.sh $RPM_BUILD_ROOT%{libdir}
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
/^datadir=/
c
datadir="%{_localstatedir}/log/milter"
.
/^piddir=/
c
piddir="%{_localstatedir}/run/milter"
.
/^libdir=/
c
libdir="%{libdir}"
.
/^python=/
c
python="%{__python}"
.
w
q
EOF
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
# install selinux modules # start.sh is used by spfmilter, srsmilter, and milter, and could be used by
mkdir -p %{buildroot}%{_datadir}/selinux/targeted # other milters using pymilter.
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
%files %files
%defattr(-,root,root,-) %defattr(-,root,root,-)
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py %doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
@@ -64,64 +79,10 @@ cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter %dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter %dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
%files selinux
%doc pymilter.te
%{_datadir}/selinux/targeted/*
%clean %clean
rm -rf $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT
%post selinux
/usr/sbin/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \
&>/dev/null || :
%postun selinux
if [ $1 -eq 0 ] ; then
/usr/sbin/semodule -s targeted -r pymilter &> /dev/null || :
fi
%changelog %changelog
* Sat Mar 1 2014 Stuart Gathman <stuart@gathman.org> 1.0-2
- Remove start.sh to track EPEL repository, suggest daemonize as replacement
- Selinux subpackage should not care about pymilter version
* Wed Jun 26 2013 Stuart Gathman <stuart@gathman.org> 1.0-1
- Allow ACCEPT as untrapped exception policy
- Optional dir for getaddrset and getaddrdict in Milter.config
- Show registered milter name in untrapped exception message.
- Include selinux subpackage
- Provide Milter.greylist export and Milter.greylist import to migrate data
* Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 0.9.8-1
- Add Milter.test module for unit testing milters.
- Fix typo that prevented setsymlist from being active.
- Change untrapped exception message to:
- "pymilter: untrapped exception in milter app"
* Thu Apr 12 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
- Remove redundant table in miltermodule
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
* 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.
(Since stacktrace is empty, the TypeError exception is confusing.)
- Fix milter-template.py
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
* Wed Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
- Handle IP6 in Milter.utils.iniplist()
- python-2.6
* Thu Jul 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.3-1
- Handle source route in Milter.utils.parse_addr()
- Fix default arg in chgfrom.
- Disable negotiate callback for libmilter < 8.14.3 (1,0,1)
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-3 * Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-3
- Change result of @noreply callbacks to NOREPLY when so negotiated. - Change result of @noreply callbacks to NOREPLY when so negotiated.
-13
View File
@@ -1,13 +0,0 @@
module pymilter 1.0;
require {
type sendmail_t;
type var_run_t;
type initrc_t;
class sock_file { write getattr };
class unix_stream_socket connectto;
}
#============= sendmail_t ==============
allow sendmail_t initrc_t:unix_stream_socket connectto;
allow sendmail_t var_run_t:sock_file { write getattr };
+13 -15
View File
@@ -35,9 +35,7 @@ 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
@@ -60,23 +58,23 @@ class sampleMilter(Milter.Milter):
# even if we wanted the Taiwanese spam, we can't read Chinese # even if we wanted the Taiwanese spam, we can't read Chinese
# (delete if you read chinese mail) # (delete if you read chinese mail)
if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'): if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'):
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
# check for common spam keywords # check for common spam keywords
if val.find("$$$") >= 0 or val.find("XXX") >= 0 \ if val.find("$$$") >= 0 or val.find("XXX") >= 0 \
or val.find("!!!") >= 0 or val.find("FREE") >= 0: or val.find("!!!") >= 0 or val.find("FREE") >= 0:
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
# check for spam that pretends to be legal # check for spam that pretends to be legal
lval = val.lower() lval = val.lower()
if lval.startswith("adv:") or lval.startswith("adv.") \ if lval.startswith("adv:") or lval.startswith("adv.") \
or lval.find('viagra') >= 0: or lval.find('viagra') >= 0:
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
return Milter.REJECT return Milter.REJECT
# check for invalid message id # check for invalid message id
if lname == 'message-id' and len(val) < 4: if lname == 'message-id' and len(val) < 4:
@@ -86,7 +84,7 @@ class sampleMilter(Milter.Milter):
# check for common bulk mailers # check for common bulk mailers
if lname == 'x-mailer' and \ if lname == 'x-mailer' and \
val.lower() in ('direct email','calypso','mail bomber'): val.lower() in ('direct email','calypso','mail bomber'):
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
@@ -123,7 +121,7 @@ class sampleMilter(Milter.Milter):
h = msg.getheaders(name) h = msg.getheaders(name)
cnt = len(h) cnt = len(h)
for i in range(cnt,0,-1): for i in range(cnt,0,-1):
self.chgheader(name,i-1,'') self.chgheader(name,i-1,'')
def eom(self): def eom(self):
if not self.fp: return Milter.ACCEPT if not self.fp: return Milter.ACCEPT
@@ -145,9 +143,9 @@ class sampleMilter(Milter.Milter):
msg = rfc822.Message(out) msg = rfc822.Message(out)
msg.rewindbody() msg.rewindbody()
while 1: while 1:
buf = out.read(8192) buf = out.read(8192)
if len(buf) == 0: break if len(buf) == 0: break
self.replacebody(buf) # feed modified message to sendmail self.replacebody(buf) # feed modified message to sendmail
return Milter.ACCEPT # ACCEPT modified message return Milter.ACCEPT # ACCEPT modified message
finally: finally:
out.close() out.close()
+3 -3
View File
@@ -1,5 +1,5 @@
[bdist_rpm] [bdist_rpm]
python=python2.6 python=python2.4
doc_files=README NEWS TODO COPYING CREDITS doc_files=README NEWS TODO
packager=Stuart D. Gathman <stuart@gathman.org> packager=Stuart D. Gathman <stuart@bmsi.com>
release=1 release=1
+8 -4
View File
@@ -2,9 +2,6 @@ import os
import sys import sys
from distutils.core import setup, Extension from distutils.core import setup, Extension
if sys.version < '2.6.5':
sys.exit('ERROR: Sorry, python 2.6.5 is required for this module.')
# FIXME: on some versions of sendmail, smutil is renamed to sm. # FIXME: on some versions of sendmail, smutil is renamed to sm.
# On slackware and debian, leave it out entirely. It depends # On slackware and debian, leave it out entirely. It depends
# on how libmilter was built by the sendmail package. # on how libmilter was built by the sendmail package.
@@ -12,8 +9,15 @@ if sys.version < '2.6.5':
libs = ["milter"] libs = ["milter"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian libdirs = ["/usr/lib/libmilter"] # needed for Debian
# patch distutils if it can't cope with the "classifiers" or
# "download_url" keywords
if sys.version < '2.2.3':
from distutils.dist import DistributionMetadata
DistributionMetadata.classifiers = None
DistributionMetadata.download_url = None
# 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 = '1.0', setup(name = "pymilter", version = '0.9.2',
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
+3 -6
View File
@@ -1,16 +1,13 @@
#!/bin/sh #!/bin/sh
appname="$1" appname="$1"
script="${2:-${appname}}" script="${2:-${appname}}"
datadir="/var/lib/milter" datadir="/var/log/milter"
logdir="/var/log/milter"
piddir="/var/run/milter" piddir="/var/run/milter"
libdir="/usr/lib/pymilter" libdir="/usr/lib/pymilter"
python="python2.4" python="python2.4"
exec >>${logdir}/${appname}.log 2>&1 exec >>${datadir}/${appname}.log 2>&1
if test -s ${datadir}/${script}.py; then if test -s ${datadir}/${script}.py; then
cd ${datadir} # use version in data dir if it exists for debugging cd ${datadir} # use version in log dir if it exists for debugging
elif test -s ${logdir}/${script}.py; then
cd ${logdir} # use version in log dir if it exists for debugging
else else
cd ${libdir} cd ${libdir}
fi fi
-2
View File
@@ -2,7 +2,6 @@ import unittest
import testmime import testmime
import testsample import testsample
import testutils import testutils
import testgrey
import os import os
def suite(): def suite():
@@ -10,7 +9,6 @@ def suite():
s.addTest(testmime.suite()) s.addTest(testmime.suite())
s.addTest(testsample.suite()) s.addTest(testsample.suite())
s.addTest(testutils.suite()) s.addTest(testutils.suite())
s.addTest(testgrey.suite())
return s return s
if __name__ == '__main__': if __name__ == '__main__':
+710
View File
@@ -0,0 +1,710 @@
From stuart@bmsi.com Wed May 1 14:37:14 2002
Return-Path: <stuart@bmsi.com>
Received: from bmsi.com (IDENT:stuart@localhost [127.0.0.1])
by gathman.bmsi.com (8.11.6/8.11.6) with ESMTP id g41IbCF01796
for <stuart@gathman.bmsi.com>; Wed, 1 May 2002 14:37:13 -0400
Sender: stuart@gathman.bmsi.com
Message-ID: <3CD035D7.18ADF27F@bmsi.com>
Date: Wed, 01 May 2002 14:37:11 -0400
From: "Stuart D. Gathman" <stuart@bmsi.com>
Organization: Business Management Systems, Inc.
X-Mailer: Mozilla 4.78 [en] (X11; U; Linux 2.4.9-21 i586)
X-Accept-Language: en
MIME-Version: 1.0
To: stuart@gathman.bmsi.com
Subject: Amazon.com--Earth's Biggest Selection
Content-Type: multipart/mixed;
boundary="------------59A46341C90BA737DD47867B"
This is a multi-part message in MIME format.
--------------59A46341C90BA737DD47867B
Content-Type: multipart/alternative;
boundary="------------0B098FB91956AC123C61B151"
--------------0B098FB91956AC123C61B151
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065
--
Stuart D. Gathman
Business Management Systems Inc. Phone: 703 591-0911 Fax: 703 591-6154
"Confutatis maledictis, flamis acribus addictis" - background song for
a Microsoft sponsored "Where do you want to go from here?" commercial.
--------------0B098FB91956AC123C61B151
Content-Type: text/html; charset=us-ascii
Content-Transfer-Encoding: 7bit
<!doctype html public "-//w3c//dtd html 4.0 transitional//en">
<html>
<A HREF="http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065">http://www.amazon.com/exec/obidos/subst/home/redirect.html/103-3111065-2579065</A>
<pre>--&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Stuart D. Gathman&nbsp;<stuart@bmsi.com>
Business Management Systems Inc.&nbsp; Phone: 703 591-0911 Fax: 703 591-6154
"Confutatis maledictis, flamis acribus addictis" - background song for
a Microsoft sponsored "Where do you want to go from here?" commercial.</pre>
&nbsp;</html>
--------------0B098FB91956AC123C61B151--
--------------59A46341C90BA737DD47867B
Content-Type: text/html; charset=us-ascii;
name="103-3111065-2579065"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="103-3111065-2579065"
Content-Base: "http://www.amazon.com/exec/obidos/subs
t/home/redirect.html/103-3111065-25
79065"
Content-Location: "http://www.amazon.com/exec/obidos/subs
t/home/redirect.html/103-3111065-25
79065"
<html>
<head>
<title>
Amazon.com--Earth's Biggest Selection
</title>
<meta name="keywords" content="amazon.com,amazon books,amazon,amazon.com books,amazon music,amazon.com music,amazon video,amazon.com video,auctions,amazon auctions,amazon.com auctions,electronics,consumer electronics,gifts,amazon gifts,amazon.com gifts,cards,e-cards,e-mail cards,greeting cards,amazon cards,amazon.com cards,toys,amazon toys,amazon.com toys,games,amazon games,amazon.com games,toys & games,toys and games">
<style type="text/css"><!-- .serif { font-family: times,serif; font-size: medium; }
.sans { font-family: verdana,arial,helvetica,sans-serif; font-size: medium; }
.small { font-family: verdana,arial,helvetica,sans-serif; font-size: small; }
.h1 { font-family: verdana,arial,helvetica,sans-serif; color: #CC6600; font-size: medium; }
.h3color { font-family: verdana,arial,helvetica,sans-serif; color: #CC6600; font-size: small; }
.tiny { font-family: verdana,arial,helvetica,sans-serif; font-size: x-small; }
.listprice { font-family: arial,verdana,helvetica,sans-serif; text-decoration: line-through; font-size: small; }
.price { font-family: verdana,arial,helvetica,sans-serif; color: #990000; font-size: small; }
--></style>
</head>
<body bgcolor="#FFFFFF" link="#003399" alink="#FF9933" vlink="#996633" text="#000000" onLoad="document.searchform.elements[1].focus()">
<a name="top"></a>
<map name="right_top_nav_map">
<area shape="rect" href=/exec/obidos/shopping-basket/ref=top_nav_sb_gateway/103-3111065-2579065 coords="0,0,80,21">
<area shape="rect" href=/exec/obidos/wishlist/ref=cm_wl_topnav_gateway/103-3111065-2579065 coords="85,0,151,21">
<area shape="rect" href=/exec/obidos/account-access-login/ref=top_nav_ya_gateway/103-3111065-2579065 coords="155,0,256,21">
<area shape="rect" href=/exec/obidos/tg/browse/-/508510/ref=top_nav_hp_gateway/103-3111065-2579065 coords="260,0,299,21">
</map>
<map name="gateway_nav_map">
<area shape=rect coords="0,0,124,28" href=/exec/obidos/tg/stores/static/-/gateway/international-gateway/ref=gw_subnav_in/103-3111065-2579065>
<area shape=rect coords="125,0,228,28" href=/exec/obidos/tg/new-for-you/top-sellers/-/main/ref=gw_subnav_ts/103-3111065-2579065>
<area shape=rect coords="229,0,332,28" href=/exec/obidos/tg/browse/-/700060/ref=gw_subnav_target/103-3111065-2579065>
<area shape=rect coords="333,0,450,28" href=/exec/obidos/tg/browse/-/909656/ref=stuffandsubnav_td1_/103-3111065-2579065>
<area shape=rect coords="451,0,580,28" href=/exec/obidos/subst/misc/sell-your-stuff.html/ref=subnav_sys_/103-3111065-2579065>
</map>
<table border=0 width=100% cellspacing=0 cellpadding=0>
<tr><td width=100%>
<center>
<table width=100% border=0 cellspacing=0 cellpadding=0 vspace=0>
<tr>
<td width=25% rowspan=2>&nbsp;</td>
<td align=left valign=bottom><a href=/exec/obidos/subst/home/redirect.html/ref=nh_gateway/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/associates/navbar2000/logo-no-border(1).gif" width=148 height=43 alt="" border=0></a></td>
<td width=10%>&nbsp;</td>
<td align=right>
<img src="http://g-images.amazon.com/images/G/01/nav/personalized/cartwish/right-topnav-default-2.gif" width=300 height=22 alt="" USEMAP=#right_top_nav_map border=0></td>
<td align=right rowspan=2 width=25%>
&nbsp;
</td>
</tr>
<tr valign=bottom>
<td colspan=3 align=center>
<table align=center border=0 cellpadding=0 cellspacing=0><tr valign=bottom>
<td><a href=/exec/obidos/subst/home/home.html/ref%3Dtab%5Fgw%5Fgw%5F1/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/welcome-on-whole.gif" width=60 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/stores/your/store-home/-/0/ref%3Dtab%5Fgw%5Ffr%5F2/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/yourstore-off-sliced._ZCSTUART%27S,0,2,0,0,verdenab,7,90,90,80_.gif" width=81 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/283155/ref%3Dtab%5Fgw%5Fb%5F3/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/books-off-sliced.gif" width=39 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/172282/ref%3Dtab%5Fgw%5Fe%5F4/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/electronics-off-sliced.gif" width=74 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/130/ref%3Dtab%5Fgw%5Fd%5F5/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/dvd-off-sliced.gif" width=35 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/171280/ref%3Dtab%5Fgw%5Ft%5F6/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/toys-off-sliced.gif" width=47 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/468642/ref%3Dtab%5Fgw%5Fvg%5F7/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/videogames-off-sliced.gif" width=73 height=26 border=0></a></td>
<td><a href=/exec/obidos/tg/browse/-/600460/ref%3Dtab%5Fgw%5F%5F8/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/corporate-off-sliced.gif" width=70 height=26 border=0></a></td>
<td><a href=/exec/obidos/subst/home/all-stores.html/ref%3Dtab_gw_storesdirectory/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/nav/personalized/tabs/see-more-off-sliced.gif" width=70 height=26 border=0></a></td>
</tr></table>
</td>
</tr>
</table>
</center>
</td></tr>
<tr align=center bgcolor=#006699>
<td><img src="http://g-images.amazon.com/images/G/01/nav/amazon/gateway/blue/gateway-subnav-default.gif" width=580 height=28 width=580 height=28 alt="" USEMAP="#gateway_nav_map" border=0></td>
</tr>
<tr>
<td bgcolor=#ffffdd align=center class=small>
<font face=verdana,arial,helvetica size=-1>
<font color="#CC6600"><B>Hello, Stuart D. Gathman.</B></font>
We have <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/subst%2Frecs%2Finstant-recs-home.html%2Fref%3Dpd_ir_gw_r/ref=ilm_stripe_272005/103-3111065-2579065&message=272005,m1,26">recommendations</A> for you.
</font><font face=verdana,arial,helvetica size=-2>
(If you're not Stuart D. Gathman, <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/flex-sign-in%2Fref%3Dpd_ir_gw_r%2F%3Fopt%3Doa%26page%3Drecs%2Fsign-in-secure.html%26response%3Dtg%2Frecs%2Frecs-post-login-dispatch%2F-%2Frecs%2Fpd_rw_gw_r/ref=ilm_stripe_272005/103-3111065-2579065&message=272005,m1,26">click here</A>.)
</font>
</td>
</tr>
</table>
<br>
<table width=100% cellpadding=0 cellspacing=0 border=0>
<tr valign=top>
<td width=174>
<TABLE border=0 cellspacing=0 cellpadding=0><TR valign=bottom align=center>
<td><img src="http://g-images.amazon.com/images/G/01/v9/search-browse/search-gateway.gif" width=171 height=19 border=0 alt="Search Amazon.com"></td>
</TR> <TR valign=top align=center><TD> <TABLE border=0 width= 171 cellpadding=1 cellspacing=0 bgcolor=#708090 ><TR> <TD width=100%><TABLE width=100% border=0 cellpadding=4 cellspacing=0 bgcolor=#708090><TR> <TD bgcolor=#FFCC66 valign=top width=100%>
<form method="post" action="/exec/obidos/search-handle-form/103-3111065-2579065" name="searchform">
<select name=index>
<option value=blended selected>All Products
<option value=books>Books
<option value=music>Popular Music
<option value=music-dd>Music Downloads
<option value=classical>Classical Music
<option value="dvd">DVD
<option value="vhs">VHS
<option value=theatrical>Movie Showtimes
<option value=toys>Toys
<option value=baby>Baby
<option value=pc-hardware>Computers
<option value=videogames>Video Games
<option value=electronics>Electronics
<option value=photo>Camera &amp; Photo
<option value=software>Software
<option value=tools>Tools &amp; Hardware
<option value=magazines>Magazines
<option value=garden>Outdoor Living
<option value=kitchen>Kitchen
<option value=travel>Travel
<option value=wireless-phones>Cell Phones & Service
<option value=outlet>Outlet
<option value=auction-redirect>Auctions
<option value=fixed-price-redirect>zShops
</select>
<input type="text" name="field-keywords" size="15">
<input type="image" height="21" width="21" border=0 value="Go" name="Go" src="http://g-images.amazon.com/images/G/01/v9/search-browse/go-button-gateway.gif" align=absmiddle>
</TD> </TR> </TABLE> </TD> </TR> </TABLE> </TD> </form>
</TR> </TABLE> <br clear=left>
<TABLE border=0 cellspacing=0 cellpadding=0>
<TR valign=bottom align=center>
<td><img src="http://g-images.amazon.com/images/G/01/v9/search-browse/browse-gateway.gif" width=171 height=19 border=0 alt="Browse Amazon.com"></td>
</TR> <TR valign=top align=center>
<TD> <TABLE border=0 width= 171 cellpadding=1 cellspacing=0 bgcolor=#708090 ><TR> <TD width=100%><TABLE width=100% border=0 cellpadding=4 cellspacing=0 bgcolor=#708090><TR> <TD bgcolor=#ffffff valign=top width=100%>
<table cellpadding=3 cellspacing=0>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/283155/ref=gw_br_bo/103-3111065-2579065">Books</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/172282/ref=gw_br_el/103-3111065-2579065">Electronics</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/540744/ref=gw_br_ba/103-3111065-2579065">Baby &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/540744/ref=gw_br_ba/103-3111065-2579065">Baby Registry</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/5174/ref=gw_br_mu/103-3111065-2579065">Music</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/redirect-to-partner/ref=gw_br_dscm/103-3111065-2579065?name=dscm&aid=2&aparam=tb5270_bhp&trx=8056">Health & Beauty</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/130/ref=gw_br_dvd/103-3111065-2579065">DVD</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/229534/ref=gw_br_sw/103-3111065-2579065">Software</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/284507/ref=gw_br_ki/103-3111065-2579065">Kitchen &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/284507/ref=gw_br_ki/103-3111065-2579065">Housewares</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/228013/ref=gw_br_hi/103-3111065-2579065">Tools &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/228013/ref=gw_br_hi/103-3111065-2579065">Hardware</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/541966/ref=gw_br_pc/103-3111065-2579065">Computers</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/502394/ref=gw_br_p/103-3111065-2579065">Camera & Photo</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/562436/ref=gw_br_th/103-3111065-2579065">Movie Showtimes</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/468642/ref=gw_br_cvg/103-3111065-2579065">Computer &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/468642/ref=gw_br_cvg/103-3111065-2579065">Video Games</a></b></td>
</tr> <tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/171280/ref=gw_br_tg/103-3111065-2579065">Toys &amp; Games</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/301185/ref=gw_br_wi/103-3111065-2579065">Cell Phones</a><br>&nbsp;&nbsp;&nbsp;<a href="/exec/obidos/tg/browse/-/301185/ref=gw_br_wi/103-3111065-2579065">& Service</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/404272/ref=gw_br_vi/103-3111065-2579065">Video</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/599858/ref=gw_br_zi/103-3111065-2579065">Magazine</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/599858/ref=gw_br_zi/103-3111065-2579065">Subscriptions</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/286168/ref=gw_br_lp/103-3111065-2579065">Outdoor Living</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/605012/ref=gw_br_tr/103-3111065-2579065">Travel</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/acn-redirect-to-partner/ref=gw_br_cars/103-3111065-2579065?partner-name=carsdirect&partner-url=home%3Fpartner%3Damzn%26customerid%3Dbrowse">Cars</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/229220/ref=gw_br_gi/103-3111065-2579065">Gifts &amp;</a><br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/229220/ref=gw_br_gi/103-3111065-2579065">Gift Certificates</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="http://s1.amazon.com/exec/varzea/subst/home/home.html/ref=gw_br_au/103-3111065-2579065">Auctions</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="http://s1.amazon.com/exec/varzea/subst/home/fixed.html/ref=gw_br_zs/103-3111065-2579065">zShops</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/517808/ref=gw_br_ou/103-3111065-2579065">Outlet</a></b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<b><a href="/exec/obidos/tg/browse/-/600460/ref=gw_br_cb/103-3111065-2579065">Corporate</a> <br>&nbsp; &nbsp;<a href="/exec/obidos/tg/browse/-/600460/ref=gw_br_cb/103-3111065-2579065">Accounts</a></b></td>
</tr>
<tr>
<td class=small>
<a href="/exec/obidos/flex-sign-in/ref=pd_fr_gw_fav_edt/103-3111065-2579065?page=personalization/favorites/favorites-sign-in-secure.html&response=favorites-edit/personalization/favorites/edit-areas.html&pass_through=product-group-id.gateway.hp&method=GET">
<img src="http://g-images.amazon.com/images/G/01/buttons/edit-favorites.gif" width=69 height=15 border=0 valign=top vspace=2></a><br>
</td>
</tr>
<tr>
<td class=small><b>Browse Partners</b></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<a href="/exec/obidos/tg/browse/-/700060/ref=gw_tarb_/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/target/target-logo-sm.gif" width=71 height=17 border=0 alt=Target></a></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<a href="/exec/obidos/tg/browse/-/171280/ref=gw_trub_/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/toys/navigation/tru-logo.gif" width=117 height=14 border=0 alt=Toysrus.com></a></td>
</tr>
<tr>
<td class=small>&#149;&nbsp;<a href="/exec/obidos/tg/browse/-/540744/ref=gw_brub_/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/toys/navigation/bru-logo.gif" width=136 height=15 border=0 alt=Babiesrus.com></a></td>
</tr>
</table>
</TD> </TR> </TABLE> </TD> </TR> </TABLE> </TD>
</TR>
</TABLE> <br>
<TABLE border=0 width=171 cellpadding=1 cellspacing=0 bgcolor=#708090 ><TR> <TD width=100%><TABLE width=100% border=0 cellpadding=4 cellspacing=0 bgcolor=#708090><TR> <TD bgcolor=#ffffff valign=top width=100%>
<font face=verdana,arial,helvetica color=#000000 size=-1><b>Special Features</b></font><br>
<font face=verdana,arial,helvetica size=-1>
<ul><li> <A href="/exec/obidos/subst/alerts/signup.html/ref=gw_hp_ls_1_1/103-3111065-2579065">Alerts</A><li> <A href="/exec/obidos/subst/misc/anywhere/anywhere.html/ref=gw_hp_ls_1_2/103-3111065-2579065">Amazon.com
Anywhere</A><li> <A href="/exec/obidos/subst/misc/amazon-credit/marketing-page.html/ref=gw_hp_ls_1_3/103-3111065-2579065">Amazon Credit Account</A><li> <A href="/exec/obidos/subst/delivers/delivers-signup-combo.html/ref=gw_hp_ls_1_4/103-3111065-2579065">Delivers</A><li><A href="/exec/obidos/tg/browse/-/225840/ref=gw_hp_ls_1_5/103-3111065-2579065">Free e-Cards</A><li><A href="/exec/obidos/subst/community/community-home.html/ref=gw_hp_ls_1_6/103-3111065-2579065">Friends &amp; Favorites</A><li> <A href="/exec/obidos/subst/gifts/gift-services/gift-certificates.html/ref=gw_hp_ls_1_7/103-3111065-2579065">Gift
Certificates</A><li> <A href="http://auctions.amazon.com/exec/varzea/subst/fx/home.html/ref=gw_hp_ls_1_8/103-3111065-2579065">Honor
System</A><li> <A href="/exec/obidos/subst/community/community.html/ref=gw_hp_ls_1_9/103-3111065-2579065">Purchase
Circles</A><li>
<A href="/exec/obidos/tg/browse/-/885446/ref=gw_hp_ls_1_10/103-3111065-2579065">Wedding
Registry</A></ul>
</font>
</TD> </TR> </TABLE> </TD> </TR> </TABLE> <br>
<TABLE border=0 width=171 cellpadding=1 cellspacing=0 bgcolor=#708090 ><TR> <TD width=100%><TABLE width=100% border=0 cellpadding=4 cellspacing=0 bgcolor=#708090><TR> <TD bgcolor=#ffffff valign=top width=100%>
<font face=verdana,arial,helvetica color=#000000 size=-1><b>Associates</b></font><br>
<font face=verdana,arial,helvetica size=-1>
Sell books, music, videos, and more from your
Web site. <A href="/exec/obidos/subst/associates/join/associates.html/ref=gw_hp_ls_2_1/103-3111065-2579065">Start earning
today</A>!<BR>
</font>
</TD> </TR> </TABLE> </TD> </TR> </TABLE> <br>
<p>
<br clear=all>
</td>
<td>&nbsp;</td>
<td>
<center>
</center>
<br clear=all><p>
<A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/tg/browse/-/283155/ref=ilm_rc_285799/103-3111065-2579065&message=285799,m1,27">
<center><img src="http://g-images.amazon.com/images/G/01/books/homepage-pricing/books-home-pricing-iii.gif" width=257 height=99 border=0></center>
</A>
<br clear=all><br>
<A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/tg/browse/-/753570/ref=ilm_rc_283024/103-3111065-2579065&message=283024,gw_lr_dvd_lor,28"><img src="http://g-images.amazon.com/images/G/01/icons/thumbnails/b00003cwt6_thumb.gif" width=41 height=60 border=0 valign=top align=left></A>
Pre-order the Oscar&#174;-winning blockbuster <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/tg/browse/-/753570/ref=ilm_rc_283024/103-3111065-2579065&message=283024,gw_lr_dvd_lor,28"><I>The Lord of the Rings: The Fellowship of the Ring</I></A>, arriving on <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/ASIN/B00003CWT6/ref=ilm_rc_283024/103-3111065-2579065&message=283024,gw_lr_dvd_lor,28">DVD</A> and <A href="http://www.amazon.com/exec/obidos/ilm-redirect/103-3111065-2579065?append-uid=no&path=http://www.amazon.com/exec/obidos/ASIN/B000065U6Q/ref=ilm_rc_283024/103-3111065-2579065&message=283024,gw_lr_dvd_lor,28">video</A> August 6.
<br clear=all><br>
<b class=small><A href="/exec/obidos/tg/browse/-/229220/ref=gw_hp_cs_1_1/103-3111065-2579065">In Gifts</A></b><br>
<A href="/exec/obidos/tg/browse/-/229220/ref=gw_hp_cs_2_1/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/marketing/mothers_day/md_sd_roto.jpg" width=100 height=95 border=0 align=left hspace=4></A>
<b><font face=verdana,arial,helvetica color=#CC6600>Mother's Day Is May 12</font></b><br>
We've made it fun and easy to buy the perfect
present for Mom. Shop by <A href="/exec/obidos/tg/stores/recs/gift-wizard-refine/-/holiday/ref=gw_hp_cs_2_2/103-3111065-2579065">recipient</A>
or <A href="/exec/obidos/tg/stores/recs/gift-wizard/-/price/ref=gw_hp_cs_2_3/103-3111065-2579065">price</A>,
browse <A href="/exec/obidos/tg/stores/recs/gift-wizard/-/topsellers/ref=gw_hp_cs_2_4/103-3111065-2579065">top
sellers</A>, or order <A href="http://www.amazon.com/exec/obidos/redirect-to-external-url/103-3111065-2579065?path=http%3A//www.proflowers.com/freechocolate/freechocolate.cfm%3FREF%3DFCHAmazonGatewayExp042702">flowers</A>.
Visit <A href="/exec/obidos/tg/browse/-/229220/ref=gw_hp_cs_2_5/103-3111065-2579065">Gifts</A> for
these and more great ideas for expressing your love and
appreciation.<BR>
&nbsp;<br clear=left>
<br clear=all>
<a href=/exec/obidos/instant-recs/recs/instant-recs-home.html/ref=pd_gw_qpt_h/103-3111065-2579065><b class=small>Your Recommendations</b></a>
<br> <b class=h1>
<i>War in Heaven</i>
</b>
</b><br>
<a href=/exec/obidos/ASIN/0802812198/ref=pd_gw_qpt_1/103-3111065-2579065><img src="http://images.amazon.com/images/P/0802812198.01.__PE20_PIm.arrow,TopLeft,-2,-19_SCTZZZZZZZ_.jpg" width=76 height=116 vspace=3 hspace=7 align=left border=0></a>
<b>Amazon.com</b><br>
"The telephone was ringing wildly," begins Charles Williams's novel <I>War in Heaven</I>, "but without result, since there was no-one in the room but the corpse." From this abrupt--and darkly humorous--start, Williams takes us on a 20th-century version of the Grail quest, with an Archdeacon, a Duke, and an...
<a href=/exec/obidos/ASIN/0802812198/ref=pd_gw_qpt_1/103-3111065-2579065>
<font size=-1>Read more</font></a>
<span class=tiny>
&#124;
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/0802812198/gw/1/pc/3/none/ref=pd_gw_qpt_1/103-3111065-2579065>Why was I recommended this?</a>&#041;
</span>
<br clear=all>
<br><b class=small>More Recommendations</b><br>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
<a href=/exec/obidos/ASIN/0471070408/ref=pd_gw_qpt_2/103-3111065-2579065><i>Reliable Linux</i></a> by Iain Campbell
<span class=tiny>
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/0471070408/gw/1/pc/3/none/ref=pd_gw_qpt_2/103-3111065-2579065>Why?</a>&#041;
</span>
<br>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
<a href=/exec/obidos/ASIN/1565926102/ref=pd_gw_qpt_3/103-3111065-2579065><i>Programming PHP</i></a> by Rasmus Lerdorf, et al
<span class=tiny>
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/1565926102/gw/1/pc/3/none/ref=pd_gw_qpt_3/103-3111065-2579065>Why?</a>&#041;
</span>
<br>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
<a href=/exec/obidos/ASIN/0802812201/ref=pd_gw_qpt_4/103-3111065-2579065><i>Descent into Hell</i></a> by Charles W. Williams
<span class=tiny>
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/0802812201/gw/1/pc/3/none/ref=pd_gw_qpt_4/103-3111065-2579065>Why?</a>&#041;
</span>
<br>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
<a href=/exec/obidos/ASIN/059600186X/ref=pd_gw_qpt_5/103-3111065-2579065><i>Network Troubleshooting Tools (O'Reilly System Administration)</i></a> by Joseph D. Sloan
<span class=tiny>
&#040;<a href=/exec/obidos/tg/recs/ir-why/-/books/0/regular/none/059600186X/gw/1/pc/3/none/ref=pd_gw_qpt_5/103-3111065-2579065>Why?</a>&#041;
</span>
<br>
<p>
<font face=verdana,arial,helvetica size=-1><a href=/exec/obidos/tg/stores/your/favorites/-/music/ref=pd_fr_gw_nr_h/103-3111065-2579065><b>Your Music Store</b></a></font><br>
<font face=verdana,arial,helvetica color=#CC6600><b>
Isaac Freeman, et al&#44;
<i>Beautiful Stars</i>
</b></font>
<br>
<a href=/exec/obidos/ASIN/B000063TQV/ref=pd_fr_qw_nr_1/103-3111065-2579065><img src="http://images.amazon.com/images/P/B000063TQV.01.26TLZZZZ.jpg" width=73 height=71 vspace=3 hspace=7 align=left border=0></a>
Great African American gospel music has an indisputable power, rooted in the audible faith of its performers and the beauty of their voices. As the bass singer of the <a href="/exec/obidos/tg/stores/artist/glance/-/73920/103-3111065-2579065">Fairfield Four</a>, an a cappella group that started more than a half century ago,...
<a href=/exec/obidos/ASIN/B000063TQV/ref=pd_fr_qw_nr_1/103-3111065-2579065><font size=-1>Read more</font></a>
<br>
<br clear=left>
<br>
<table border=0 cellpadding=2 cellspacing=0><tr><td colspan=2>
<p><b class="small">More Stores:</b>
</td></tr>
<tr valign=top><td width=1%><a href=/exec/obidos/tg/stores/your/favorites/-/electronics/ref=pd_fr_qw_nr_2/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-electronics-icon.gif" width=18 height=18 alt=Icon border=0 align=absmiddle></a></td><td><b class="small"><a href=/exec/obidos/tg/stores/your/favorites/-/electronics/ref=pd_fr_gw_nr_2_p/103-3111065-2579065>Your Electronics Store</a>:</b> <a href=/exec/obidos/ASIN/B000063574/ref=pd_fr_gw_nr_2/103-3111065-2579065>iRiver SlimX iMP-350 CD/MP3 Player with 8 minutes ASP and Upgradeable Firmware</a>
by iRiver
</td></tr>
<tr valign=top><td width=1%><a href=/exec/obidos/tg/stores/your/favorites/-/video/ref=pd_fr_qw_nr_3/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-video-icon.gif" width=18 height=18 alt=Icon border=0 align=absmiddle></a></td><td><b class="small"><a href=/exec/obidos/tg/stores/your/favorites/-/video/ref=pd_fr_gw_nr_3_p/103-3111065-2579065>Your Video Store</a>:</b> <a href=/exec/obidos/ASIN/B000062XNA/ref=pd_fr_gw_nr_3/103-3111065-2579065><i>Ocean's Eleven</i></a>
<b>VHS</b> ~ George Clooney
</td></tr>
</table>
<p>
<b><font face=verdana,arial,helvetica color=#CC6600>Listmania!</font></b><br>
<font face=verdana,arial,helvetica size=-2>
(<a href=/exec/obidos/tg/browse/-/542566/103-3111065-2579065>What is this?</a>)
</font><br>
<table width=100% border=0 cellpadding=5 cellspacing=0>
<tr valign=top>
<td width=50% class=small>
<a href=/exec/obidos/tg/listmania/list-browse/-/2RKS17C9X4D3F/ref=pd_gw_lmq_1/103-3111065-2579065><img src="http://images.amazon.com/images/P/0072127732.01.__PIm.arrow,TopLeft,-2,-19_SCTZZZZZZZ_.jpg" width=76 height=109 border=0 vspace=4 hspace=5></a>
<p>
<font face=verdana,arial,helvetica size=-1>
<a href=/exec/obidos/tg/listmania/list-browse/-/2RKS17C9X4D3F/ref=pd_gw_lmq_1/103-3111065-2579065><b>Best Linux Security books</b></a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/A3362WVVMJ3LE9/ref=pd_gw_lmq_n1/103-3111065-2579065>J. Parker</a>, Administrator, hacker.<br>
(7 item list)</font>
</td>
<td width=50% class=small>
<a href=/exec/obidos/tg/listmania/list-browse/-/2B0DIAPG2D3RT/ref=pd_gw_lmq_2/103-3111065-2579065><img src="http://images.amazon.com/images/P/0070419531.01.__PIm.arrow,TopLeft,-2,-19_SCTZZZZZZZ_.jpg" width=76 height=109 border=0 vspace=4 hspace=5></a>
<p>
<font face=verdana,arial,helvetica size=-1>
<a href=/exec/obidos/tg/listmania/list-browse/-/2B0DIAPG2D3RT/ref=pd_gw_lmq_2/103-3111065-2579065><b>Networking</b></a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/AJINE650CAMUQ/ref=pd_gw_lmq_n2/103-3111065-2579065>gakis</a>, Engineer<br>
(13 item list)</font>
</td>
</tr>
<tr>
<td colspan=2 class=small><ul>
<li><a href=/exec/obidos/tg/listmania/list-browse/-/IEF1DNVKZO8B/ref=pd_gw_lmq_3/103-3111065-2579065>My Coder Library</a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/A3RK9LZQKL2YIN/ref=pd_gw_lmq_n3/103-3111065-2579065>John Washam</a><br> <li><a href=/exec/obidos/tg/listmania/list-browse/-/LE6A7H4L7VZK/ref=pd_gw_lmq_4/103-3111065-2579065>ALL THE FANTASY YOU'LL EVER NE</a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/A3628L43ZVEMP5/ref=pd_gw_lmq_n4/103-3111065-2579065>aramis</a><br> <li><a href=/exec/obidos/tg/listmania/list-browse/-/1MD5H6RUOIMIU/ref=pd_gw_lmq_5/103-3111065-2579065>Mythopoeic Fantasy</a>:&nbsp;A list by <a href=/exec/obidos/tg/cm/member-fil/-/A7CSNW9E46NR5/ref=pd_gw_lmq_n5/103-3111065-2579065>Vera Nazarian</a><br> </ul></td></tr></table>
<p>
<b class=small><A href="/exec/obidos/tg/browse/-/605012/ref=gw_hp_cb_1_1/103-3111065-2579065">In Travel</A></b><br>
<A href="/exec/obidos/tg/browse/-/605012/ref=gw_hp_cb_2_1/103-3111065-2579065"><img src="http://g-images.amazon.com/images/G/01/travel/promotions/travel-gateway1.gif" width=100 height=95 border=0 align=left hspace=4></A>
<b><font face=verdana,arial,helvetica color=#CC6600>Your Next Vacation Starts
Here</font></b><br>
Save up to 70% on hotels from Vegas to New York
and everywhere in between on <A href="/exec/obidos/acn-redirect-to-partner/103-3111065-2579065?partner-name=expedia&partner-url=pubspec/scripts/eap.asp%3FEAPID%3D11420-1%26GOTO%3DDAILY%26Page%3D/deals/hoteldeals.asp%3Frfrr%3D-2980">Expedia.com</A>.
Book a flight during Hotwire's <A href="/exec/obidos/acn-redirect-to-partner/103-3111065-2579065?partner-name=hotwire&partner-url=index.jsp%3Fsid%3D39151%26bid%3DB627">major-airline Spring Sale</A> through May 2 and fly the
big-name airlines at no-name airline prices. <A href="/exec/obidos/acn-redirect-to-partner/103-3111065-2579065?partner-name=thevacationstore&partner-url=cruises/show_cruise.asp%3Fd%3D%26i%3D743065%26c%3D24%26v%3D110">The
Vacation Store</A> is offering seven-day Holland America
Caribbean cruises from just $599. <BR>
&nbsp;<br clear=left>
<td width=174>
<table width=100% cellpadding=3 cellspacing=0 border=0>
<tr>
<td>
<a href=/exec/obidos/subst/xs/hotpicks.html/ref=xs_ie_13_gw/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/marketing/cross-shop/web-labs/lp_gate_roto_t._ZCStuart%5c,,3,5,300,300,verdenab,14,204,0,0_SCLZZZZZZZ_.gif" width=174 height=34 border=0></a><br>
<a href=/exec/obidos/subst/xs/hotpicks.html/ref=xs_ie_13_gw/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/marketing/cross-shop/web-labs/lp_gate_roto_m.gif" width=174 height=200 border=0></a><br>
<a href=/exec/obidos/subst/xs/hotpicks.html/ref=xs_ie_13_gw/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/marketing/cross-shop/web-labs/lp_gate_roto_b.gif" width=174 height=231 border=0></a><br>
<a href=/exec/obidos/tg/new-for-you/new-for-you/-/main/ref=pd_nfy_gw_n/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/banners/n4u/n4u-header-recognized-01.gif" width=174 height=41 hspace=0 vspace=0 align=right border=0 alt="New For You"></a><br clear=all>
<table border=0 bgcolor=#708090 cellpadding=1 cellspacing=0 width=174 align=right valign=top vspace=0 hspace=0><tr><td>
<table border=0 cellpadding=3 cellspacing=0 width=100% bgcolor=#ffffff>
<tr><td bgcolor=#ffffff align=middle>
<span class=small><font color=#CC6600><b>Stuart,</b></font> check out what's<b> <a href=/exec/obidos/tg/new-for-you/new-for-you/-/main/ref=pd_nfy_gw_n/103-3111065-2579065>New for You</a></b>:<br></span>
</td></tr>
<tr><td bgcolor=#ffffff align=middle>
<span class=tiny>(If you're not Stuart D. Gathman, <a href=/exec/obidos/flex-sign-in/ref=pd_nfy_gw_n/103-3111065-2579065?opt=o&page=misc/login/flex-sign-in-secure.html&response=tg/new-for-you/new-for-you/-/main>click here</a>.)</span>
<br><br>
</td></tr>
<tr bgcolor=#eeeecc><td>
<a href=/exec/obidos/tg/new-for-you/inbox/inbox/-/main/ref=pd_nfy_gw_ibx/103-3111065-2579065><b class=small>Your Message Center</b></a>
</td></tr>
<tr bgcolor=#ffffee><td>
<table><tr bgcolor=#ffffee>
<td valign=top><a href=/exec/obidos/tg/new-for-you/inbox/inbox/-/main/ref=pd_nfy_gw_ibx/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/exclamation-clear.gif" width=20 height=20 border=0 alt=!></a></td>
<td class=small> You have <a href=/exec/obidos/tg/new-for-you/inbox/inbox/-/main/ref=pd_nfy_gw_ibx/103-3111065-2579065>5 new messages</a>.
<br><br>
</td>
</tr></table>
</td></tr>
<tr bgcolor=#eeeecc><td>
<font face=verdana,arial,helvetica size=-1><a href=/exec/obidos/shopping-basket/ref=pd_nfy_gw_sc/103-3111065-2579065><b>Your Shopping Cart</b></a></font>
</td></tr>
<tr><td>
<table><tr>
<td valign=top><a href=/exec/obidos/shopping-basket/ref=pd_nfy_gw_sc/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/shopping-cart-small.gif" width=25 height=25 border=0 alt="Shopping Cart" align=left></a></td>
<td valign=top><font face=verdana,arial,helvetica size=-1>You have 0 items in <a href=/exec/obidos/shopping-basket/ref=pd_nfy_gw_sc/103-3111065-2579065>your Shopping Cart</a>.</font><br><br></td>
</tr></table>
</td></tr></table>
<table border=0 cellpadding=3 cellspacing=0 width=100% bgcolor=#ffffff vspace=0>
<tr bgcolor=#eeeecc><td class=small>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/main/ref=pd_nfy_gw_n/103-3111065-2579065><b>Your New Releases</b></a>
</td></tr></table>
<table border=0 cellpadding=3 cellspacing=0 width=100% bgcolor=#ffffff vspace=0>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/music/37/ref=pd_nfy_gw_n1/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-music-icon.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/music/37/ref=pd_nfy_gw_n1/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Pop</font></a>
</td></tr>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/music/173429/ref=pd_nfy_gw_n2/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-music-icon.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/music/173429/ref=pd_nfy_gw_n2/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Christian & Gospel</font></a>
</td></tr>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/books/5/ref=pd_nfy_gw_n3/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/books/5/ref=pd_nfy_gw_n3/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Computers & Internet</font></a>
</td></tr>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/kitchen/289814/ref=pd_nfy_gw_n4/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/icon-kitchen-blue.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/kitchen/289814/ref=pd_nfy_gw_n4/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Cookware</font></a>
</td></tr>
<tr valign=top><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/video/141/ref=pd_nfy_gw_n5/103-3111065-2579065><img src="http://g-images.amazon.com/images/G/01/icons/small-blue-vhs-icon.gif" width=18 height=18 border=0 alt=Icon ></a></td><td>
<a href=/exec/obidos/tg/new-for-you/new-releases/-/video/141/ref=pd_nfy_gw_n5/103-3111065-2579065><font face=verdana,arial,helvetica size=-1>Action & Adventure</font></a>
</td></tr>
</td></tr>
<tr><td colspan=2 align=left> <img src="http://g-images.amazon.com/images/G/01/icons/orange-arrow.gif" width=10 height=9 border=0> <a href=/exec/obidos/tg/new-for-you/new-releases/-/main/ref=pd_nfy_gw_n/103-3111065-2579065><font face=verdana,arial,helvetica size=-1><b>More New Releases</b></font></a><p>
</td></tr></table>
<table border=0 cellpadding=3 cellspacing=0 width=100% bgcolor=#ffffff>
<tr bgcolor=#eeeecc><td class=small>
<a href=/exec/obidos/tg/new-for-you/movers-and-shakers/-/books/ref=pd_gw_msgr/103-3111065-2579065><b>Movers &amp; Shakers</b></a>
</td></tr></table>
<table border=0 cellpadding=2 cellspacing=0 width=100% bgcolor=#ffffff vspace=0>
<tr><td valign=top align=center>
<img src="http://g-images.amazon.com/images/G/01/icons/uparrow_green2.gif" width=13 height=11 alt="Up">
</td>
<td valign=top>
<font color=#339900 face=verdana,arial,helvetica size=-1><b>974%</b></font> </td></tr>
<tr><td valign=top align=left>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-dvd-icon.gif" width=18 height=18 border=0 alt=Icon >
</td>
<td valign=top>
<font face=verdana,arial,helvetica size=-1><a href=/exec/obidos/tg/new-for-you/movers-and-shakers/-/dvd/ref=pd_gw_msd2/103-3111065-2579065>Dorothy L. Sayers Mysteries (Strong Poison / Have His Carcass / Gaudy Night)</a>
<font face=verdana,arial,helvetica size=-1>
<b>DVD</b>
<br>~ Dorothy L. Sayers
</font>
</font>
</td></tr>
<tr><td valign=top align=center>
<img src="http://g-images.amazon.com/images/G/01/icons/uparrow_green2.gif" width=13 height=11 alt="Up">
</td>
<td valign=top>
<font color=#339900 face=verdana,arial,helvetica size=-1><b>2,415%</b></font> </td></tr>
<tr><td valign=top align=left>
<img src="http://g-images.amazon.com/images/G/01/icons/small-blue-books-icon.gif" width=18 height=18 border=0 alt=Icon >
</td>
<td valign=top>
<font face=verdana,arial,helvetica size=-1><a href=/exec/obidos/tg/new-for-you/movers-and-shakers/-/books/ref=pd_gw_msb2/103-3111065-2579065>Artemis Fowl</a>
<br><font face=verdana,arial,helvetica size=-1>by Eoin Colfer</font>
</font>
</td></tr>
<tr><td colspan=2>
<img src="http://g-images.amazon.com/images/G/01/icons/orange-arrow.gif" width=10 height=9 border=0> <font face=verdana,arial,helvetica size=-1><b><a href=/exec/obidos/tg/new-for-you/movers-and-shakers/-/books/ref=pd_gw_msgr/103-3111065-2579065>More Movers & Shakers</a></b>
<br>
</td></tr></table>
</td></tr></table>
</td></tr></table>
</td></tr></table>
<br clear="all">
<center>
<form method="post" action="/exec/obidos/search-handle-form/103-3111065-2579065">
<table border=0 width=100% cellpadding=1 cellspacing=0 bgcolor=#999999>
<tr><td>
<table border=0 width=100% bgcolor=#ffffff cellspacing=0 cellpadding=5 class="small">
<tr valign=top><td width=33% class="small">
<b>Where's My Stuff?</b><br>
&#149; Track your <a href="/exec/obidos/flex-sign-in/ref=hy_f_1/103-3111065-2579065?opt=ab&page=help/ya-sign-in-secure.html&response=order-history-filtered&method=POST&ss-order-filter=wheres-my-stuff&return-url=order-history-filtered">recent orders</a>.<br>
&#149; View or change your orders in <a href="/exec/obidos/account-access-login/ref=hy_f_2/103-3111065-2579065">Your Account</a>.
<script language="JavaScript1.1" type="text/javascript">
<!--
var agt=navigator.userAgent.toLowerCase();
var is_major = parseInt(navigator.appVersion);
var is_nav = ((agt.indexOf('mozilla')!=-1) && (agt.indexOf('spoofer')==-1)
&& (agt.indexOf('compatible') == -1) && (agt.indexOf('opera')==-1)
&& (agt.indexOf('webtv')==-1) && (agt.indexOf('hotjava')==-1));
var is_gecko = (agt.indexOf('gecko') != -1);
var is_ie = ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1));
var is_aol = (agt.indexOf("aol") != -1);
var is_opera = (agt.indexOf("opera") != -1);
var is_win = ( (agt.indexOf("win")!=-1) || (agt.indexOf("16bit")!=-1) );
//-->
</script>
<script language="JavaScript1.1" type="text/javascript">
<!--
var OpenedWin;
function openWin (URL, width, height) {
OpenedWin = window.open(URL, "demo_window", "width="+width+",height="+height+",status=no,menubar=no,location=no,toolbar=no,directories=no,scrollbars=no");
if (! is_aol) {
var NewX = (screen.availWidth/2)-(width/2);
var NewY = (screen.availHeight/2)-(height/2);
OpenedWin.moveTo(NewX, NewY);
NewX = null;
NewY = null;
}
}
function launch (URL, width, height) {
if (!URL || !width || !height) {
alert("Error");
} else if (width>screen.availWidth || height>screen.availHeight) {
var message;
message = "Your screen resolution is too low to display the demo.\nClick 'OK' if you wish to continue anyway.\n";
message += '\n Your screen resolution: '+screen.width+' x '+screen.height;
message += ' | Viewable: '+screen.availWidth+' x '+screen.availHeight;
message += '\n Required: '+width+' x '+height;
if (confirm(message)) {
message = "If you can not find the close buttons, use your keyboard:\n";
message += 'Windows: ALT+F4\n';
message += 'Macintosh: CONTROL+W';
alert(message);
openWin(URL, width, height);
}
} else {
openWin(URL, width, height);
}
}
function displayLink(text){
if ( is_major >= 4 && is_win && ( is_nav || is_ie || is_opera || is_gecko ) ) {
document.write(text);
};
}
//-->
</script>
<script language="JavaScript1.1" type="text/javascript">
<!--
displayLink('<br>&#149; See our <b><a href=javascript:launch(\'/exec/obidos/subst/help/demo-wms/display-demo.html/ref=hy_f_demo/103-3111065-2579065\',788,444)>animated demo</a></b>!');
//-->
</script>
</td>
<td width=33% class="small">
<b>Shipping &amp; Returns</b><br>
&#149; See our <a href="/exec/obidos/tg/browse/-/468520/ref=hy_f_3/103-3111065-2579065">shipping rates &amp; policies</a>.<br>
&#149; <a href="/exec/obidos/subst/help/self-service-returns.html/ref=hy_f_4/103-3111065-2579065">Return</a> an item (here's our <a href="/exec/obidos/tg/browse/-/468532/103-3111065-2579065">Returns Policy</a>).
</td>
<td width=33% class="small">
<b>Need Help?</b><br>
&#149; Forgot your password? <a href="/exec/obidos/self-service-forgot-password-get-email/ref=hy_f_6/103-3111065-2579065">Click here</a>.
<br>
&#149; <a href="/exec/obidos/subst/gifts/gift-certificates/gc-redeeming.html/ref=hy_f_7/103-3111065-2579065">Redeem</a> or <a href="/exec/obidos/subst/gifts/gift-services/gift-certificates.html/ref=hy_f_8/103-3111065-2579065">buy</a> a gift certificate.<br>
&#149; <a href="/exec/obidos/tg/browse/-/508510/ref=hy_f_9/103-3111065-2579065">Visit our Help department</a>. <br>
</td></tr>
</table>
</td></tr>
<tr><td>
<table border=0 width=100% bgcolor=#FFCC66 cellspacing=0 cellpadding=5>
<tr><td align=center class="small">
<b>Search&nbsp;</b>
<select name=index>
<option value=blended selected>All Products
<option value=books>Books
<option value=music>Popular Music
<option value=music-dd>Music Downloads
<option value=classical>Classical Music
<option value="dvd">DVD
<option value="vhs">VHS
<option value=theatrical>Movie Showtimes
<option value=toys>Toys
<option value=baby>Baby
<option value=pc-hardware>Computers
<option value=videogames>Video Games
<option value=electronics>Electronics
<option value=photo>Camera &amp; Photo
<option value=software>Software
<option value=tools>Tools &amp; Hardware
<option value=magazines>Magazines
<option value=garden>Outdoor Living
<option value=kitchen>Kitchen
<option value=travel>Travel
<option value=wireless-phones>Cell Phones & Service
<option value=outlet>Outlet
<option value=auction-redirect>Auctions
<option value=fixed-price-redirect>zShops
</select>
<b>&nbsp;&nbsp;for&nbsp;&nbsp;</b>
<input type="text" name="field-keywords" size="15">&nbsp;&nbsp;
<input type=image name="Go" value="Go!" border=0 alt="Go!" src=http://g-images.amazon.com/images/G/01/v9/search-browse/go-button-gateway.gif width=21 height=21 border=0 align=absmiddle > </td></tr></table>
</td></tr>
</table>
</form>
<p align=center>
<b class=h1>Stuart D. Gathman, make </b><font color=#990000><b class=sans>$</b><b class=sans>310.61</b></font><br />
<b class=sans>Sell <a href="/exec/obidos/flex-sign-in/ref=sdp_bbump_gw/103-3111065-2579065?opt=an&page=misc/login/flex-sign-in-secure.html&response=tg/stores/static/-/used/sell-your-collection/1/">your past purchases</a> at Amazon.com today!</b>
</p>
<table width="100%">
<tr>
<td width="50%" valign="top" align="left">
<span class="small"><a href=/exec/obidos/change-style/subst/home/redirect.html/103-3111065-2579065>Text Only</a></span>
</td>
<td width="50%" valign="top" align="right" class="small">
<a href="#top">Top of Page</a>
</td>
</tr>
</table>
<center>
<p>
<a href=/exec/obidos/subst/home/all-stores.html/ref=gw_bt_st/103-3111065-2579065>Directory of All Stores</a><p>
Our International Sites:
<a href="/exec/obidos/redirect-to-external-url/ref=gw_bt_uk/103-3111065-2579065?path=http%3A//www.amazon.co.uk/exec/obidos/redirect-home%3Ftag%3Dintl-usgt-ukhome-21%26site%3Damazon">United Kingdom</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="/exec/obidos/redirect-to-external-url/ref=gw_bt_de/103-3111065-2579065?path=http%3A//www.amazon.de/exec/obidos/redirect-home%3Ftag%3Dintl-usgt-dehome-21%26site%3Dhome">Germany</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="/exec/obidos/redirect-to-external-url/ref=gw_bt_jp/103-3111065-2579065?path=http%3A//www.amazon.co.jp/exec/obidos/redirect-home%3Ftag%3Dintl-usgatew-jphome-22%26site%3Damazon">Japan</a>
&nbsp;&nbsp|&nbsp;&nbsp;
<a href="/exec/obidos/redirect-to-external-url/ref=gw_bt_fr/103-3111065-2579065?path=http%3A//www.amazon.fr/exec/obidos/redirect-home%3Fsite%3Damazon%26tag%3Dusfr-gatew-footer-21">France</a>
<p>
<a href=/exec/obidos/tg/browse/-/508510/ref=gw_bt_he/103-3111065-2579065>Help</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href=/exec/obidos/shopping-basket/ref=gw_bt_sc/103-3111065-2579065>Shopping Cart</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href=/exec/obidos/account-access-login/ref=gw_bt_ya/103-3111065-2579065>Your Account</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="http://s1.amazon.com/exec/varzea/ts/announcement-list-zshops/slp/ref=gw_bt_si/103-3111065-2579065">Sell Items</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="/exec/obidos/flex-sign-in/ref=gw_bt_oc/103-3111065-2579065?opt=a&page=ordering/one-click-address-sign-in-secure.html&response=one-click-main&method=GET&return-url=one-click-main">1-Click Settings</a>
<p>
<a href=/exec/obidos/subst/misc/company-info.html/ref=gw_bt_aa/103-3111065-2579065>About Amazon.com</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href=/exec/obidos/tg/stores/job-listings/-/generic/home/103-3111065-2579065>Join Our Staff</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="/exec/obidos/subst/associates/join/associates.html/ref=gw_bt_as/103-3111065-2579065">Join Associates</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href=/exec/obidos/subst/partners/direct/direct-application.html/ref=gw_bt_ad/103-3111065-2579065>Join Advantage</a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="http://s1.amazon.com/exec/varzea/subst/fx/home.html/ref=gw_bt_hs/103-3111065-2579065">Join Honor System</a>
</center>
<center>
<p>
<div class="tiny" align=center>
<A HREF="/exec/obidos/subst/misc/policy/conditions-of-use.html/103-3111065-2579065">Conditions of Use</A> | <A HREF="/exec/obidos/tg/browse/-/468496/103-3111065-2579065">Privacy Notice</A> &copy; 1996-2002, Amazon.com, Inc. or its affiliates
</div>
</center>
<!-- whfhYn47qD1fv3PW2R8XWAkFcMwteHFKxorD -->
</body>
</html>
--------------59A46341C90BA737DD47867B--
-18587
View File
File diff suppressed because it is too large Load Diff
-55
View File
@@ -1,55 +0,0 @@
import unittest
import doctest
import os
#from Milter.greylist import Greylist
from Milter.greysql import Greylist
class GreylistTestCase(unittest.TestCase):
def setUp(self):
self.fname = 'test.db'
os.remove(self.fname)
def tearDown(self):
#os.remove(self.fname)
pass
def testGrey(self):
grey = Greylist(self.fname)
# first time
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com')
self.assertEqual(rc,0)
# not in window yet
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*60)
self.assertEqual(rc,0)
# within window
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=15*60)
self.assertEqual(rc,1)
# new triple
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=15*60)
self.assertEqual(rc,0)
# seen again
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*3600)
self.assertEqual(rc,2)
# new one past expire
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=6*3600)
self.assertEqual(rc,0)
# original past retain
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=37*24*3600)
self.assertEqual(rc,0)
# new one for testing expire
rc = grey.check('1.2.3.5','flub@bar.com','baz@spat.com',timeinc=20*24*3600)
self.assertEqual(rc,0)
grey.close()
# test cleanup
grey = Greylist(self.fname)
rc = grey.clean(timeinc=37*24*3600)
self.assertEqual(rc,1)
grey.close()
def suite():
s = unittest.makeSuite(GreylistTestCase,'test')
return s
if __name__ == '__main__':
unittest.TextTestRunner().run(suite())
+4 -36
View File
@@ -1,10 +1,4 @@
# $Log$ # $Log$
# Revision 1.5 2011/06/09 17:27:42 customdesigned
# Documentation updates.
#
# Revision 1.4 2005/07/20 14:49:44 customdesigned
# Handle corrupt and empty ZIP files.
#
# Revision 1.3 2005/06/17 01:49:39 customdesigned # Revision 1.3 2005/06/17 01:49:39 customdesigned
# Handle zip within zip. # Handle zip within zip.
# #
@@ -32,7 +26,6 @@ import socket
import StringIO import StringIO
import email import email
import sys import sys
import Milter
from email import Errors from email import Errors
samp1_txt1 = """Dear Agent 1 samp1_txt1 = """Dear Agent 1
@@ -72,12 +65,12 @@ class MimeTestCase(unittest.TestCase):
# python 2.4 doesn't get exceptions on missing boundaries, and # python 2.4 doesn't get exceptions on missing boundaries, and
# if message is modified, output is readable by mail clients # if message is modified, output is readable by mail clients
if sys.hexversion < 0x02040000: if sys.hexversion < 0x02040000:
self.fail('should get boundary error parsing bad rfc822 attachment') self.fail('should get boundary error parsing bad rfc822 attachment')
except Errors.BoundaryError: except Errors.BoundaryError:
pass pass
def testDefang(self,vname='virus1',part=1, def testDefang(self,vname='virus1',part=1,
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'): fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
msg = mime.message_from_file(open('test/'+vname,"r")) msg = mime.message_from_file(open('test/'+vname,"r"))
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.failUnless(msg.ismodified(),"virus not removed") self.failUnless(msg.ismodified(),"virus not removed")
@@ -111,7 +104,7 @@ class MimeTestCase(unittest.TestCase):
self.failIf(msg.ismultipart()) self.failIf(msg.ismultipart())
txt2 = msg.get_payload() txt2 = msg.get_payload()
self.failUnless(txt2 == mime.virus_msg % \ self.failUnless(txt2 == mime.virus_msg % \
(fname,hostname,None),txt2) (fname,hostname,None),txt2)
# honey virus has a sneaky ASP payload which is parsed correctly # honey virus has a sneaky ASP payload which is parsed correctly
# by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1 # by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1
@@ -125,7 +118,7 @@ class MimeTestCase(unittest.TestCase):
txt2 = parts[1].get_payload() txt2 = parts[1].get_payload()
txt3 = parts[2].get_payload() txt3 = parts[2].get_payload()
self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % \ self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % \
(fname,hostname,None),txt2) (fname,hostname,None),txt2)
if txt3 != '': if txt3 != '':
self.failUnless(txt3.rstrip()+'\n' == mime.virus_msg % \ self.failUnless(txt3.rstrip()+'\n' == mime.virus_msg % \
('story[1].asp',hostname,None),txt3) ('story[1].asp',hostname,None),txt3)
@@ -153,31 +146,6 @@ class MimeTestCase(unittest.TestCase):
# test zip within zip # test zip within zip
self.testDefang('ziploop',1,'stuart@bmsi.com.zip') self.testDefang('ziploop',1,'stuart@bmsi.com.zip')
def _chk_name(self,name):
self.filename = name
def _chk_attach(self,msg):
"Filter attachments by content."
# check for bad extensions
mime.check_name(msg,ckname=self._chk_name,scan_zip=True)
# remove scripts from HTML
mime.check_html(msg)
# don't let a tricky virus slip one past us
msg = msg.get_submsg()
if isinstance(msg,email.Message.Message):
return mime.check_attachments(msg,self._chk_attach)
return Milter.CONTINUE
def testCheckAttach(self,fname="test1"):
# test1 contains a very long filename
msg = mime.message_from_file(open('test/'+fname,'r'))
mime.defang(msg,scan_zip=True)
self.failIf(msg.ismodified())
msg = mime.message_from_file(open('test/test2','r'))
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(rc,Milter.CONTINUE)
def testHTML(self,fname=""): def testHTML(self,fname=""):
result = StringIO.StringIO() result = StringIO.StringIO()
filter = mime.HTMLScriptFilter(result) filter = mime.HTMLScriptFilter(result)
+94 -10
View File
@@ -4,12 +4,96 @@ import sample
import mime import mime
import rfc822 import rfc822
import StringIO import StringIO
from Milter.test import TestBase
class TestMilter(TestBase,sample.sampleMilter): class TestMilter(sample.sampleMilter):
def __init__(self): def __init__(self):
TestBase.__init__(self) self.logfp = open("test/milter.log","a")
sample.sampleMilter.__init__(self)
def log(self,*msg):
for i in msg: print >>self.logfp, i,
print >>self.logfp
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self.bodyreplaced = True
else:
raise IOError,"replacebody not called from eom()"
# FIXME: rfc822 indexing does not really reflect the way chg/add header
# work for a milter
def chgheader(self,field,idx,value):
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
if value == '':
del self._msg[field]
else:
self._msg[field] = value
self.headerschanged = True
def addheader(self,field,value):
self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value
self.headerschanged = True
def feedMsg(self,fname):
self._body = None
self.bodyreplaced = False
self.headerschanged = 0
fp = open('test/'+fname,'r')
msg = rfc822.Message(fp)
rc = self.envfrom('<spam@advertisements.com>')
if rc != Milter.CONTINUE: return rc
rc = self.envrcpt('<victim@lamb.com>')
if rc != Milter.CONTINUE: return rc
line = None
for h in msg.headers:
if h[:1].isspace():
line = line + h
continue
if not line:
line = h
continue
s = line.split(': ',1)
rc = self.header(s[0],s[1].strip())
if rc != Milter.CONTINUE: return rc
line = h
if line:
s = line.split(': ',1)
rc = self.header(s[0],s[1])
if rc != Milter.CONTINUE: return rc
rc = self.eoh()
if rc != Milter.CONTINUE: return rc
while 1:
buf = fp.read(8192)
if len(buf) == 0: break
rc = self.body(buf)
if rc != Milter.CONTINUE: return rc
self._msg = msg
self._body = StringIO.StringIO()
rc = self.eom()
if self.bodyreplaced:
body = self._body.getvalue()
else:
msg.rewindbody()
body = msg.fp.read()
self._body = StringIO.StringIO()
self._body.writelines(msg.headers)
self._body.write('\n')
self._body.write(body)
return rc
def connect(self,host='localhost'):
self._body = None
self.bodyreplaced = False
rc = sample.sampleMilter.connect(self,host,1,0)
if rc != Milter.CONTINUE and rc != Milter.ACCEPT:
self.close()
return rc
rc = self.hello('spamrelay')
if rc != Milter.CONTINUE:
self.close()
return rc
class BMSMilterTestCase(unittest.TestCase): class BMSMilterTestCase(unittest.TestCase):
@@ -19,7 +103,7 @@ class BMSMilterTestCase(unittest.TestCase):
self.failUnless(rc == Milter.CONTINUE) self.failUnless(rc == Milter.CONTINUE)
rc = milter.feedMsg(fname) rc = milter.feedMsg(fname)
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter._bodyreplaced,"Message body not replaced") self.failUnless(milter.bodyreplaced,"Message body not replaced")
fp = milter._body fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue()) open('test/'+fname+".tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus1.out","r").read()) #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
@@ -34,7 +118,7 @@ class BMSMilterTestCase(unittest.TestCase):
milter.connect('somehost') milter.connect('somehost')
rc = milter.feedMsg(fname) rc = milter.feedMsg(fname)
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
fp = milter._body fp = milter._body
open('test/'+fname+".tstout","w").write(fp.getvalue()) open('test/'+fname+".tstout","w").write(fp.getvalue())
milter.close() milter.close()
@@ -44,17 +128,17 @@ class BMSMilterTestCase(unittest.TestCase):
milter.connect('somehost') milter.connect('somehost')
rc = milter.feedMsg('samp1') rc = milter.feedMsg('samp1')
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.") self.failIf(milter.bodyreplaced,"Milter needlessly replaced body.")
rc = milter.feedMsg("virus3") rc = milter.feedMsg("virus3")
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter._bodyreplaced,"Message body not replaced") self.failUnless(milter.bodyreplaced,"Message body not replaced")
fp = milter._body fp = milter._body
open("test/virus3.tstout","w").write(fp.getvalue()) open("test/virus3.tstout","w").write(fp.getvalue())
#self.failUnless(fp.getvalue() == open("test/virus3.out","r").read()) #self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
rc = milter.feedMsg("virus6") rc = milter.feedMsg("virus6")
self.failUnless(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.failUnless(milter._bodyreplaced,"Message body not replaced") self.failUnless(milter.bodyreplaced,"Message body not replaced")
self.failUnless(milter._headerschanged,"Message headers not adjusted") self.failUnless(milter.headerschanged,"Message headers not adjusted")
fp = milter._body fp = milter._body
open("test/virus6.tstout","w").write(fp.getvalue()) open("test/virus6.tstout","w").write(fp.getvalue())
milter.close() milter.close()