diff --git a/Milter/__init__.py b/Milter/__init__.py
index d0212de4728a87accde204f9d463c039b46251af..58ebdf03616900034c9d80a6347512073083b97a 100755
--- a/Milter/__init__.py
+++ b/Milter/__init__.py
@@ -4,14 +4,14 @@
 
 # A thin OO wrapper for the milter module
 
+__version__ = '0.9.2'
+
 import os
 import milter
 import thread
 
 from milter import *
 
-__version__ = '0.9.2'
-
 _seq_lock = thread.allocate_lock()
 _seq = 0
 
@@ -24,19 +24,32 @@ def uniqueID():
   _seq_lock.release()
   return seqno
 
-OPTIONAL_CALLBACKS = frozenset((
-  'connect','hello','envfrom','envrcpt','data','header','eoh','body','unknown'))
+OPTIONAL_CALLBACKS = {
+  '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 nocallback(func):
-  if not func.__name__ in OPTIONAL_CALLBACKS:
+  try:
+    func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][1]
+  except KeyError:
     raise ValueError(
       '@nocallback applied to non-optional method: '+func.__name__)
-  func.milter_protocol = 'NO'
   return func
+
 def noreply(func):
-  if not func.__name__ in OPTIONAL_CALLBACKS:
+  try:
+    func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][0]
+  except KeyErro:
     raise ValueError(
       '@noreply applied to non-optional method: '+func.__name__)
-  func.milter_protocol = 'NR'
   return func
 
 class DisabledAction(RuntimeError):
@@ -49,8 +62,8 @@ class Base(object):
   "The core class interface to the milter module."
 
   def _setctx(self,ctx):
-    self.__ctx = ctx
-    self.__actions = CURR_ACTS         # all actions enabled by default
+    self._ctx = ctx
+    self._actions = CURR_ACTS         # all actions enabled by default
     if ctx:
       ctx.setpriv(self)
   def log(self,*msg): pass
@@ -76,32 +89,27 @@ class Base(object):
   def abort(self): return CONTINUE
   def close(self): return CONTINUE
 
+  # Return mask of SMFIP_N.. protocol bits to clear for this class
+  @classmethod
+  def protocol_mask(klass):
+    try:
+      return klass._protocol_mask
+    except AttributeError:
+      p = 0
+      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
+    
   # Default negotiation sets P_NO* and P_NR* for callbacks
   # marked @nocallback and @noreply respectively
   def negotiate(self,opts):
     try:
-      self.__actions,p,f1,f2 = opts
-      for func,nr,nc in (
-        (self.connect,P_NR_CONN,P_NOCONNECT),
-        (self.hello,P_NR_HELO,P_NOHELO),
-        (self.envfrom,P_NR_MAIL,P_NOMAIL),
-        (self.envrcpt,P_NR_RCPT,P_NORCPT),
-        (self.data,P_NR_DATA,P_NODATA),
-        (self.unknown,P_NR_UNKN,P_NOUNKNOWN),
-        (self.eoh,P_NR_EOH,P_NOEOH),
-        (self.body,P_NR_BODY,P_NOBODY),
-        (self.header,P_NR_HDR,P_NOHDRS)
-      ):
-        ca = getattr(func,'milter_protocol',None)
-        if ca != 'NR':
-	  p &= ~nr
-        #elif p & nr:
-	#  self.log(func.__name__,'NOREPLY')
-        if ca != 'NO':
-	  p &= ~nc
-        #elif p & nc:
-	#  self.log(func.__name__,'NOCALLBACK')
-      opts[1] = p & ~P_RCPT_REJ & ~P_HDR_LEADSPC
+      self._actions,p,f1,f2 = opts
+      opts[1] = p & ~self.protocol_mask() & ~P_RCPT_REJ & ~P_HDR_LEADSPC
       opts[2] = 0
       opts[3] = 0
       self.log("Negotiated:",opts)
@@ -112,53 +120,53 @@ class Base(object):
 
   # Milter methods which can be invoked from most callbacks
   def getsymval(self,sym):
-    return self.__ctx.getsymval(sym)
+    return self._ctx.getsymval(sym)
 
   # If sendmail 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)
+    return self._ctx.setreply(rcode,xcode,msg,*ml)
 
   # may only be called from negotiate callback
   def setsmlist(self,stage,macros):
-    if not self.__actions & SETSMLIST: raise DisabledAction("SETSMLIST")
+    if not self._actions & SETSMLIST: raise DisabledAction("SETSMLIST")
     if type(macros) in (list,tuple):
       macros = ' '.join(macros)
-    return self.__ctx.setsmlist(stage,macros)
+    return self._ctx.setsmlist(stage,macros)
 
   # Milter methods which can only be called from eom callback.
   def addheader(self,field,value,idx=-1):
-    if not self.__actions & ADDHDRS: raise DisabledAction("ADDHDRS")
-    return self.__ctx.addheader(field,value,idx)
+    if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS")
+    return self._ctx.addheader(field,value,idx)
 
   def chgheader(self,field,idx,value):
-    if not self.__actions & CHGHDRS: raise DisabledAction("CHGHDRS")
-    return self.__ctx.chgheader(field,idx,value)
+    if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS")
+    return self._ctx.chgheader(field,idx,value)
 
   def addrcpt(self,rcpt,params=None):
-    if not self.__actions & ADDRCPT: raise DisabledAction("ADDRCPT")
-    return self.__ctx.addrcpt(rcpt,params)
+    if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT")
+    return self._ctx.addrcpt(rcpt,params)
 
   def delrcpt(self,rcpt):
-    if not self.__actions & DELRCPT: raise DisabledAction("DELRCPT")
-    return self.__ctx.delrcpt(rcpt)
+    if not self._actions & DELRCPT: raise DisabledAction("DELRCPT")
+    return self._ctx.delrcpt(rcpt)
 
   def replacebody(self,body):
-    if not self.__actions & MODBODY: raise DisabledAction("MODBODY")
-    return self.__ctx.replacebody(body)
+    if not self._actions & MODBODY: raise DisabledAction("MODBODY")
+    return self._ctx.replacebody(body)
 
   def chgfrom(self,sender,params=None):
-    if not self.__actions & CHGFROM: raise DisabledAction("CHGFROM")
-    return self.__ctx.chgfrom(sender,params)
+    if not self._actions & CHGFROM: raise DisabledAction("CHGFROM")
+    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):
-    if not self.__actions & QUARANTINE: raise DisabledAction("QUARANTINE")
-    return self.__ctx.quarantine(reason)
+    if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE")
+    return self._ctx.quarantine(reason)
 
   def progress(self):
-    return self.__ctx.progress()
+    return self._ctx.progress()
   
 # A logging but otherwise do nothing Milter base class included
 # for compatibility with previous versions of pymilter.
diff --git a/pymilter.spec b/pymilter.spec
index 22955a9768b171241c7f47ec38bc7d68315f57ec..e0d2dac4f50ab678ea2c3e02cf3546dceca8f3a9 100644
--- a/pymilter.spec
+++ b/pymilter.spec
@@ -12,7 +12,7 @@
 Summary: Python interface to sendmail milter API
 Name: pymilter
 Version: 0.9.2
-Release: 1%{dist}
+Release: 2%{dist}
 Source: http://downloads.sourceforge.net/pymilter/%{name}-%{version}.tar.gz
 License: GPLv2+
 Group: Development/Libraries
@@ -85,6 +85,7 @@ rm -rf $RPM_BUILD_ROOT
 %changelog
 * 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