Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1efc262dc5 | |||
| f3fbb1c99d | |||
| 27887daf3f | |||
| 23defb880b | |||
| 7502c29e47 | |||
| 594d3ad365 | |||
| b2e0b2ebc6 | |||
| 04a241f1e9 | |||
| 16bfe5d4da | |||
| 70d19001c0 | |||
| 0d001dd8e9 |
@@ -31,7 +31,7 @@ PROJECT_NAME = pymilter
|
||||
# This could be handy for archiving the generated documentation or
|
||||
# if some version control system is used.
|
||||
|
||||
PROJECT_NUMBER = 0.9.5
|
||||
PROJECT_NUMBER = 0.9.6
|
||||
|
||||
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
|
||||
# base path where the generated documentation will be put.
|
||||
|
||||
+48
-6
@@ -8,13 +8,15 @@
|
||||
# Copyright 2001,2009 Business Management Systems, Inc.
|
||||
# This code is under the GNU General Public License. See COPYING for details.
|
||||
|
||||
__version__ = '0.9.5'
|
||||
__version__ = '0.9.7'
|
||||
|
||||
import os
|
||||
import re
|
||||
import milter
|
||||
import thread
|
||||
|
||||
from milter import *
|
||||
from functools import wraps
|
||||
|
||||
_seq_lock = thread.allocate_lock()
|
||||
_seq = 0
|
||||
@@ -47,6 +49,9 @@ OPTIONAL_CALLBACKS = {
|
||||
'header':(P_NR_HDR,P_NOHDRS)
|
||||
}
|
||||
|
||||
## @private
|
||||
R = re.compile(r'%+')
|
||||
|
||||
## @private
|
||||
def decode_mask(bits,names):
|
||||
t = [ (s,getattr(milter,s)) for s in names]
|
||||
@@ -137,7 +142,12 @@ def nocallback(func):
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
'@nocallback applied to non-optional method: '+func.__name__)
|
||||
return func
|
||||
def wrapper(self,*args):
|
||||
if func(self,*args) != CONTINUE:
|
||||
raise RuntimeError('%s return code must be CONTINUE with @nocallback'
|
||||
% func.__name__)
|
||||
return CONTINUE
|
||||
return wrapper
|
||||
|
||||
## Function decorator to disable callback reply.
|
||||
# If the MTA supports it, tells the MTA not to wait for a reply from
|
||||
@@ -152,9 +162,14 @@ def noreply(func):
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
'@noreply applied to non-optional method: '+func.__name__)
|
||||
@wraps(func)
|
||||
def wrapper(self,*args):
|
||||
rc = func(self,*args)
|
||||
if self._protocol & nr_mask: return NOREPLY
|
||||
if self._protocol & nr_mask:
|
||||
if rc != CONTINUE:
|
||||
raise RuntimeError('%s return code must be CONTINUE with @noreply'
|
||||
% func.__name__)
|
||||
return NOREPLY
|
||||
return rc
|
||||
wrapper.milter_protocol = nr_mask
|
||||
return wrapper
|
||||
@@ -247,7 +262,9 @@ class Base(object):
|
||||
|
||||
## Defined by subclasses to write log messages.
|
||||
def log(self,*msg): pass
|
||||
## Called for each connection to the MTA.
|
||||
## Called for each connection to the MTA. Called by the
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_connect">
|
||||
# xxfi_connect</a> callback.
|
||||
# The <code>hostname</code> provided by the local MTA is either
|
||||
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
|
||||
# The format of hostaddr depends on the socket family:
|
||||
@@ -260,6 +277,17 @@ class Base(object):
|
||||
# <dt><code>socket.AF_UNIX</code>
|
||||
# <dd>A string with the socketname
|
||||
# </dl>
|
||||
# To vary behavior based on what port the client connected to,
|
||||
# for example skipping blacklist checks for port 587 (which must
|
||||
# be authenticated), use @link #getsymval getsymval('{daemon_port}') @endlink.
|
||||
# The <code>{daemon_port}</code> macro must be enabled in sendmail.cf
|
||||
# <pre>
|
||||
# O Milter.macros.connect=j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}
|
||||
# </pre>
|
||||
# or sendmail.mc
|
||||
# <pre>
|
||||
# define(`confMILTER_MACROS_CONNECT', ``j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}'')dnl
|
||||
# </pre>
|
||||
# @param hostname the PTR name or bracketed IP of the SMTP client
|
||||
# @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
|
||||
# or <code>socket.AF_UNIX</code>
|
||||
@@ -271,12 +299,26 @@ class Base(object):
|
||||
# this almost always results in terminating the connection.
|
||||
@nocallback
|
||||
def hello(self,hostname): return CONTINUE
|
||||
## Called when the SMTP client says MAIL FROM.
|
||||
## Called when the SMTP client says MAIL FROM. Called by the
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_envfrom">
|
||||
# xxfi_envfrom</a> callback.
|
||||
# Returning REJECT rejects the message, but not the connection.
|
||||
# The sender is the "envelope" from as defined by
|
||||
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
|
||||
# For the From: header (author) defined in
|
||||
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
||||
# see @link #header the header callback @endlink.
|
||||
@nocallback
|
||||
def envfrom(self,f,*str): return CONTINUE
|
||||
## Called when the SMTP client says RCPT TO.
|
||||
## Called when the SMTP client says RCPT TO. Called by the
|
||||
# <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
|
||||
# xxfi_envrcpt</a> callback.
|
||||
# Returning REJECT rejects the current recipient, not the entire message.
|
||||
# The recipient is the "envelope" recipient as defined by
|
||||
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
|
||||
# For recipients defined in
|
||||
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
||||
# for example To: or Cc:, see @link #header the header callback @endlink.
|
||||
@nocallback
|
||||
def envrcpt(self,to,*str): return CONTINUE
|
||||
## Called when the SMTP client says DATA.
|
||||
|
||||
+24
-4
@@ -70,15 +70,23 @@ class Session(object):
|
||||
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
|
||||
post: isinstance(__return__, types.ListType)
|
||||
"""
|
||||
if name.endswith('.'): name = name[:-1]
|
||||
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
|
||||
return [] # invalid DNS name (too long or empty)
|
||||
result = self.cache.get( (name, qtype) )
|
||||
cname = None
|
||||
if result: return result
|
||||
cnamek = (name,'CNAME')
|
||||
cname = self.cache.get( cnamek )
|
||||
|
||||
if not result:
|
||||
if cname:
|
||||
cname = cname[0]
|
||||
else:
|
||||
safe2cache = Session.SAFE2CACHE
|
||||
for k, v in DNSLookup(name, qtype):
|
||||
if k == (name, 'CNAME'):
|
||||
if k == cnamek:
|
||||
cname = v
|
||||
if (qtype,k[1]) in safe2cache:
|
||||
if k[1] == 'CNAME' or (qtype,k[1]) in safe2cache:
|
||||
self.cache.setdefault(k, []).append(v)
|
||||
result = self.cache.get( (name, qtype), [])
|
||||
if not result and cname:
|
||||
@@ -89,10 +97,22 @@ class Session(object):
|
||||
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
||||
cnames[name] = cname
|
||||
if cname in cnames:
|
||||
raise DNSError, 'CNAME loop'
|
||||
raise DNSError('CNAME loop')
|
||||
result = self.dns(cname, qtype, cnames=cnames)
|
||||
if result:
|
||||
self.cache[(name,qtype)] = result
|
||||
return result
|
||||
|
||||
def dns_txt(self, domainname, enc='ascii'):
|
||||
"Get a list of TXT records for a domain name."
|
||||
if domainname:
|
||||
try:
|
||||
return [''.join(s.decode(enc) for s in a)
|
||||
for a in self.dns(domainname, 'TXT')]
|
||||
except UnicodeEncodeError:
|
||||
raise DNSError('Non-ascii character in SPF TXT record.')
|
||||
return []
|
||||
|
||||
DNS.DiscoverNameServers()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ 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)
|
||||
return socket.inet_pton(socket.AF_INET6,s.strip())
|
||||
else:
|
||||
from pyip6 import inet_ntop, inet_pton
|
||||
|
||||
|
||||
+48
-34
@@ -35,6 +35,12 @@ $ python setup.py help
|
||||
libraries=["milter","smutil","resolv"]
|
||||
|
||||
* $Log$
|
||||
* Revision 1.30 2012/04/12 23:08:06 customdesigned
|
||||
* Support RFC2553 on BSD
|
||||
*
|
||||
* Revision 1.29 2011/06/09 15:45:27 customdesigned
|
||||
* Print callback name for non-int return error.
|
||||
*
|
||||
* Revision 1.28 2011/06/08 23:13:48 customdesigned
|
||||
* Generate special exception when callback return not int.
|
||||
*
|
||||
@@ -257,10 +263,10 @@ $ python setup.py help
|
||||
#define HAVE_IPV6_SUPPORT /* use this for #ifdef's later on */
|
||||
/* Now see if it supports the RFC-2553 socket's API spec. Early
|
||||
* IPv6 "prototype" implementations existed before the RFC was
|
||||
* published. Unfortunately I know of now good way to do this
|
||||
* published. Unfortunately I know of no good way to do this
|
||||
* other than with OS-specific tests.
|
||||
*/
|
||||
#ifdef linux
|
||||
#if defined(__FreeBSD_kernel__) || defined(__linux__)
|
||||
#define HAVE_IPV6_RFC2553
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
@@ -274,41 +280,49 @@ $ python setup.py help
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Yes, these are static. If you need multiple different callbacks, */
|
||||
/* it's cleaner to use multiple filters, or convert to OO method calls. */
|
||||
static PyObject *connect_callback = NULL;
|
||||
static PyObject *helo_callback = NULL;
|
||||
static PyObject *envfrom_callback = NULL;
|
||||
static PyObject *envrcpt_callback = NULL;
|
||||
static PyObject *header_callback = NULL;
|
||||
static PyObject *eoh_callback = NULL;
|
||||
static PyObject *body_callback = NULL;
|
||||
static PyObject *eom_callback = NULL;
|
||||
static PyObject *abort_callback = NULL;
|
||||
static PyObject *close_callback = NULL;
|
||||
enum callbacks {
|
||||
CONNECT,HELO,ENVFROM,ENVRCPT,HEADER,EOH,BODY,EOM,ABORT,CLOSE,
|
||||
#ifdef SMFIS_ALL_OPTS
|
||||
static PyObject *unknown_callback = NULL;
|
||||
static PyObject *data_callback = NULL;
|
||||
static PyObject *negotiate_callback = NULL;
|
||||
UNKNOWN,DATA,NEGOTIATE,
|
||||
#endif
|
||||
NUMCALLBACKS
|
||||
};
|
||||
|
||||
#define connect_callback callback[CONNECT].cb
|
||||
#define helo_callback callback[HELO].cb
|
||||
#define envfrom_callback callback[ENVFROM].cb
|
||||
#define envrcpt_callback callback[ENVRCPT].cb
|
||||
#define header_callback callback[HEADER].cb
|
||||
#define eoh_callback callback[EOH].cb
|
||||
#define body_callback callback[BODY].cb
|
||||
#define eom_callback callback[EOM].cb
|
||||
#define abort_callback callback[ABORT].cb
|
||||
#define close_callback callback[CLOSE].cb
|
||||
#define unknown_callback callback[UNKNOWN].cb
|
||||
#define data_callback callback[DATA].cb
|
||||
#define negotiate_callback callback[NEGOTIATE].cb
|
||||
|
||||
/* Yes, these are static. If you need multiple different callbacks,
|
||||
it's cleaner to use multiple filters, or convert to OO method calls. */
|
||||
|
||||
static struct MilterCallback {
|
||||
PyObject **cbp;
|
||||
PyObject *cb;
|
||||
const char *name;
|
||||
} callback_names[] = {
|
||||
{ &connect_callback,"connect" },
|
||||
{ &helo_callback,"helo" },
|
||||
{ &envfrom_callback,"envfrom" },
|
||||
{ &envrcpt_callback,"envrcpt" },
|
||||
{ &header_callback,"header" },
|
||||
{ &eoh_callback,"eoh" },
|
||||
{ &body_callback,"body" },
|
||||
{ &eom_callback,"eom" },
|
||||
{ &abort_callback,"abort" },
|
||||
{ &close_callback,"close" },
|
||||
} callback[NUMCALLBACKS+1] = {
|
||||
{ NULL ,"connect" },
|
||||
{ NULL ,"helo" },
|
||||
{ NULL ,"envfrom" },
|
||||
{ NULL ,"envrcpt" },
|
||||
{ NULL ,"header" },
|
||||
{ NULL ,"eoh" },
|
||||
{ NULL ,"body" },
|
||||
{ NULL ,"eom" },
|
||||
{ NULL ,"abort" },
|
||||
{ NULL ,"close" },
|
||||
#ifdef SMFIS_ALL_OPTS
|
||||
{ &unknown_callback,"unknown" },
|
||||
{ &data_callback,"data" },
|
||||
{ &negotiate_callback,"negotiate" },
|
||||
{ NULL ,"unknown" },
|
||||
{ NULL ,"data" },
|
||||
{ NULL ,"negotiate" },
|
||||
#endif
|
||||
{ NULL , NULL }
|
||||
};
|
||||
@@ -667,8 +681,8 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
|
||||
const char *cbname = "milter";
|
||||
char buf[40];
|
||||
Py_DECREF(result);
|
||||
for (p = callback_names; p->cbp; ++p) {
|
||||
if (cb == *p->cbp) {
|
||||
for (p = callback; p->name; ++p) {
|
||||
if (cb == p->cb) {
|
||||
cbname = p->name;
|
||||
break;
|
||||
}
|
||||
|
||||
+6
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
Summary: Python interface to sendmail milter API
|
||||
Name: %{pythonbase}-pymilter
|
||||
Version: 0.9.6
|
||||
Version: 0.9.7
|
||||
Release: 1%{dist}
|
||||
Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
|
||||
License: GPLv2+
|
||||
@@ -75,6 +75,11 @@ chmod a+x $RPM_BUILD_ROOT%{libdir}/start.sh
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
%changelog
|
||||
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
|
||||
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
|
||||
- Remove redundant table in miltermodule
|
||||
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
|
||||
|
||||
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1
|
||||
- Raise ValueError on unescaped '%' passed to setreply
|
||||
- Grace time at end of Greylist window
|
||||
|
||||
@@ -35,7 +35,9 @@ class sampleMilter(Milter.Milter):
|
||||
# multiple messages can be received on a single connection
|
||||
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
|
||||
# of each message.
|
||||
@Milter.noreply
|
||||
def envfrom(self,f,*str):
|
||||
"start of MAIL transaction"
|
||||
self.log("mail from",f,str)
|
||||
self.fp = StringIO.StringIO()
|
||||
self.tempname = None
|
||||
|
||||
@@ -13,7 +13,7 @@ libs = ["milter"]
|
||||
libdirs = ["/usr/lib/libmilter"] # needed for Debian
|
||||
|
||||
# NOTE: importing Milter to obtain version fails when milter.so not built
|
||||
setup(name = "pymilter", version = '0.9.6',
|
||||
setup(name = "pymilter", version = '0.9.7',
|
||||
description="Python interface to sendmail milter API",
|
||||
long_description="""\
|
||||
This is a python extension module to enable python scripts to
|
||||
|
||||
Reference in New Issue
Block a user