Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d71095dbac | |||
| 5290bc0668 | |||
| 92ad624c3b | |||
| 7c5899b0cd | |||
| c6ccea9099 | |||
| eea110d120 | |||
| 4b2c08c0cf | |||
| 953e8a61fa | |||
| fa4408540e | |||
| 65986632de | |||
| e44321561b | |||
| 344ee43f22 | |||
| 99bf3209c6 | |||
| 2848a090e3 | |||
| c29a21d2dd | |||
| 25a02d9de2 | |||
| c20e82e3d4 | |||
| a3889189f0 | |||
| f86bda2ba4 | |||
| 3ed14cc6ab | |||
| aeff1f8ab5 | |||
| a7bd7b71d8 | |||
| 939fc61df7 | |||
| f6a3b57fb9 | |||
| 3428477eca | |||
| 144fe264c4 | |||
| a3530d4c49 | |||
| 307c54e1b1 | |||
| 66f8a1d437 | |||
| 73e1f469ce | |||
| 2e45d6e187 | |||
| 6a1996117c | |||
| 77c0ce6b2e | |||
| 7311f65150 | |||
| 84bd61aac1 | |||
| 372fad6ac9 | |||
| 60963b3c37 | |||
| 6221f8b753 | |||
| 344ecc7c07 | |||
| ee14614c3e | |||
| 4bb2403223 | |||
| d58546930a | |||
| f8efbb23df | |||
| 26b006455e | |||
| 9b7ca633f3 | |||
| 5928e99520 | |||
| 6d3833da72 | |||
| 2937935fea | |||
| 31aa39034b | |||
| cb31963492 | |||
| ed17f9cecf | |||
| 0e1a2de41f | |||
| 9f419e3fc8 | |||
| 6913fd3e66 | |||
| 780ac63ebe | |||
| b51c08ba3a | |||
| 2e7805e531 | |||
| b1eae98453 | |||
| 9118364164 |
@@ -7,6 +7,13 @@ 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
|
||||||
|
for suggesting a class method to compute and cache protocol masks
|
||||||
|
habnabit@habnabit.org
|
||||||
|
for suggesting function attributes and decorators for protocol negotiation
|
||||||
Dwayne Litzenberger, B.A.Sc.
|
Dwayne Litzenberger, B.A.Sc.
|
||||||
for library_dirs patch to compile on Debian
|
for library_dirs patch to compile on Debian
|
||||||
Dave MacQuigg
|
Dave MacQuigg
|
||||||
|
|||||||
+437
-63
@@ -1,22 +1,20 @@
|
|||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
## @package Milter
|
||||||
# Copyright 2001 Business Management Systems, Inc.
|
# A thin OO wrapper for the milter module.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
# A thin OO wrapper for the milter module
|
__version__ = '0.9.3'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import milter
|
import milter
|
||||||
import thread
|
import thread
|
||||||
|
|
||||||
from milter import ACCEPT,CONTINUE,REJECT,DISCARD,TEMPFAIL, \
|
from milter import *
|
||||||
set_flags, setdbg, setbacklog, settimeout, error, \
|
|
||||||
ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS, \
|
|
||||||
V1_ACTS, V2_ACTS, CURR_ACTS
|
|
||||||
|
|
||||||
try: from milter import QUARANTINE
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
__version__ = '0.8.5'
|
|
||||||
|
|
||||||
_seq_lock = thread.allocate_lock()
|
_seq_lock = thread.allocate_lock()
|
||||||
_seq = 0
|
_seq = 0
|
||||||
@@ -30,30 +28,391 @@ def uniqueID():
|
|||||||
_seq_lock.release()
|
_seq_lock.release()
|
||||||
return seqno
|
return seqno
|
||||||
|
|
||||||
class Milter:
|
OPTIONAL_CALLBACKS = {
|
||||||
"""A simple class interface to the milter module.
|
'connect':(P_NR_CONN,P_NOCONNECT),
|
||||||
"""
|
'hello':(P_NR_HELO,P_NOHELO),
|
||||||
|
'envfrom':(P_NR_MAIL,P_NOMAIL),
|
||||||
|
'envrcpt':(P_NR_RCPT,P_NORCPT),
|
||||||
|
'data':(P_NR_DATA,P_NODATA),
|
||||||
|
'unknown':(P_NR_UNKN,P_NOUNKNOWN),
|
||||||
|
'eoh':(P_NR_EOH,P_NOEOH),
|
||||||
|
'body':(P_NR_BODY,P_NOBODY),
|
||||||
|
'header':(P_NR_HDR,P_NOHDRS)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
# milter 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. The <code>_protocol</code>
|
||||||
|
# member 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.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
## Function decorator to disable callback methods.
|
||||||
|
# If the MTA supports it, tells the MTA not to call 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.
|
||||||
|
# @since 0.9.2
|
||||||
|
def nocallback(func):
|
||||||
|
try:
|
||||||
|
func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][1]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(
|
||||||
|
'@nocallback applied to non-optional method: '+func.__name__)
|
||||||
|
return func
|
||||||
|
|
||||||
|
## 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):
|
||||||
|
try:
|
||||||
|
nr_mask = OPTIONAL_CALLBACKS[func.__name__][0]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(
|
||||||
|
'@noreply applied to non-optional method: '+func.__name__)
|
||||||
|
def wrapper(self,*args):
|
||||||
|
rc = func(self,*args)
|
||||||
|
if self._protocol & nr_mask: return NOREPLY
|
||||||
|
return rc
|
||||||
|
wrapper.milter_protocol = nr_mask
|
||||||
|
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):
|
||||||
|
pass
|
||||||
|
|
||||||
|
## A do "nothing" Milter base class.
|
||||||
|
# Python milters should derive from this class
|
||||||
|
# unless they are using the low lever milter module directly.
|
||||||
|
# All optional callbacks are disabled, and automatically
|
||||||
|
# reenabled when overridden.
|
||||||
|
# @since 0.9.2
|
||||||
|
class Base(object):
|
||||||
|
"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):
|
||||||
self.__ctx = ctx
|
self._ctx = ctx
|
||||||
|
self._actions = CURR_ACTS # all actions enabled by default
|
||||||
|
self._protocol = 0 # no protocol options by default
|
||||||
if ctx:
|
if ctx:
|
||||||
ctx.setpriv(self)
|
ctx.setpriv(self)
|
||||||
|
## @var _actions
|
||||||
|
# A bitmask of actions this milter has negotiated to use.
|
||||||
|
# By default, all actions are enabled. This may be changed
|
||||||
|
# by calling <code>milter.set_flags</code>, or by overriding
|
||||||
|
# the negotiate callback. The bits include:
|
||||||
|
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
|
||||||
|
# CHGHDRS,QUARANTINE,CHGFROM,SETSMLIST</code>.
|
||||||
|
# The <code>Milter.CURR_ACTS</code> bitmask is all actions
|
||||||
|
# known when the milter module was compiled.
|
||||||
|
# @since 0.9.2
|
||||||
|
#
|
||||||
|
|
||||||
# user replaceable callbacks
|
## @var _protocol
|
||||||
|
# A bitmask of protocol options this milter has negotiated.
|
||||||
|
# The bits generally indicate that a particular step should be
|
||||||
|
# skipped, since previous versions of the milter protocol had
|
||||||
|
# no provision for skipping steps.
|
||||||
|
# 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
|
||||||
|
|
||||||
|
## Defined by subclasses to write log messages.
|
||||||
|
def log(self,*msg): pass
|
||||||
|
## Called for each connection to the MTA.
|
||||||
|
# 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>
|
||||||
|
# @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
|
||||||
|
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
|
||||||
|
def hello(self,hostname): return CONTINUE
|
||||||
|
## Called when the SMTP client says MAIL FROM.
|
||||||
|
# Returning REJECT rejects the message, but not the connection.
|
||||||
|
@nocallback
|
||||||
|
def envfrom(self,f,*str): return CONTINUE
|
||||||
|
## Called when the SMTP client says RCPT TO.
|
||||||
|
# Returning REJECT rejects the current recipient, not the entire message.
|
||||||
|
@nocallback
|
||||||
|
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
|
||||||
|
def data(self): return CONTINUE
|
||||||
|
## Called for each header field in the message body.
|
||||||
|
@nocallback
|
||||||
|
def header(self,field,value): return CONTINUE
|
||||||
|
## Called at the blank line that terminates the header fields.
|
||||||
|
@nocallback
|
||||||
|
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
|
||||||
|
def body(self,blk): return CONTINUE
|
||||||
|
## Called when the SMTP client issues an unknown command.
|
||||||
|
# @param cmd the unknown command
|
||||||
|
# @since 0.9.2
|
||||||
|
@nocallback
|
||||||
|
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
|
||||||
|
## Called when the connection is abnormally terminated.
|
||||||
|
# The close callback is still called also.
|
||||||
|
def abort(self): return CONTINUE
|
||||||
|
## Called when the connection is closed.
|
||||||
|
def close(self): return CONTINUE
|
||||||
|
|
||||||
|
## 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
|
||||||
|
def protocol_mask(klass):
|
||||||
|
try:
|
||||||
|
return klass._protocol_mask
|
||||||
|
except AttributeError:
|
||||||
|
p = P_RCPT_REJ | P_HDR_LEADSPC # turn these new features off by default
|
||||||
|
for func,(nr,nc) in OPTIONAL_CALLBACKS.items():
|
||||||
|
func = getattr(klass,func)
|
||||||
|
ca = getattr(func,'milter_protocol',0)
|
||||||
|
#print func,hex(nr),hex(nc),hex(ca)
|
||||||
|
p |= (nr|nc) & ~ca
|
||||||
|
klass._protocol_mask = p
|
||||||
|
return p
|
||||||
|
|
||||||
|
## Negotiate milter protocol options.
|
||||||
|
# Default negotiation sets P_NO* and P_NR* for callbacks
|
||||||
|
# marked @@nocallback and @@noreply respectively, leaves all
|
||||||
|
# actions enabled, and enables Milter.SKIP.
|
||||||
|
# @since 0.9.2
|
||||||
|
def negotiate(self,opts):
|
||||||
|
try:
|
||||||
|
self._actions,p,f1,f2 = opts
|
||||||
|
opts[1] = self._protocol = p & ~self.protocol_mask()
|
||||||
|
opts[2] = 0
|
||||||
|
opts[3] = 0
|
||||||
|
#self.log("Negotiated:",opts)
|
||||||
|
except:
|
||||||
|
# don't change anything if something went wrong
|
||||||
|
return ALL_OPTS
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# @param sym the macro name
|
||||||
|
def getsymval(self,sym):
|
||||||
|
return self._ctx.getsymval(sym)
|
||||||
|
|
||||||
|
## Set the SMTP reply code and message.
|
||||||
|
# If the MTA does not support setmlreply, then only the
|
||||||
|
# first msg line is used.
|
||||||
|
def setreply(self,rcode,xcode=None,msg=None,*ml):
|
||||||
|
return self._ctx.setreply(rcode,xcode,msg,*ml)
|
||||||
|
|
||||||
|
## Tell the MTA which macro names will be used.
|
||||||
|
# The <code>Milter.SETSMLIST</code> action flag must be set.
|
||||||
|
#
|
||||||
|
# May only be called from negotiate callback.
|
||||||
|
# @since 0.9.2
|
||||||
|
# @param stage the protocol stage to set to macro list for
|
||||||
|
# @param macros a string with a space delimited list of macros
|
||||||
|
def setsmlist(self,stage,macros):
|
||||||
|
if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST")
|
||||||
|
if type(macros) in (list,tuple):
|
||||||
|
macros = ' '.join(macros)
|
||||||
|
return self._ctx.setsmlist(stage,macros)
|
||||||
|
|
||||||
|
# Milter methods which can only be called from eom callback.
|
||||||
|
|
||||||
|
## Add a mail header field.
|
||||||
|
# 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
|
||||||
|
def addheader(self,field,value,idx=-1):
|
||||||
|
if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS")
|
||||||
|
return self._ctx.addheader(field,value,idx)
|
||||||
|
|
||||||
|
## Change the value of a mail header field.
|
||||||
|
# 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
|
||||||
|
def chgheader(self,field,idx,value):
|
||||||
|
if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS")
|
||||||
|
return self._ctx.chgheader(field,idx,value)
|
||||||
|
|
||||||
|
## Add a recipient to the message.
|
||||||
|
# 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
|
||||||
|
def addrcpt(self,rcpt,params=None):
|
||||||
|
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)
|
||||||
|
## Delete a recipient from the message.
|
||||||
|
# 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
|
||||||
|
def delrcpt(self,rcpt):
|
||||||
|
if not self._actions & DELRCPT: raise DisabledAction("DELRCPT")
|
||||||
|
return self._ctx.delrcpt(rcpt)
|
||||||
|
|
||||||
|
## Replace the message body.
|
||||||
|
# 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
|
||||||
|
def replacebody(self,body):
|
||||||
|
if not self._actions & MODBODY: raise DisabledAction("MODBODY")
|
||||||
|
return self._ctx.replacebody(body)
|
||||||
|
|
||||||
|
## Change the SMTP envelope sender address.
|
||||||
|
# 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
|
||||||
|
def chgfrom(self,sender,params=None):
|
||||||
|
if not self._actions & CHGFROM: raise DisabledAction("CHGFROM")
|
||||||
|
return self._ctx.chgfrom(sender,params)
|
||||||
|
|
||||||
|
## Quarantine the message.
|
||||||
|
# When quarantined, a message goes into the mailq as if to be delivered,
|
||||||
|
# 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
|
||||||
|
def quarantine(self,reason):
|
||||||
|
if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE")
|
||||||
|
return self._ctx.quarantine(reason)
|
||||||
|
|
||||||
|
## Tell the MTA to wait a bit longer.
|
||||||
|
# Resets timeouts in the MTA that detect a "hung" milter.
|
||||||
|
def progress(self):
|
||||||
|
return self._ctx.progress()
|
||||||
|
|
||||||
|
## A logging but otherwise do nothing Milter base class.
|
||||||
|
# This is included for compatibility with previous versions of pymilter.
|
||||||
|
# The logging callbacks are marked @@noreply.
|
||||||
|
class Milter(Base):
|
||||||
|
"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,
|
||||||
print
|
print
|
||||||
|
|
||||||
|
@noreply
|
||||||
def connect(self,hostname,family,hostaddr):
|
def connect(self,hostname,family,hostaddr):
|
||||||
"Called for each connection to sendmail."
|
"Called for each connection to sendmail."
|
||||||
self.log("connect from %s at %s" % (hostname,hostaddr))
|
self.log("connect from %s at %s" % (hostname,hostaddr))
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
def hello(self,hostname):
|
def hello(self,hostname):
|
||||||
"Called after the HELO command."
|
"Called after the HELO command."
|
||||||
self.log("hello from %s" % hostname)
|
self.log("hello from %s" % hostname)
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
def envfrom(self,f,*str):
|
def envfrom(self,f,*str):
|
||||||
"""Called to begin each message.
|
"""Called to begin each message.
|
||||||
f -> string message sender
|
f -> string message sender
|
||||||
@@ -62,25 +421,24 @@ class Milter:
|
|||||||
self.log("mail from",f,str)
|
self.log("mail from",f,str)
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
def envrcpt(self,to,*str):
|
def envrcpt(self,to,*str):
|
||||||
"Called for each message recipient."
|
"Called for each message recipient."
|
||||||
self.log("rcpt to",to,str)
|
self.log("rcpt to",to,str)
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
def header(self,field,value):
|
def header(self,field,value):
|
||||||
"Called for each message header."
|
"Called for each message header."
|
||||||
self.log("%s: %s" % (field,value))
|
self.log("%s: %s" % (field,value))
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
def eoh(self):
|
def eoh(self):
|
||||||
"Called after all headers are processed."
|
"Called after all headers are processed."
|
||||||
self.log("eoh")
|
self.log("eoh")
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
|
|
||||||
def body(self,unused):
|
|
||||||
"Called to transfer the message body."
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
def eom(self):
|
def eom(self):
|
||||||
"Called at the end of message."
|
"Called at the end of message."
|
||||||
self.log("eom")
|
self.log("eom")
|
||||||
@@ -96,58 +454,46 @@ class Milter:
|
|||||||
self.log("close")
|
self.log("close")
|
||||||
return CONTINUE
|
return CONTINUE
|
||||||
|
|
||||||
# Milter methods which can be invoked from callbacks
|
## The milter connection factory
|
||||||
def getsymval(self,sym):
|
# This factory method is called for each connection to create the
|
||||||
return self.__ctx.getsymval(sym)
|
# python object that tracks the connection. It should return
|
||||||
|
# an object derived from Milter.Base.
|
||||||
# If sendmail does not support setmlreply, then only the
|
#
|
||||||
# first msg line is used.
|
# Note that since python is dynamic, this variable can be changed while
|
||||||
def setreply(self,rcode,xcode=None,msg=None,*ml):
|
# the milter is running: for instance, to a new subclass based on a
|
||||||
return self.__ctx.setreply(rcode,xcode,msg,*ml)
|
# change in configuration.
|
||||||
|
|
||||||
# Milter methods which can only be called from eom callback.
|
|
||||||
def addheader(self,field,value,idx=-1):
|
|
||||||
return self.__ctx.addheader(field,value,idx)
|
|
||||||
|
|
||||||
def chgheader(self,field,idx,value):
|
|
||||||
return self.__ctx.chgheader(field,idx,value)
|
|
||||||
|
|
||||||
def addrcpt(self,rcpt,params=None):
|
|
||||||
return self.__ctx.addrcpt(rcpt,params)
|
|
||||||
|
|
||||||
def delrcpt(self,rcpt):
|
|
||||||
return self.__ctx.delrcpt(rcpt)
|
|
||||||
|
|
||||||
def replacebody(self,body):
|
|
||||||
return self.__ctx.replacebody(body)
|
|
||||||
|
|
||||||
def chgfrom(self,sender,params=None):
|
|
||||||
return self.__ctx.chgfrom(sender,params)
|
|
||||||
|
|
||||||
# When quarantined, a message goes into the mailq as if to be delivered,
|
|
||||||
# but delivery is deferred until the message is unquarantined.
|
|
||||||
def quarantine(self,reason):
|
|
||||||
return self.__ctx.quarantine(reason)
|
|
||||||
|
|
||||||
def progress(self):
|
|
||||||
return self.__ctx.progress()
|
|
||||||
|
|
||||||
factory = Milter
|
factory = Milter
|
||||||
|
|
||||||
def connectcallback(ctx,hostname,family,hostaddr):
|
## @private
|
||||||
|
def negotiate_callback(ctx,opts):
|
||||||
|
m = factory()
|
||||||
|
m._setctx(ctx)
|
||||||
|
return m.negotiate(opts)
|
||||||
|
|
||||||
|
## @private
|
||||||
|
def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
|
||||||
|
m = ctx.getpriv()
|
||||||
|
if not m:
|
||||||
|
# If not already created (because the current MTA doesn't support
|
||||||
|
# xmfi_negotiate), create the connection object.
|
||||||
m = factory()
|
m = factory()
|
||||||
m._setctx(ctx)
|
m._setctx(ctx)
|
||||||
return m.connect(hostname,family,hostaddr)
|
return m.connect(hostname,family,hostaddr)
|
||||||
|
|
||||||
def closecallback(ctx):
|
## @private
|
||||||
|
def close_callback(ctx):
|
||||||
m = ctx.getpriv()
|
m = ctx.getpriv()
|
||||||
if not m: return CONTINUE
|
if not m: return CONTINUE
|
||||||
|
try:
|
||||||
rc = m.close()
|
rc = m.close()
|
||||||
|
finally:
|
||||||
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 parm list to keyword dictionary."
|
"Convert ESMTP parms with values to keyword dictionary."
|
||||||
kw = {}
|
kw = {}
|
||||||
for s in args:
|
for s in args:
|
||||||
pos = s.find('=')
|
pos = s.find('=')
|
||||||
@@ -155,6 +501,18 @@ 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
|
||||||
@@ -169,6 +527,11 @@ def envcallback(c,args):
|
|||||||
pargs.append(s)
|
pargs.append(s)
|
||||||
return c(*pargs,**kw)
|
return c(*pargs,**kw)
|
||||||
|
|
||||||
|
## Run the milter.
|
||||||
|
# @param name the name of the milter known by the MTA
|
||||||
|
# @param socketname the socket to be passed to <code>milter.setconn</code>
|
||||||
|
# @param timeout the time in secs the MTA should wait for a response before
|
||||||
|
# considering this milter dead
|
||||||
def runmilter(name,socketname,timeout = 0):
|
def runmilter(name,socketname,timeout = 0):
|
||||||
# This bit is here on the assumption that you will be starting this filter
|
# This bit is here on the assumption that you will be starting this filter
|
||||||
# before sendmail. If sendmail is not running and the socket already exists,
|
# before sendmail. If sendmail is not running and the socket already exists,
|
||||||
@@ -194,7 +557,7 @@ def runmilter(name,socketname,timeout = 0):
|
|||||||
|
|
||||||
# The default flags set include everything
|
# The default flags set include everything
|
||||||
# milter.set_flags(milter.ADDHDRS)
|
# milter.set_flags(milter.ADDHDRS)
|
||||||
milter.set_connect_callback(connectcallback)
|
milter.set_connect_callback(connect_callback)
|
||||||
milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host))
|
milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host))
|
||||||
# For envfrom and envrcpt, we would like to convert ESMTP parms to keyword
|
# For envfrom and envrcpt, we would like to convert ESMTP parms to keyword
|
||||||
# parms, but then all existing users would have to include **kw to accept
|
# parms, but then all existing users would have to include **kw to accept
|
||||||
@@ -207,12 +570,20 @@ def runmilter(name,socketname,timeout = 0):
|
|||||||
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
|
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
|
||||||
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
|
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
|
||||||
milter.set_abort_callback(lambda ctx: ctx.getpriv().abort())
|
milter.set_abort_callback(lambda ctx: ctx.getpriv().abort())
|
||||||
milter.set_close_callback(closecallback)
|
milter.set_close_callback(close_callback)
|
||||||
|
|
||||||
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(),
|
||||||
|
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
|
||||||
|
negotiate=ncb
|
||||||
|
)
|
||||||
start_seq = _seq
|
start_seq = _seq
|
||||||
try:
|
try:
|
||||||
milter.main()
|
milter.main()
|
||||||
@@ -225,3 +596,6 @@ __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
|
||||||
|
#
|
||||||
|
|||||||
+29
-16
@@ -1,12 +1,22 @@
|
|||||||
# provide a higher level interface to pydns
|
## @package Milter.dns
|
||||||
|
# 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()
|
||||||
@@ -24,25 +34,28 @@ 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')
|
||||||
|
))
|
||||||
|
|
||||||
SAFE2CACHE = {
|
## Cached DNS lookup.
|
||||||
('MX','A'): None,
|
# @param name the DNS label to query
|
||||||
('MX','MX'): None,
|
# @param qtype the query type, e.g. 'A'
|
||||||
('CNAME','A'): None,
|
# @param cnames tracks CNAMES already followed in recursive calls
|
||||||
('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.
|
||||||
|
|
||||||
|
|||||||
+71
-9
@@ -5,6 +5,21 @@
|
|||||||
# 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.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
|
||||||
|
# Allow arbitrary object, not just spf.query like, to provide data for create_msg
|
||||||
|
#
|
||||||
# Revision 1.15 2007/09/24 20:13:26 customdesigned
|
# Revision 1.15 2007/09/24 20:13:26 customdesigned
|
||||||
# Remove explicit spf dependency.
|
# Remove explicit spf dependency.
|
||||||
#
|
#
|
||||||
@@ -23,7 +38,31 @@
|
|||||||
# 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
|
||||||
@@ -31,12 +70,25 @@ import Milter
|
|||||||
import time
|
import time
|
||||||
import dns
|
import dns
|
||||||
|
|
||||||
def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None):
|
## 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=''):
|
||||||
"""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.split('@')
|
user,domain = mailfrom.rsplit('@',1)
|
||||||
if not session: session = dns.Session()
|
if not session: session = dns.Session()
|
||||||
try:
|
try:
|
||||||
mxlist = session.dns(domain,'MX')
|
mxlist = session.dns(domain,'MX')
|
||||||
@@ -62,21 +114,31 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None):
|
|||||||
raise smtplib.SMTPHeloError(code, resp)
|
raise smtplib.SMTPHeloError(code, resp)
|
||||||
if msg:
|
if msg:
|
||||||
try:
|
try:
|
||||||
smtp.sendmail('<>',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: <>')
|
code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom)
|
||||||
if code != 250:
|
if code != 250:
|
||||||
raise smtplib.SMTPSenderRefused(code, resp, '<>')
|
raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom)
|
||||||
code,resp = smtp.rcpt(mailfrom)
|
if isinstance(mailfrom,basestring):
|
||||||
|
mailfrom = [mailfrom]
|
||||||
|
badrcpts = {}
|
||||||
|
for rcpt in mailfrom:
|
||||||
|
code,resp = smtp.rcpt(rcpt)
|
||||||
if code not in (250,251):
|
if code not in (250,251):
|
||||||
return (code,resp) # permanent error
|
badrcpts[rcpt] = (code,resp)# permanent error
|
||||||
smtp.quit()
|
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:
|
||||||
return x.recipients[mailfrom] # permanent error
|
if len(x.recipients) == 1:
|
||||||
|
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:
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
"""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
|
||||||
+76
-5
@@ -1,3 +1,7 @@
|
|||||||
|
## @package Milter.utils
|
||||||
|
# Miscellaneous functions.
|
||||||
|
#
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import struct
|
import struct
|
||||||
import socket
|
import socket
|
||||||
@@ -7,17 +11,48 @@ from email.Header import decode_header
|
|||||||
#import email.Utils
|
#import email.Utils
|
||||||
import rfc822
|
import rfc822
|
||||||
|
|
||||||
ip4re = re.compile(r'^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$')
|
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
|
||||||
|
ip4re = re.compile(PAT_IP4+'$')
|
||||||
|
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(str):
|
def addr2bin(str):
|
||||||
"Convert a string IPv4 address into an unsigned integer."
|
"""Convert a string IPv4 address into an unsigned integer."""
|
||||||
|
try:
|
||||||
return struct.unpack("!L", socket.inet_aton(str))[0]
|
return struct.unpack("!L", socket.inet_aton(str))[0]
|
||||||
|
except socket.error:
|
||||||
|
raise socket.error("Invalid IP4 address: "+str)
|
||||||
|
|
||||||
|
def bin2long6(str):
|
||||||
|
"""Convert binary IP6 address into an unsigned Python long integer."""
|
||||||
|
h, l = struct.unpack("!QQ", str)
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
from pyip6 import inet_ntop, inet_pton
|
||||||
|
|
||||||
MASK = 0xFFFFFFFFL
|
MASK = 0xFFFFFFFFL
|
||||||
|
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
|
||||||
|
|
||||||
def cidr(i,n):
|
def cidr(i,n,mask=MASK):
|
||||||
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
|
||||||
@@ -27,8 +62,19 @@ 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]):
|
||||||
@@ -38,10 +84,21 @@ def iniplist(ipaddr,iplist):
|
|||||||
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.
|
||||||
|
|
||||||
@@ -91,13 +148,27 @@ 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:]]
|
||||||
return t.split('@')
|
if t.startswith('@'):
|
||||||
|
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.
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
## @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 callback events.
|
||||||
|
#
|
||||||
|
# Each event 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 <code>Milter.Base</code> 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 <code>Milter.Milter</code> class provides an alternate default
|
||||||
|
# implementation that logs the main milter events, but otherwise does nothing.
|
||||||
|
# It is provided for compatibility.
|
||||||
|
#
|
||||||
|
# The <code>mime</code> module provides a wrapper for the Python email package
|
||||||
|
# that fixes some bugs, and simplifies modifying selected parts of a MIME
|
||||||
|
# message.
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
# Document miltermodule for Doxygen
|
||||||
|
#
|
||||||
|
|
||||||
|
## @package milter
|
||||||
|
#
|
||||||
|
# A thin wrapper around libmilter.
|
||||||
|
#
|
||||||
|
|
||||||
|
## 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):
|
||||||
|
def getsymval(self,sym): pass
|
||||||
|
def setreply(self,rcode,xcode,*msg): pass
|
||||||
|
def addheader(self,name,value,idx=-1): pass
|
||||||
|
def chgheader(self,name,idx,value): pass
|
||||||
|
def addrcpt(self,rcpt,params=None): pass
|
||||||
|
def delrcpt(self,rcpt): pass
|
||||||
|
def replacebody(self,data): pass
|
||||||
|
def setpriv(self,priv): pass
|
||||||
|
def getpriv(self): pass
|
||||||
|
def quarantine(self,reason): pass
|
||||||
|
def progress(self): pass
|
||||||
|
def chgfrom(self,sender,param=None): pass
|
||||||
|
def setsmlist(self,stage,macrolist): pass
|
||||||
|
|
||||||
|
class error(Exception): pass
|
||||||
|
|
||||||
|
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
|
||||||
|
def set_exception_policy(code): pass
|
||||||
|
def register(name,negotiate=None,unknown=None,data=None): pass
|
||||||
|
def opensocket(rmsock): pass
|
||||||
|
def main(): pass
|
||||||
|
|
||||||
|
## Set the libmilter debugging level.
|
||||||
|
# smfi_setdbg 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.
|
||||||
|
def setdbg(lev): pass
|
||||||
|
|
||||||
|
def settimeout(secs): pass
|
||||||
|
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.
|
||||||
|
# <pre>
|
||||||
|
# setconn('unix:/var/run/pythonfilter')
|
||||||
|
# setconn('inet:8800') # listen on ANY interface
|
||||||
|
# setconn('inet:7871@@publichost') # listen on a specific interface
|
||||||
|
# setconn('inet6:8020')
|
||||||
|
# </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
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
web:
|
||||||
|
doxygen
|
||||||
|
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
|
||||||
|
|
||||||
|
VERSION=0.9.4
|
||||||
|
CVSTAG=pymilter-0_9_4
|
||||||
|
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)
|
||||||
+6
-4
@@ -15,7 +15,7 @@ from socket import AF_INET, AF_INET6
|
|||||||
from Milter import parse_addr
|
from Milter import parse_addr
|
||||||
|
|
||||||
|
|
||||||
class myMilter(Milter.Milter):
|
class myMilter(Milter.Base):
|
||||||
|
|
||||||
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.
|
||||||
@@ -23,6 +23,7 @@ class myMilter(Milter.Milter):
|
|||||||
# 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.
|
||||||
|
@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,
|
||||||
@@ -70,6 +71,7 @@ class myMilter(Milter.Milter):
|
|||||||
|
|
||||||
|
|
||||||
## def envrcpt(self, to, *str):
|
## def envrcpt(self, to, *str):
|
||||||
|
@noreply
|
||||||
def envrcpt(self, recipient, *str):
|
def envrcpt(self, recipient, *str):
|
||||||
rcptinfo = to,Milter.dictfromlist(str)
|
rcptinfo = to,Milter.dictfromlist(str)
|
||||||
self.R.append(rcptinfo)
|
self.R.append(rcptinfo)
|
||||||
@@ -77,21 +79,21 @@ class myMilter(Milter.Milter):
|
|||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|||||||
+268
-14
@@ -35,6 +35,42 @@ $ python setup.py help
|
|||||||
libraries=["milter","smutil","resolv"]
|
libraries=["milter","smutil","resolv"]
|
||||||
|
|
||||||
* $Log$
|
* $Log$
|
||||||
|
* 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
|
||||||
|
* Typo SMFIS_ALL_OPTS
|
||||||
|
*
|
||||||
|
* Revision 1.21 2009/05/29 19:49:40 customdesigned
|
||||||
|
* Typo calling helo instead of negotiate.
|
||||||
|
*
|
||||||
|
* Revision 1.20 2009/05/29 18:25:59 customdesigned
|
||||||
|
* Null terminate keyword list.
|
||||||
|
*
|
||||||
|
* Revision 1.19 2009/05/28 18:36:42 customdesigned
|
||||||
|
* Support new callbacks, including negotiate
|
||||||
|
*
|
||||||
|
* Revision 1.18 2009/05/21 21:53:05 customdesigned
|
||||||
|
* First cut at support unknown, data, negotiate callbacks.
|
||||||
|
*
|
||||||
|
* Revision 1.17 2009/02/06 04:28:08 customdesigned
|
||||||
|
* Oops! Missing options argument pointer for addrcpt.
|
||||||
|
*
|
||||||
|
* Revision 1.16 2008/12/16 04:21:05 customdesigned
|
||||||
|
* Fedora release
|
||||||
|
*
|
||||||
|
* Revision 1.15 2008/12/13 20:29:56 customdesigned
|
||||||
|
* Split off milter applications.
|
||||||
|
*
|
||||||
* Revision 1.14 2008/12/04 19:43:00 customdesigned
|
* Revision 1.14 2008/12/04 19:43:00 customdesigned
|
||||||
* Doc updates.
|
* Doc updates.
|
||||||
*
|
*
|
||||||
@@ -251,6 +287,12 @@ 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;
|
||||||
|
|
||||||
@@ -289,6 +331,7 @@ _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);
|
||||||
@@ -327,6 +370,7 @@ 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. */
|
||||||
@@ -375,7 +419,7 @@ generic_set_callback(PyObject *args,char *t,PyObject **cb) {
|
|||||||
callback = 0;
|
callback = 0;
|
||||||
else {
|
else {
|
||||||
if (!PyCallable_Check(callback)) {
|
if (!PyCallable_Check(callback)) {
|
||||||
PyErr_SetString(PyExc_TypeError, "parameter must be callable");
|
PyErr_SetString(PyExc_TypeError, "callback parameter must be callable");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
Py_INCREF(callback);
|
Py_INCREF(callback);
|
||||||
@@ -777,6 +821,87 @@ milter_wrap_abort(SMFICTX *ctx) {
|
|||||||
return generic_noarg_wrapper(ctx,abort_callback);
|
return generic_noarg_wrapper(ctx,abort_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef SMFIS_ALL_OPTS
|
||||||
|
static PyObject *unknown_callback = NULL;
|
||||||
|
static PyObject *data_callback = NULL;
|
||||||
|
static PyObject *negotiate_callback = NULL;
|
||||||
|
|
||||||
|
static int
|
||||||
|
milter_wrap_unknown(SMFICTX *ctx, const char *cmd) {
|
||||||
|
PyObject *arglist;
|
||||||
|
milter_ContextObject *c;
|
||||||
|
|
||||||
|
if (unknown_callback == NULL) return SMFIS_CONTINUE;
|
||||||
|
c = _get_context(ctx);
|
||||||
|
if (!c) return SMFIS_TEMPFAIL;
|
||||||
|
arglist = Py_BuildValue("(Os)", c, cmd);
|
||||||
|
return _generic_wrapper(c, unknown_callback, arglist);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
milter_wrap_data(SMFICTX *ctx) {
|
||||||
|
return generic_noarg_wrapper(ctx,data_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
milter_wrap_negotiate(SMFICTX *ctx,
|
||||||
|
unsigned long f0,
|
||||||
|
unsigned long f1,
|
||||||
|
unsigned long f2,
|
||||||
|
unsigned long f3,
|
||||||
|
unsigned long *pf0,
|
||||||
|
unsigned long *pf1,
|
||||||
|
unsigned long *pf2,
|
||||||
|
unsigned long *pf3) {
|
||||||
|
PyObject *arglist, *optlist;
|
||||||
|
milter_ContextObject *c;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
if (negotiate_callback == NULL) return SMFIS_ALL_OPTS;
|
||||||
|
c = _get_context(ctx);
|
||||||
|
if (!c)
|
||||||
|
return SMFIS_REJECT; // do not contact us again for current connection
|
||||||
|
optlist = Py_BuildValue("[kkkk]",f0,f1,f2,f3);
|
||||||
|
if (optlist == NULL)
|
||||||
|
arglist = NULL;
|
||||||
|
else
|
||||||
|
arglist = Py_BuildValue("(OO)", c, optlist);
|
||||||
|
PyThreadState *t = c->t;
|
||||||
|
c->t = 0; // do not release thread in _generic_wrapper
|
||||||
|
rc = _generic_wrapper(c, negotiate_callback, arglist);
|
||||||
|
c->t = t;
|
||||||
|
if (rc == SMFIS_CONTINUE) {
|
||||||
|
#if 0 // PyArgs_Parse deprecated and going away
|
||||||
|
if (!PyArgs_Parse(optlist,"[kkkk]",pf0,pf1,pf2,pf3)) {
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear(); /* must clear since not returning to python */
|
||||||
|
rc = SMFIS_REJECT;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
unsigned long *pa[4] = { pf0,pf1,pf2,pf3 };
|
||||||
|
unsigned long fa[4] = { f0,f1,f2,f3 };
|
||||||
|
int len = PyList_Size(optlist);
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < 4; ++i) {
|
||||||
|
*pa[i] = (i <= len)
|
||||||
|
? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
|
||||||
|
: fa[i];
|
||||||
|
}
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
PyErr_Print();
|
||||||
|
PyErr_Clear();
|
||||||
|
rc = SMFIS_REJECT;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else if (rc != SMFIS_ALL_OPTS)
|
||||||
|
rc = SMFIS_REJECT;
|
||||||
|
Py_DECREF(optlist);
|
||||||
|
_release_thread(t);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static int
|
static int
|
||||||
milter_wrap_close(SMFICTX *ctx) {
|
milter_wrap_close(SMFICTX *ctx) {
|
||||||
/* xxfi_close can be called out of order - even before connect.
|
/* xxfi_close can be called out of order - even before connect.
|
||||||
@@ -808,17 +933,63 @@ milter_wrap_close(SMFICTX *ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static char milter_register__doc__[] =
|
static char milter_register__doc__[] =
|
||||||
"register(name) -> 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.";
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
milter_register(PyObject *self, PyObject *args) {
|
milter_register(PyObject *self, PyObject *args, PyObject *kwds) {
|
||||||
if (!PyArg_ParseTuple(args, "s:register", &description.xxfi_name))
|
static char *kwlist[] = { "name","unknown","data","negotiate", NULL };
|
||||||
|
static PyObject** const cbp[3] =
|
||||||
|
{ &unknown_callback, &data_callback, &negotiate_callback };
|
||||||
|
PyObject *cb[3] = { NULL, NULL, NULL };
|
||||||
|
int i;
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|OOO:register", kwlist,
|
||||||
|
&description.xxfi_name, &cb[0],&cb[1],&cb[2]))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
for (i = 0; i < 3; ++i) {
|
||||||
|
PyObject *callback = cb[i];
|
||||||
|
if (callback != NULL && callback != Py_None) {
|
||||||
|
if (!PyCallable_Check(callback)) {
|
||||||
|
char err[80];
|
||||||
|
sprintf(err,"%s parameter must be callable",kwlist[i]);
|
||||||
|
PyErr_SetString(PyExc_TypeError, err);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < 3; ++i) {
|
||||||
|
PyObject *callback = cb[i];
|
||||||
|
if (callback != NULL) { // if keyword specified
|
||||||
|
if (callback == Py_None) {
|
||||||
|
callback = NULL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Py_INCREF(callback);
|
||||||
|
}
|
||||||
|
PyObject *oldval = *cbp[i];
|
||||||
|
*cbp[i] = callback;
|
||||||
|
if (oldval) {
|
||||||
|
Py_DECREF(oldval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return _generic_return(smfi_register(description), "cannot register");
|
return _generic_return(smfi_register(description), "cannot register");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char milter_opensocket__doc__[] =
|
||||||
|
"opensocket(rmsock) -> None\n\
|
||||||
|
Attempts to create and open the socket provided with setconn.\n\
|
||||||
|
Removes the socket first if rmsock is True.";
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
milter_opensocket(PyObject *self, PyObject *args) {
|
||||||
|
char rmsock = 0;
|
||||||
|
if (!PyArg_ParseTuple(args, "b:opensocket", &rmsock))
|
||||||
|
return NULL;
|
||||||
|
return _generic_return(smfi_opensocket(rmsock), "cannot opensocket");
|
||||||
|
}
|
||||||
|
|
||||||
static 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\
|
||||||
@@ -912,6 +1083,30 @@ milter_stop(PyObject *self, PyObject *args) {
|
|||||||
return _thread_return(t,smfi_stop(), "cannot stop");
|
return _thread_return(t,smfi_stop(), "cannot stop");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char milter_getdiag__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 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 char milter_getsymval__doc__[] =
|
static 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.";
|
||||||
@@ -1037,7 +1232,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;
|
char *params = NULL;
|
||||||
SMFICTX *ctx;
|
SMFICTX *ctx;
|
||||||
PyThreadState *t;
|
PyThreadState *t;
|
||||||
|
|
||||||
@@ -1094,7 +1289,7 @@ milter_addrcpt(PyObject *self, PyObject *args) {
|
|||||||
PyThreadState *t;
|
PyThreadState *t;
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "s|z:addrcpt", &rcpt)) return NULL;
|
if (!PyArg_ParseTuple(args, "s|z:addrcpt", &rcpt, ¶ms)) 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();
|
||||||
@@ -1124,8 +1319,7 @@ milter_delrcpt(PyObject *self, PyObject *args) {
|
|||||||
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_delrcpt(ctx, rcpt),
|
return _thread_return(t,smfi_delrcpt(ctx, rcpt), "cannot delete recipient");
|
||||||
"cannot delete recipient");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_replacebody__doc__[] =
|
static char milter_replacebody__doc__[] =
|
||||||
@@ -1146,8 +1340,8 @@ milter_replacebody(PyObject *self, PyObject *args) {
|
|||||||
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_replacebody(ctx, bodyp, bodylen),
|
return _thread_return(t,smfi_replacebody(ctx,
|
||||||
"cannot replace message body");
|
(unsigned char *)bodyp, bodylen), "cannot replace message body");
|
||||||
}
|
}
|
||||||
|
|
||||||
static char milter_setpriv__doc__[] =
|
static char milter_setpriv__doc__[] =
|
||||||
@@ -1232,6 +1426,27 @@ milter_progress(PyObject *self, PyObject *args) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef SMFIF_SETSMLIST
|
||||||
|
static char milter_setsmlist__doc__[] =
|
||||||
|
"setsmlist(stage,macrolist) -> None\n\
|
||||||
|
Tell the MTA which macro values we are interested in for a given stage";
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
milter_setsmlist(PyObject *self, PyObject *args) {
|
||||||
|
SMFICTX *ctx;
|
||||||
|
PyThreadState *t;
|
||||||
|
int stage = 0;
|
||||||
|
char *smlist = 0;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(args, "is:setsmlist",&stage, &smlist)) return NULL;
|
||||||
|
ctx = _find_context(self);
|
||||||
|
if (ctx == NULL) return NULL;
|
||||||
|
t = PyEval_SaveThread();
|
||||||
|
return _thread_return(t,smfi_setsmlist(ctx,stage,smlist),
|
||||||
|
"cannot set macro list");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static PyMethodDef context_methods[] = {
|
static PyMethodDef context_methods[] = {
|
||||||
{ "getsymval", milter_getsymval, METH_VARARGS, milter_getsymval__doc__},
|
{ "getsymval", milter_getsymval, METH_VARARGS, milter_getsymval__doc__},
|
||||||
{ "setreply", milter_setreply, METH_VARARGS, milter_setreply__doc__},
|
{ "setreply", milter_setreply, METH_VARARGS, milter_setreply__doc__},
|
||||||
@@ -1250,6 +1465,9 @@ static PyMethodDef context_methods[] = {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef SMFIF_CHGFROM
|
#ifdef SMFIF_CHGFROM
|
||||||
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
|
{ "chgfrom", milter_chgfrom, METH_VARARGS, milter_chgfrom__doc__},
|
||||||
|
#endif
|
||||||
|
#ifdef SMFIF_SETSMLIST
|
||||||
|
{ "setsmlist", milter_setsmlist, METH_VARARGS, milter_setsmlist__doc__},
|
||||||
#endif
|
#endif
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
};
|
};
|
||||||
@@ -1272,7 +1490,12 @@ static struct smfiDesc description = { /* Set some reasonable defaults */
|
|||||||
milter_wrap_body,
|
milter_wrap_body,
|
||||||
milter_wrap_eom,
|
milter_wrap_eom,
|
||||||
milter_wrap_abort,
|
milter_wrap_abort,
|
||||||
milter_wrap_close
|
milter_wrap_close,
|
||||||
|
#ifdef SMFIS_ALL_OPTS
|
||||||
|
milter_wrap_unknown,
|
||||||
|
milter_wrap_data,
|
||||||
|
milter_wrap_negotiate
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
static PyMethodDef milter_methods[] = {
|
static PyMethodDef milter_methods[] = {
|
||||||
@@ -1287,15 +1510,17 @@ static PyMethodDef milter_methods[] = {
|
|||||||
{ "set_eom_callback", milter_set_eom_callback, METH_VARARGS, milter_set_eom_callback__doc__},
|
{ "set_eom_callback", milter_set_eom_callback, METH_VARARGS, milter_set_eom_callback__doc__},
|
||||||
{ "set_abort_callback", milter_set_abort_callback, METH_VARARGS, milter_set_abort_callback__doc__},
|
{ "set_abort_callback", milter_set_abort_callback, METH_VARARGS, milter_set_abort_callback__doc__},
|
||||||
{ "set_close_callback", milter_set_close_callback, METH_VARARGS, milter_set_close_callback__doc__},
|
{ "set_close_callback", milter_set_close_callback, METH_VARARGS, milter_set_close_callback__doc__},
|
||||||
{ "set_exception_policy", milter_set_exception_policy,METH_VARARGS, milter_set_exception_policy__doc__},
|
{ "set_exception_policy", milter_set_exception_policy, METH_VARARGS, milter_set_exception_policy__doc__},
|
||||||
{ "register", milter_register, METH_VARARGS, milter_register__doc__},
|
{ "register", (PyCFunction)milter_register,METH_VARARGS|METH_KEYWORDS, milter_register__doc__},
|
||||||
{ "register", milter_register, METH_VARARGS, milter_register__doc__},
|
{ "opensocket", milter_opensocket, METH_VARARGS, milter_opensocket__doc__},
|
||||||
{ "main", milter_main, METH_VARARGS, milter_main__doc__},
|
{ "main", milter_main, METH_VARARGS, milter_main__doc__},
|
||||||
{ "setdbg", milter_setdbg, METH_VARARGS, milter_setdbg__doc__},
|
{ "setdbg", milter_setdbg, METH_VARARGS, milter_setdbg__doc__},
|
||||||
{ "settimeout", milter_settimeout, METH_VARARGS, milter_settimeout__doc__},
|
{ "settimeout", milter_settimeout, METH_VARARGS, milter_settimeout__doc__},
|
||||||
{ "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 }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1364,6 +1589,35 @@ initmilter(void) {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef SMFIF_CHGFROM
|
#ifdef SMFIF_CHGFROM
|
||||||
setitem(d,"CHGFROM",SMFIF_CHGFROM);
|
setitem(d,"CHGFROM",SMFIF_CHGFROM);
|
||||||
|
#endif
|
||||||
|
#ifdef SMFIF_SETSMLIST
|
||||||
|
setitem(d,"SETSMLIST",SMFIF_SETSMLIST);
|
||||||
|
#endif
|
||||||
|
#ifdef SMFIS_ALL_OPTS
|
||||||
|
setitem(d,"P_RCPT_REJ",SMFIP_RCPT_REJ);
|
||||||
|
setitem(d,"P_NR_CONN",SMFIP_NR_CONN);
|
||||||
|
setitem(d,"P_NR_HELO",SMFIP_NR_HELO);
|
||||||
|
setitem(d,"P_NR_MAIL",SMFIP_NR_MAIL);
|
||||||
|
setitem(d,"P_NR_RCPT",SMFIP_NR_RCPT);
|
||||||
|
setitem(d,"P_NR_DATA",SMFIP_NR_DATA);
|
||||||
|
setitem(d,"P_NR_UNKN",SMFIP_NR_UNKN);
|
||||||
|
setitem(d,"P_NR_EOH",SMFIP_NR_EOH);
|
||||||
|
setitem(d,"P_NR_BODY",SMFIP_NR_BODY);
|
||||||
|
setitem(d,"P_NR_HDR",SMFIP_NR_HDR);
|
||||||
|
setitem(d,"P_NOCONNECT",SMFIP_NOCONNECT);
|
||||||
|
setitem(d,"P_NOHELO",SMFIP_NOHELO);
|
||||||
|
setitem(d,"P_NOMAIL",SMFIP_NOMAIL);
|
||||||
|
setitem(d,"P_NORCPT",SMFIP_NORCPT);
|
||||||
|
setitem(d,"P_NODATA",SMFIP_NODATA);
|
||||||
|
setitem(d,"P_NOUNKNOWN",SMFIP_NOUNKNOWN);
|
||||||
|
setitem(d,"P_NOEOH",SMFIP_NOEOH);
|
||||||
|
setitem(d,"P_NOBODY",SMFIP_NOBODY);
|
||||||
|
setitem(d,"P_NOHDRS",SMFIP_NOHDRS);
|
||||||
|
setitem(d,"P_HDR_LEADSPC",SMFIP_HDR_LEADSPC);
|
||||||
|
setitem(d,"P_SKIP",SMFIP_SKIP);
|
||||||
|
setitem(d,"ALL_OPTS",SMFIS_ALL_OPTS);
|
||||||
|
setitem(d,"SKIP",SMFIS_SKIP);
|
||||||
|
setitem(d,"NOREPLY",SMFIS_NOREPLY);
|
||||||
#endif
|
#endif
|
||||||
setitem(d,"CONTINUE", SMFIS_CONTINUE);
|
setitem(d,"CONTINUE", SMFIS_CONTINUE);
|
||||||
setitem(d,"REJECT", SMFIS_REJECT);
|
setitem(d,"REJECT", SMFIS_REJECT);
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
# $Log$
|
# $Log$
|
||||||
|
# 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.
|
||||||
#
|
#
|
||||||
@@ -70,8 +76,12 @@
|
|||||||
# with old milter code.
|
# with old milter code.
|
||||||
#
|
#
|
||||||
|
|
||||||
# This module provides a "defang" function to replace naughty attachments
|
## @package mime
|
||||||
# with a warning message.
|
# This module provides a "defang" function to replace naughty attachments.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
@@ -93,6 +103,8 @@ 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')
|
||||||
@@ -103,6 +115,8 @@ 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
|
||||||
@@ -142,12 +156,9 @@ 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
|
#
|
||||||
# Headerchange attribute can be assigned a function to be called when
|
# Tracks modifications to headers of body or any part independently.
|
||||||
# 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
|
||||||
@@ -158,6 +169,12 @@ class MimeMessage(Message):
|
|||||||
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
|
||||||
|
|
||||||
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)
|
||||||
if val != failobj and param == 'boundary' and unquote:
|
if val != failobj and param == 'boundary' and unquote:
|
||||||
|
|||||||
+76
-41
@@ -1,23 +1,23 @@
|
|||||||
%define __python python2.4
|
%define __python python2.6
|
||||||
%define version 0.9.0
|
|
||||||
%define release 1.el4
|
|
||||||
%define libdir %{_libdir}/pymilter
|
%define libdir %{_libdir}/pymilter
|
||||||
%define name pymilter
|
%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
|
||||||
%define redhat7 0
|
%define pythonbase python26
|
||||||
|
|
||||||
Summary: Python interface to sendmail milter API
|
Summary: Python interface to sendmail milter API
|
||||||
Name: %{name}
|
Name: %{pythonbase}-pymilter
|
||||||
Version: %{version}
|
Version: 0.9.4
|
||||||
Release: %{release}
|
Release: 1%{dist}
|
||||||
Source: %{name}-%{version}.tar.gz
|
Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
|
||||||
#Patch: %{name}-%{version}.patch
|
|
||||||
License: GPLv2+
|
License: GPLv2+
|
||||||
Group: Development/Libraries
|
Group: Development/Libraries
|
||||||
BuildRoot: %{_tmppath}/%{name}-buildroot
|
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
||||||
Vendor: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
Url: http://www.bmsi.com/python/milter.html
|
Url: http://www.bmsi.com/python/milter.html
|
||||||
Requires: %{__python} >= 2.4, sendmail >= 8.13
|
Requires: %{pythonbase}, sendmail >= 8.13
|
||||||
BuildRequires: %{__python}-devel >= 2.4, sendmail-devel >= 8.13
|
# Need python2.4 specific pydns, not the version for system python
|
||||||
|
Requires: %{pythonbase}-pydns
|
||||||
|
# Needed for callbacks, not a core function but highly useful for milters
|
||||||
|
BuildRequires: ed, %{pythonbase}-devel, 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
|
||||||
@@ -26,31 +26,31 @@ modules provide for navigating and modifying MIME parts, sending
|
|||||||
DSNs, and doing CBV.
|
DSNs, and doing CBV.
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
%setup -q
|
%setup -q -n pymilter-%{version}
|
||||||
#patch -p0 -b .bms
|
|
||||||
|
|
||||||
%build
|
%build
|
||||||
%if %{redhat7}
|
env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
|
||||||
LDFLAGS="-s"
|
|
||||||
%else # Redhat builds debug packages after 7.3
|
|
||||||
LDFLAGS="-g"
|
|
||||||
%endif
|
|
||||||
env CFLAGS="$RPM_OPT_FLAGS" LDFLAGS="$LDFLAGS" %{__python} setup.py build
|
|
||||||
|
|
||||||
%install
|
%install
|
||||||
rm -rf $RPM_BUILD_ROOT
|
rm -rf $RPM_BUILD_ROOT
|
||||||
%{__python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
|
%{__python} setup.py install --root=$RPM_BUILD_ROOT
|
||||||
mkdir -p $RPM_BUILD_ROOT/var/run/milter
|
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
|
||||||
|
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
|
||||||
mkdir -p $RPM_BUILD_ROOT%{libdir}
|
mkdir -p $RPM_BUILD_ROOT%{libdir}
|
||||||
%ifos aix4.1
|
|
||||||
cat >$RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
cd /var/log/milter
|
|
||||||
exec /usr/local/bin/python bms.py >>milter.log 2>&1
|
|
||||||
EOF
|
|
||||||
%else # not aix4.1
|
|
||||||
cp start.sh $RPM_BUILD_ROOT%{libdir}
|
cp start.sh $RPM_BUILD_ROOT%{libdir}
|
||||||
ed $RPM_BUILD_ROOT%{libdir}/start.sh <<'EOF'
|
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=/
|
/^python=/
|
||||||
c
|
c
|
||||||
python="%{__python}"
|
python="%{__python}"
|
||||||
@@ -58,43 +58,78 @@ python="%{__python}"
|
|||||||
w
|
w
|
||||||
q
|
q
|
||||||
EOF
|
EOF
|
||||||
%endif
|
|
||||||
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
|
chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
|
||||||
%if !%{redhat7}
|
|
||||||
#grep '.pyc$' INSTALLED_FILES | sed -e 's/c$/o/' >>INSTALLED_FILES
|
|
||||||
%endif
|
|
||||||
|
|
||||||
# start.sh is used by spfmilter and milter, and could be used by
|
# start.sh is used by spfmilter, srsmilter, and milter, and could be used by
|
||||||
# other milters running on redhat
|
# other milters using pymilter.
|
||||||
%files -f INSTALLED_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
|
||||||
%config %{libdir}/start.sh
|
%{python_sitearch}/*
|
||||||
%dir %attr(0755,mail,mail) /var/run/milter
|
%{libdir}
|
||||||
|
%dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
|
||||||
|
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
|
||||||
|
|
||||||
%clean
|
%clean
|
||||||
rm -rf $RPM_BUILD_ROOT
|
rm -rf $RPM_BUILD_ROOT
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* 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
|
||||||
|
- Change result of @noreply callbacks to NOREPLY when so negotiated.
|
||||||
|
|
||||||
|
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-2
|
||||||
|
- Cache callback negotiation
|
||||||
|
|
||||||
|
* Thu May 28 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-1
|
||||||
|
- Add new callback support: data,negotiate,unknown
|
||||||
|
- Auto-negotiate protocol steps
|
||||||
|
|
||||||
|
* Thu Feb 05 2009 Stuart Gathman <stuart@bmsi.com> 0.9.1-1
|
||||||
|
- Fix missing address of optional param to addrcpt
|
||||||
|
|
||||||
|
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-4
|
||||||
|
- Stop using INSTALLED_FILES to make Fedora happy
|
||||||
|
- Remove config flag from start.sh glue
|
||||||
|
- Own /var/log/milter
|
||||||
|
- Use _localstatedir
|
||||||
|
|
||||||
|
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-2
|
||||||
|
- Changes to meet Fedora standards
|
||||||
|
|
||||||
* Mon Nov 24 2008 Stuart Gathman <stuart@bmsi.com> 0.9.0-1
|
* Mon Nov 24 2008 Stuart Gathman <stuart@bmsi.com> 0.9.0-1
|
||||||
- Split pymilter into its own CVS module
|
- Split pymilter into its own CVS module
|
||||||
- Support chgfrom and addrcpt_par
|
- Support chgfrom and addrcpt_par
|
||||||
- Support NS records in Milter.dns
|
- Support NS records in Milter.dns
|
||||||
|
|
||||||
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-2
|
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-2
|
||||||
- /var/run/milter directory must be owned by mail
|
- /var/run/milter directory must be owned by mail
|
||||||
|
|
||||||
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-1
|
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-1
|
||||||
- improved parsing into email and fullname (still 2 self test failures)
|
- improved parsing into email and fullname (still 2 self test failures)
|
||||||
- implement no-DSN CBV, reduce full DSNs
|
- implement no-DSN CBV, reduce full DSNs
|
||||||
|
|
||||||
* Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1
|
* Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1
|
||||||
- Use ifarch hack to build milter and milter-spf packages as noarch
|
- Use ifarch hack to build milter and milter-spf packages as noarch
|
||||||
- Remove spf dependency from dsn.py, add dns.py
|
- Remove spf dependency from dsn.py, add dns.py
|
||||||
|
|
||||||
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
|
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
|
||||||
- move AddrCache, parse_addr, iniplist to Milter package
|
- move AddrCache, parse_addr, iniplist to Milter package
|
||||||
- move parse_header to Milter.utils
|
- move parse_header to Milter.utils
|
||||||
- fix plock for missing source and can't change owner/group
|
- fix plock for missing source and can't change owner/group
|
||||||
- split out pymilter and pymilter-spf packages
|
- split out pymilter and pymilter-spf packages
|
||||||
- move milter apps to /usr/lib/pymilter
|
- move milter apps to /usr/lib/pymilter
|
||||||
|
|
||||||
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
|
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
|
||||||
- SPF moved to pyspf RPM
|
- SPF moved to pyspf RPM
|
||||||
|
|
||||||
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
|
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
|
||||||
- Support CBV timeout
|
- Support CBV timeout
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[bdist_rpm]
|
[bdist_rpm]
|
||||||
python=python2.4
|
python=python2.6
|
||||||
doc_files=README NEWS TODO
|
doc_files=README NEWS TODO
|
||||||
packager=Stuart D. Gathman <stuart@bmsi.com>
|
packager=Stuart D. Gathman <stuart@bmsi.com>
|
||||||
release=1
|
release=1
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from distutils.core import setup, Extension
|
from distutils.core import setup, Extension
|
||||||
|
|
||||||
# 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.
|
||||||
libs = ["milter", "smutil"]
|
#libs = ["milter", "smutil"]
|
||||||
|
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
|
# patch distutils if it can't cope with the "classifiers" or
|
||||||
@@ -16,7 +17,7 @@ if sys.version < '2.2.3':
|
|||||||
DistributionMetadata.download_url = 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 = '0.9.0',
|
setup(name = "pymilter", version = '0.9.4',
|
||||||
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
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
appname="$1"
|
appname="$1"
|
||||||
script="${2:-${appname}}"
|
script="${2:-${appname}}"
|
||||||
datadir=/var/log/milter
|
datadir="/var/log/milter"
|
||||||
|
piddir="/var/run/milter"
|
||||||
|
libdir="/usr/lib/pymilter"
|
||||||
python="python2.4"
|
python="python2.4"
|
||||||
exec >>${datadir}/${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 log dir if it exists for debugging
|
cd ${datadir} # use version in log dir if it exists for debugging
|
||||||
else
|
else
|
||||||
cd /usr/lib/pymilter
|
cd ${libdir}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
${python} ${script}.py &
|
${python} ${script}.py &
|
||||||
echo $! >/var/run/milter/${appname}.pid
|
echo $! >${piddir}/${appname}.pid
|
||||||
|
|||||||
-710
@@ -1,710 +0,0 @@
|
|||||||
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>--
|
|
||||||
Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
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.</pre>
|
|
||||||
</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> </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%> </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%>
|
|
||||||
|
|
||||||
</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 & Photo
|
|
||||||
<option value=software>Software
|
|
||||||
<option value=tools>Tools & 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>• <b><a href="/exec/obidos/tg/browse/-/283155/ref=gw_br_bo/103-3111065-2579065">Books</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/172282/ref=gw_br_el/103-3111065-2579065">Electronics</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/540744/ref=gw_br_ba/103-3111065-2579065">Baby &</a><br> <a href="/exec/obidos/tg/browse/-/540744/ref=gw_br_ba/103-3111065-2579065">Baby Registry</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/5174/ref=gw_br_mu/103-3111065-2579065">Music</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <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>• <b><a href="/exec/obidos/tg/browse/-/130/ref=gw_br_dvd/103-3111065-2579065">DVD</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/229534/ref=gw_br_sw/103-3111065-2579065">Software</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/284507/ref=gw_br_ki/103-3111065-2579065">Kitchen &</a><br> <a href="/exec/obidos/tg/browse/-/284507/ref=gw_br_ki/103-3111065-2579065">Housewares</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/228013/ref=gw_br_hi/103-3111065-2579065">Tools &</a><br> <a href="/exec/obidos/tg/browse/-/228013/ref=gw_br_hi/103-3111065-2579065">Hardware</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/541966/ref=gw_br_pc/103-3111065-2579065">Computers</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <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>• <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>• <b><a href="/exec/obidos/tg/browse/-/468642/ref=gw_br_cvg/103-3111065-2579065">Computer &</a><br> <a href="/exec/obidos/tg/browse/-/468642/ref=gw_br_cvg/103-3111065-2579065">Video Games</a></b></td>
|
|
||||||
</tr> <tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/171280/ref=gw_br_tg/103-3111065-2579065">Toys & Games</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/301185/ref=gw_br_wi/103-3111065-2579065">Cell Phones</a><br> <a href="/exec/obidos/tg/browse/-/301185/ref=gw_br_wi/103-3111065-2579065">& Service</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/404272/ref=gw_br_vi/103-3111065-2579065">Video</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/599858/ref=gw_br_zi/103-3111065-2579065">Magazine</a><br> <a href="/exec/obidos/tg/browse/-/599858/ref=gw_br_zi/103-3111065-2579065">Subscriptions</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <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>• <b><a href="/exec/obidos/tg/browse/-/605012/ref=gw_br_tr/103-3111065-2579065">Travel</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <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>• <b><a href="/exec/obidos/tg/browse/-/229220/ref=gw_br_gi/103-3111065-2579065">Gifts &</a><br> <a href="/exec/obidos/tg/browse/-/229220/ref=gw_br_gi/103-3111065-2579065">Gift Certificates</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <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>• <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>• <b><a href="/exec/obidos/tg/browse/-/517808/ref=gw_br_ou/103-3111065-2579065">Outlet</a></b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class=small>• <b><a href="/exec/obidos/tg/browse/-/600460/ref=gw_br_cb/103-3111065-2579065">Corporate</a> <br> <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>• <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>• <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>• <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 & 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> </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®-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>
|
|
||||||
<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>
|
|
||||||
|
|
|
||||||
(<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>)
|
|
||||||
</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>
|
|
||||||
(<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>)
|
|
||||||
</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>
|
|
||||||
(<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>)
|
|
||||||
</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>
|
|
||||||
(<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>)
|
|
||||||
</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>
|
|
||||||
(<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>)
|
|
||||||
</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,
|
|
||||||
<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>: 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>: 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>: 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>: 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>: 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>
|
|
||||||
<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 & 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>
|
|
||||||
• 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>
|
|
||||||
• 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>• 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 & Returns</b><br>
|
|
||||||
• See our <a href="/exec/obidos/tg/browse/-/468520/ref=hy_f_3/103-3111065-2579065">shipping rates & policies</a>.<br>
|
|
||||||
• <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>
|
|
||||||
• Forgot your password? <a href="/exec/obidos/self-service-forgot-password-get-email/ref=hy_f_6/103-3111065-2579065">Click here</a>.
|
|
||||||
<br>
|
|
||||||
• <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>
|
|
||||||
• <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 </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 & Photo
|
|
||||||
<option value=software>Software
|
|
||||||
<option value=tools>Tools & 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> for </b>
|
|
||||||
<input type="text" name="field-keywords" size="15">
|
|
||||||
<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>
|
|
||||||
|
|
|
||||||
<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>
|
|
||||||
|
|
|
||||||
<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>
|
|
||||||
 |
|
|
||||||
<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> |
|
|
||||||
<a href=/exec/obidos/shopping-basket/ref=gw_bt_sc/103-3111065-2579065>Shopping Cart</a> |
|
|
||||||
<a href=/exec/obidos/account-access-login/ref=gw_bt_ya/103-3111065-2579065>Your Account</a> |
|
|
||||||
<a href="http://s1.amazon.com/exec/varzea/ts/announcement-list-zshops/slp/ref=gw_bt_si/103-3111065-2579065">Sell Items</a> |
|
|
||||||
<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> |
|
|
||||||
<a href=/exec/obidos/tg/stores/job-listings/-/generic/home/103-3111065-2579065>Join Our Staff</a> |
|
|
||||||
<a href="/exec/obidos/subst/associates/join/associates.html/ref=gw_bt_as/103-3111065-2579065">Join Associates</a> |
|
|
||||||
<a href=/exec/obidos/subst/partners/direct/direct-application.html/ref=gw_bt_ad/103-3111065-2579065>Join Advantage</a> |
|
|
||||||
<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> © 1996-2002, Amazon.com, Inc. or its affiliates
|
|
||||||
</div>
|
|
||||||
</center>
|
|
||||||
<!-- whfhYn47qD1fv3PW2R8XWAkFcMwteHFKxorD -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
--------------59A46341C90BA737DD47867B--
|
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import StringIO
|
|||||||
|
|
||||||
class TestMilter(sample.sampleMilter):
|
class TestMilter(sample.sampleMilter):
|
||||||
|
|
||||||
|
_protocol = 0
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.logfp = open("test/milter.log","a")
|
self.logfp = open("test/milter.log","a")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user