Compare commits

..

1 Commits

Author SHA1 Message Date
cvs2svn d7ef47d76b This commit was manufactured by cvs2svn to create tag 'pymilter-1_0'.
Sprout from master 2014-03-01 23:38:51 UTC Stuart Gathman <stuart@gathman.org> 'Release 1.0-2'
Cherrypick from bmsi 2005-05-31 18:10:47 UTC Stuart Gathman <stuart@gathman.org> 'Release 0.7.2':
    test/big5
    test/bounce
    test/bounce1
    test/bound
    test/honey
    test/missingboundary
    test/samp1
    test/spam44
    test/spam7
    test/spam8
    test/test1
    test/test8
    test/virus1
    test/virus13
    test/virus2
    test/virus3
    test/virus4
    test/virus5
    test/virus6
    test/virus7
2014-03-01 23:38:52 +00:00
55 changed files with 1402 additions and 2545 deletions
-8
View File
@@ -1,8 +0,0 @@
*.pyc
build/
test/*.out
test/*.tstout
test/*.log
test.db
dist
MANIFEST
-210
View File
@@ -1,213 +1,3 @@
# Revision 1.35 2013/03/14 22:11:25 customdesigned
# Release 0.9.8
#
# Revision 1.34 2013/03/09 05:42:14 customdesigned
# Make TestBase members private, fix getsymlist misspelling.
#
# Revision 1.33 2013/03/09 00:25:23 customdesigned
# Better untrapped exception message. const char for doc comments.
#
# Revision 1.32 2013/01/13 01:46:16 customdesigned
# Doc updates.
#
# Revision 1.31 2012/04/12 23:32:50 customdesigned
# Replace redundant callback array with macros. If this doesn't break anything,
# macros can be eliminated with code changes.
#
# Revision 1.30 2012/04/12 23:08:06 customdesigned
# Support RFC2553 on BSD
#
# Revision 1.29 2011/06/09 15:45:27 customdesigned
# Print callback name for non-int return error.
#
# Revision 1.28 2011/06/08 23:13:48 customdesigned
# Generate special exception when callback return not int.
#
# Revision 1.27 2009/07/28 21:45:54 customdesigned
# Add getversion() to return runtime version.
#
# Revision 1.26 2009/07/28 21:08:20 customdesigned
# Increment del count.
#
# Revision 1.25 2009/07/28 20:58:55 customdesigned
# getdiag method
#
# Revision 1.24 2009/06/09 01:54:44 customdesigned
# Forgot to initialize optional parameter.
#
# Revision 1.23 2009/05/29 20:44:58 customdesigned
# Typo SMFIP_NO constants.
#
# Revision 1.22 2009/05/29 19:53:36 customdesigned
# 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
# Doc updates.
#
# Revision 1.13 2008/11/23 03:06:47 customdesigned
# Milter support for chgfrom.
#
# Revision 1.12 2008/11/21 20:42:52 customdesigned
# Support smfi_chgfrom and smfi_addrcpt_par.
#
# Revision 1.11 2007/09/25 02:26:29 customdesigned
# Update license.
#
# Revision 1.10 2006/02/12 02:00:42 customdesigned
# Resolve FIXME for wrap_close.
#
# Revision 1.9 2005/12/23 21:46:36 customdesigned
# Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER)
#
# Revision 1.8 2005/10/20 23:23:36 customdesigned
# Include smfi_progress is SMFIR_PROGRESS defined
#
# Revision 1.7 2005/10/20 23:04:46 customdesigned
# Add optional idx for position of added header.
#
# Revision 1.6 2005/07/15 22:18:17 customdesigned
# Support callback exception policy
#
# Revision 1.5 2005/06/24 04:20:07 customdesigned
# Report context allocation error.
#
# Revision 1.4 2005/06/24 04:12:43 customdesigned
# Remove unused name argument to generic wrappers.
#
# Revision 1.3 2005/06/24 03:57:35 customdesigned
# Handle close called before connect.
#
# Revision 1.2 2005/06/02 04:18:55 customdesigned
# Update copyright notices after reading article on /.
#
# Revision 1.1.1.2 2005/05/31 18:09:06 customdesigned
# Release 0.7.1
#
# Revision 2.31 2004/08/23 02:24:36 stuart
# Support setbacklog
#
# Revision 2.30 2004/08/21 20:29:53 stuart
# Support option of 11 lines max for mlreply.
#
# Revision 2.29 2004/08/21 04:14:29 stuart
# mlreply support
#
# Revision 2.28 2004/08/21 02:45:21 stuart
# Don't leak int constants if module unloaded.
#
# Revision 2.27 2004/04/06 03:19:59 stuart
# Release 0.6.8
#
# Revision 2.26 2004/03/04 21:43:06 stuart
# Fix memory leak by removing unused dynamic template buffer,
# thanks again to Alexander Kourakos.
#
# Revision 2.25 2004/03/01 19:45:03 stuart
# Release 0.6.5
#
# Revision 2.24 2004/03/01 18:56:50 stuart
# Support progress reporting.
#
# Revision 2.23 2004/03/01 18:36:09 stuart
# Plug memory leak. Thanks to Alexander Kourakos.
#
# Revision 2.22 2003/11/02 03:01:46 stuart
# Adjust SMTP error codes after careful reading of standard.
#
# Revision 2.21 2003/06/24 19:57:04 stuart
# Allow removing a python milter callback by setting to None.
#
# Revision 2.20 2003/02/13 17:08:57 stuart
# IPV6 support
#
# Revision 2.19 2003/02/13 16:58:29 stuart
# Support passing None to setreply and chgheader.
#
# Revision 2.18 2002/12/11 16:44:06 stuart
# Support QUARANTINE if supported by libmilter.
#
# Revision 2.17 2002/04/18 20:20:35 stuart
# Fix for NULL hostaddr in connect callback from Jason Erickson.
#
# Revision 2.16 2001/09/26 13:29:09 stuart
# sa_len not supported by linux.
#
# Revision 2.15 2001/09/25 17:28:40 stuart
# Copyrights, documentation, release 0.3.1
#
# Revision 2.14 2001/09/25 00:36:57 stuart
# Pass hostaddr to python code in format used by standard socket module.
#
# Revision 2.13 2001/09/24 23:44:55 stuart
# Return old callback from setcallback functions.
#
# Revision 2.12 2001/09/24 20:02:30 stuart
# Remove redundant setpriv
#
# Revision 2.11 2001/09/23 22:26:35 stuart
# Update docs. Streamline Milter.py
# update testbms.py to reflect actual sendmail behaviour with multiple
# messages per connection.
#
# Revision 2.10 2001/09/22 15:33:42 stuart
# More doc comment updates.
#
# Revision 2.9 2001/09/22 14:52:27 stuart
# Actually return retval in _generic_return.
# Go over doc comments.
#
# Revision 2.8 2001/09/22 01:59:32 stuart
# Prevent reentrant call of milter_main, which libmilter doesn't support.
#
# Revision 2.7 2001/09/22 01:47:37 stuart
# Forgot to set milter interp.
#
# Revision 2.6 2001/09/22 01:23:53 stuart
# Added proper threading after research in python docs.
#
# Revision 2.5 2001/09/21 20:08:51 stuart
# Release 0.2.3
#
# Revision 2.4 2001/09/20 16:18:16 stuart
# libmilter checks in_eom state, so we don't have to.
#
# Revision 2.3 2001/09/19 06:02:33 stuart
# Make more stuff static.
#
# Revision 2.1 2001/09/19 04:24:13 stuart
# Use extension type to track context in python.
#
# Revision 1.4 2001/09/18 18:48:28 stuart
# clear private data reference in _clear_context
#
# Revision 1.3 2001/09/15 04:19:37 stuart
# nasty off by 1 mem overwrite bugs in wrap_env
# generic_set_callback
#
# Revision 1.2 2001/09/15 03:15:39 stuart
# several bugs fixed, works smoothly
#
# Revision 1.69 2006/11/04 22:09:39 customdesigned # Revision 1.69 2006/11/04 22:09:39 customdesigned
# Another lame DSN heuristic. Block PTR cache poisoning attack. # Another lame DSN heuristic. Block PTR cache poisoning attack.
# #
+1 -1
View File
@@ -31,7 +31,7 @@ PROJECT_NAME = pymilter
# This could be handy for archiving the generated documentation or # This could be handy for archiving the generated documentation or
# if some version control system is used. # if some version control system is used.
PROJECT_NUMBER = 1.0.5 PROJECT_NUMBER = 0.9.8
# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
# base path where the generated documentation will be put. # base path where the generated documentation will be put.
+1 -1
View File
@@ -2,7 +2,7 @@ include COPYING
include TODO include TODO
include NEWS include NEWS
include CREDITS include CREDITS
include README.md include README
include ChangeLog include ChangeLog
include MANIFEST.in include MANIFEST.in
include testsample.py include testsample.py
+31 -150
View File
@@ -8,18 +8,12 @@
# Copyright 2001,2009 Business Management Systems, Inc. # Copyright 2001,2009 Business Management Systems, Inc.
# This code is under the GNU General Public License. See COPYING for details. # This code is under the GNU General Public License. See COPYING for details.
from __future__ import print_function __version__ = '0.9.8'
__version__ = '1.0.5'
import os import os
import re import re
import milter import milter
import sys import thread
try:
import thread
except:
# libmilter uses posix threads
import _thread as thread
from milter import * from milter import *
from functools import wraps from functools import wraps
@@ -49,12 +43,6 @@ OPTIONAL_CALLBACKS = {
'header':(P_NR_HDR,P_NOHDRS) 'header':(P_NR_HDR,P_NOHDRS)
} }
MACRO_CALLBACKS = {
'connect': M_CONNECT,
'hello': M_HELO, 'envfrom': M_ENVFROM, 'envrcpt': M_ENVRCPT,
'data': M_DATA, 'eom': M_EOM, 'eoh': M_EOH
}
## @private ## @private
R = re.compile(r'%+') R = re.compile(r'%+')
@@ -113,7 +101,7 @@ def rejected_recipients(klass):
return enable_protocols(klass,P_RCPT_REJ) return enable_protocols(klass,P_RCPT_REJ)
## Milter leading space on headers. A class decorator that calls ## Milter leading space on headers. A class decorator that calls
# enable_protocols() with the P_HDR_LEADSPC flag. By default, # enable_protocols() with the P_HEAD_LEADSPC flag. By default,
# header continuation lines are collected and joined before getting # header continuation lines are collected and joined before getting
# sent to a milter. Headers modified or added by the milter are # sent to a milter. Headers modified or added by the milter are
# folded by the MTA as necessary according to its own standards. # folded by the MTA as necessary according to its own standards.
@@ -131,7 +119,7 @@ def rejected_recipients(klass):
# @param klass the %milter application class to modify # @param klass the %milter application class to modify
# @return the modified %milter class # @return the modified %milter class
def header_leading_space(klass): def header_leading_space(klass):
return enable_protocols(klass,P_HDR_LEADSPC) return enable_protocols(klass,P_HEAD_LEADSPC)
## Function decorator to disable callback methods. ## Function decorator to disable callback methods.
# If the MTA supports it, tells the MTA not to invoke this callback, # If the MTA supports it, tells the MTA not to invoke this callback,
@@ -148,7 +136,6 @@ def nocallback(func):
except KeyError: except KeyError:
raise ValueError( raise ValueError(
'@nocallback applied to non-optional method: '+func.__name__) '@nocallback applied to non-optional method: '+func.__name__)
@wraps(func)
def wrapper(self,*args): def wrapper(self,*args):
if func(self,*args) != CONTINUE: if func(self,*args) != CONTINUE:
raise RuntimeError('%s return code must be CONTINUE with @nocallback' raise RuntimeError('%s return code must be CONTINUE with @nocallback'
@@ -181,47 +168,6 @@ def noreply(func):
wrapper.milter_protocol = nr_mask wrapper.milter_protocol = nr_mask
return wrapper return wrapper
## Function decorator to set decoding error strategy.
# Current RFCs define UTF-8 as the standard encoding for SMTP
# envelope and header fields. By default, Milter.Base decodes
# envelope and header values with errors='surrogateescape'.
# Applications can recover the original bytes with
# <pre>
# b = s.encode(errors='surrogateescape')
# </pre>
# This preserves information, but can lead to unexpected exceptions
# as you cannot, e.g. print strings with surrogates.
# Illegal bytes occur quite often in real life, so there must
# be a way to deal with them.
# This decorator can change the error strategy to
# <ul>
# <li> bytes - original bytes are passed unmodified
# <li> strict - pass bytes if illegal bytes are present, string otherwise
# <li> ignore - illegal bytes are removed
# <li> replace - illegal bytes are replaced with a unicode error symbol
# </ul>
#
def decode(strategy):
def setstrategy(func):
func.error_strategy = strategy
return func
return setstrategy
## Function decorator to set macros used in a callback.
# By default, the MTA sends all macros defined for a callback.
# If some or all of these are unused, the bandwidth can be saved
# by listing the ones that are used.
# @since 1.0.2
def symlist(*syms):
def setsyms(func):
if len(syms) > 5:
raise ValueError('@symlist limited to 5 macros by MTA: '+func.__name__)
if func.__name__ not in MACRO_CALLBACKS:
raise ValueError('@symlist applied to non-symlist method: '+func.__name__)
func._symlist = syms
return func
return setsyms
## Disabled action exception. ## Disabled action exception.
# set_flags() can tell the MTA that this application will not use certain # set_flags() can tell the MTA that this application will not use certain
# features (such as CHGFROM). This can also be negotiated for each # features (such as CHGFROM). This can also be negotiated for each
@@ -311,7 +257,7 @@ class Base(object):
## Defined by subclasses to write log messages. ## Defined by subclasses to write log messages.
def log(self,*msg): pass def log(self,*msg): pass
## Called for each connection to the MTA. Called by the ## Called for each connection to the MTA. Called by the
# <a href="milter_api/xxfi_connect.html"> # <a href="https://www.milter.org/developers/api/xxfi_connect">
# xxfi_connect</a> callback. # xxfi_connect</a> callback.
# The <code>hostname</code> provided by the local MTA is either # The <code>hostname</code> provided by the local MTA is either
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available. # the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
@@ -347,22 +293,8 @@ class Base(object):
# this almost always results in terminating the connection. # this almost always results in terminating the connection.
@nocallback @nocallback
def hello(self,hostname): return CONTINUE def hello(self,hostname): return CONTINUE
## Called with bytes by default global envfrom callback.
# @since 1.0.5
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
# to pass bytes to @link #header the header callback @endlink instead,
# or trap utf-8 conversion exception, etc.
def envfrom_bytes(self,*b):
try:
e = getattr(self.envfrom,'error_strategy','surrogateescape')
if e == 'bytes':
#self.envfrom_bytes = self.envfrom
return self.envfrom(*b)
s = [v.decode(encoding='utf-8',errors=e) for v in b]
except UnicodeDecodeError: s = b
return self.envfrom(s[0],*s[1:])
## Called when the SMTP client says MAIL FROM. Called by the ## Called when the SMTP client says MAIL FROM. Called by the
# <a href="milter_api/xxfi_envfrom.html"> # <a href="https://www.milter.org/developers/api/xxfi_envfrom">
# xxfi_envfrom</a> callback. # xxfi_envfrom</a> callback.
# Returning REJECT rejects the message, but not the connection. # Returning REJECT rejects the message, but not the connection.
# The sender is the "envelope" from as defined by # The sender is the "envelope" from as defined by
@@ -371,23 +303,9 @@ class Base(object):
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>, # <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
# see @link #header the header callback @endlink. # see @link #header the header callback @endlink.
@nocallback @nocallback
def envfrom(self,f,*s): return CONTINUE def envfrom(self,f,*str): return CONTINUE
## Called with bytes by default global envrcpt callback.
# @since 1.0.5
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
# to pass bytes to @link #header the header callback @endlink instead,
# or trap utf-8 conversion exception, etc.
def envrcpt_bytes(self,*b):
try:
e = getattr(self.envrcpt,'error_strategy','surrogateescape')
if e == 'bytes':
#self.envrcpt_bytes = self.envrcpt
return self.envrcpt(*b)
s = [v.decode(encoding='utf-8',errors=e) for v in b]
except UnicodeDecodeError: s = b
return self.envrcpt(s[0],*s[1:])
## Called when the SMTP client says RCPT TO. Called by the ## Called when the SMTP client says RCPT TO. Called by the
# <a href="milter_api/xxfi_envrcpt.html"> # <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
# xxfi_envrcpt</a> callback. # xxfi_envrcpt</a> callback.
# Returning REJECT rejects the current recipient, not the entire message. # Returning REJECT rejects the current recipient, not the entire message.
# The recipient is the "envelope" recipient as defined by # The recipient is the "envelope" recipient as defined by
@@ -403,30 +321,7 @@ class Base(object):
# @since 0.9.2 # @since 0.9.2
@nocallback @nocallback
def data(self): return CONTINUE def data(self): return CONTINUE
## Called with bytes by default global header callback.
# @param fld name decoded as ascii
# @param val field value as bytes
# @since 1.0.5
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
# to pass bytes to @link #header the header callback @endlink instead,
# e.g. by assignment:
# <pre>
# mymilter.header_bytes = mymilter.header
# </pre>
# The <code>@decode('bytes')</code> decorator will also do this.
#
def header_bytes(self,fld,val):
try:
e = getattr(self.header,'error_strategy','surrogateescape')
if e == 'bytes':
self.header_bytes = self.header
return self.header(fld,val)
s = val.decode(encoding='utf-8',errors=e)
except UnicodeDecodeError: s = val
return self.header(fld,s)
## Called for each header field in the message body. ## Called for each header field in the message body.
# @param field name decoded as ascii
# @param value field value decoded as utf-8 on python3
@nocallback @nocallback
def header(self,field,value): return CONTINUE def header(self,field,value): return CONTINUE
## Called at the blank line that terminates the header fields. ## Called at the blank line that terminates the header fields.
@@ -470,13 +365,13 @@ class Base(object):
for func,(nr,nc) in OPTIONAL_CALLBACKS.items(): for func,(nr,nc) in OPTIONAL_CALLBACKS.items():
func = getattr(klass,func) func = getattr(klass,func)
ca = getattr(func,'milter_protocol',0) ca = getattr(func,'milter_protocol',0)
#print(func,hex(nr),hex(nc),hex(ca)) #print func,hex(nr),hex(nc),hex(ca)
p |= (nr|nc) & ~ca p |= (nr|nc) & ~ca
klass._protocol_mask = p klass._protocol_mask = p
return p return p
## Negotiate milter protocol options. Called by the ## Negotiate milter protocol options. Called by the
# <a href="milter_api/xxfi_negotiate.html"> # <a href="https://www.milter.org/developers/api/xxfi_negotiate">
# xffi_negotiate</a> callback. This is an advanced callback, # xffi_negotiate</a> callback. This is an advanced callback,
# do not override unless you know what you are doing. Most # do not override unless you know what you are doing. Most
# negotiation can be done simply by using the supplied # negotiation can be done simply by using the supplied
@@ -493,16 +388,11 @@ class Base(object):
def negotiate(self,opts): def negotiate(self,opts):
try: try:
self._actions,p,f1,f2 = opts self._actions,p,f1,f2 = opts
for func,stage in MACRO_CALLBACKS.items():
func = getattr(self,func)
syms = getattr(func,'_symlist',None)
if syms is not None:
self.setsymlist(stage,*syms)
opts[1] = self._protocol = p & ~self.protocol_mask() opts[1] = self._protocol = p & ~self.protocol_mask()
opts[2] = 0 opts[2] = 0
opts[3] = 0 opts[3] = 0
#self.log("Negotiated:",opts) #self.log("Negotiated:",opts)
except Exception as x: except:
# don't change anything if something went wrong # don't change anything if something went wrong
return ALL_OPTS return ALL_OPTS
return CONTINUE return CONTINUE
@@ -512,7 +402,7 @@ class Base(object):
## Return the value of an MTA macro. Sendmail macro names ## Return the value of an MTA macro. Sendmail macro names
# are either single chars (e.g. "j") or multiple chars enclosed # are either single chars (e.g. "j") or multiple chars enclosed
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent. # in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
# See <a href="milter_api/smfi_getsymval.html"> # See <a href="https://www.milter.org/developers/api/smfi_getsymval">
# smfi_getsymval</a> for default sendmail macros. # smfi_getsymval</a> for default sendmail macros.
# @param sym the macro name # @param sym the macro name
def getsymval(self,sym): def getsymval(self,sym):
@@ -526,7 +416,7 @@ class Base(object):
# head scratching. What will <i>really</i> irritate you, however, # head scratching. What will <i>really</i> irritate you, however,
# is that if you carefully double any '%%', your message will be # is that if you carefully double any '%%', your message will be
# sent - but with the '%%' still doubled! # sent - but with the '%%' still doubled!
# See <a href="milter_api/smfi_setreply.html"> # See <a href="https://www.milter.org/developers/api/smfi_setreply">
# smfi_setreply</a> for more information. # smfi_setreply</a> for more information.
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string. # @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>. # rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
@@ -548,32 +438,28 @@ class Base(object):
# set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, # set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT,
# M_DATA, M_EOM, M_EOH. # M_DATA, M_EOM, M_EOH.
# #
# May only be called from negotiate callback. Hence, this is an advanced # May only be called from negotiate callback.
# feature. Use the @@symlist function decorator to conviently set
# the macros used by a callback.
# @since 0.9.8, previous version was misspelled! # @since 0.9.8, previous version was misspelled!
# @param stage the protocol stage to set to macro list for, # @param stage the protocol stage to set to macro list for,
# one of the M_* constants defined in Milter # one of the M_* constants defined in Milter
# @param macros space separated and/or lists of strings # @param macros space separated and/or lists of strings
def setsymlist(self,stage,*macros): def setsymlist(self,stage,*macros):
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST") if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
if len(macros) > 5:
raise ValueError('setsymlist limited to 5 macros by MTA')
a = [] a = []
for m in macros: for m in macros:
try: try:
m = m.encode('utf8') m = m.encode('utf8')
except: pass except: pass
try: try:
m = m.split(b' ') m = m.split(' ')
a += m
except: pass except: pass
return self._ctx.setsymlist(stage,b' '.join(a)) a += m
return self._ctx.setsmlist(stage,' '.join(a))
# Milter methods which can only be called from eom callback. # Milter methods which can only be called from eom callback.
## Add a mail header field. ## Add a mail header field.
# Calls <a href="milter_api/smfi_addheader.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_addheader">
# smfi_addheader</a>. # smfi_addheader</a>.
# The <code>Milter.ADDHDRS</code> action flag must be set. # The <code>Milter.ADDHDRS</code> action flag must be set.
# #
@@ -587,7 +473,7 @@ class Base(object):
return self._ctx.addheader(field,value,idx) return self._ctx.addheader(field,value,idx)
## Change the value of a mail header field. ## Change the value of a mail header field.
# Calls <a href="milter_api/smfi_chgheader.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">
# smfi_chgheader</a>. # smfi_chgheader</a>.
# The <code>Milter.CHGHDRS</code> action flag must be set. # The <code>Milter.CHGHDRS</code> action flag must be set.
# #
@@ -601,7 +487,7 @@ class Base(object):
return self._ctx.chgheader(field,idx,value) return self._ctx.chgheader(field,idx,value)
## Add a recipient to the message. ## Add a recipient to the message.
# Calls <a href="milter_api/smfi_addrcpt.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">
# smfi_addrcpt</a>. # smfi_addrcpt</a>.
# If no corresponding mail header is added, this is like a Bcc. # If no corresponding mail header is added, this is like a Bcc.
# The syntax of the recipient is the same as used in the SMTP # The syntax of the recipient is the same as used in the SMTP
@@ -621,7 +507,7 @@ class Base(object):
raise DisabledAction("ADDRCPT_PAR") raise DisabledAction("ADDRCPT_PAR")
return self._ctx.addrcpt(rcpt,params) return self._ctx.addrcpt(rcpt,params)
## Delete a recipient from the message. ## Delete a recipient from the message.
# Calls <a href="milter_api/smfi_delrcpt.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">
# smfi_delrcpt</a>. # smfi_delrcpt</a>.
# The recipient should match one passed to the envrcpt callback. # The recipient should match one passed to the envrcpt callback.
# The <code>Milter.DELRCPT</code> action flag must be set. # The <code>Milter.DELRCPT</code> action flag must be set.
@@ -634,7 +520,7 @@ class Base(object):
return self._ctx.delrcpt(rcpt) return self._ctx.delrcpt(rcpt)
## Replace the message body. ## Replace the message body.
# Calls <a href="milter_api/smfi_replacebody.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">
# smfi_replacebody</a>. # smfi_replacebody</a>.
# The entire message body must be replaced. # The entire message body must be replaced.
# Call repeatedly with blocks of data until the entire body is transferred. # Call repeatedly with blocks of data until the entire body is transferred.
@@ -648,7 +534,7 @@ class Base(object):
return self._ctx.replacebody(body) return self._ctx.replacebody(body)
## Change the SMTP envelope sender address. ## Change the SMTP envelope sender address.
# Calls <a href="milter_api/smfi_chgfrom.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">
# smfi_chgfrom</a>. # smfi_chgfrom</a>.
# The syntax of the sender is that same as used in the SMTP # The syntax of the sender is that same as used in the SMTP
# MAIL FROM command (and as delivered to the envfrom callback), # MAIL FROM command (and as delivered to the envfrom callback),
@@ -665,7 +551,7 @@ class Base(object):
return self._ctx.chgfrom(sender,params) return self._ctx.chgfrom(sender,params)
## Quarantine the message. ## Quarantine the message.
# Calls <a href="milter_api/smfi_quarantine.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">
# smfi_quarantine</a>. # smfi_quarantine</a>.
# When quarantined, a message goes into the mailq as if to be delivered, # When quarantined, a message goes into the mailq as if to be delivered,
# but delivery is deferred until the message is unquarantined. # but delivery is deferred until the message is unquarantined.
@@ -679,7 +565,7 @@ class Base(object):
return self._ctx.quarantine(reason) return self._ctx.quarantine(reason)
## Tell the MTA to wait a bit longer. ## Tell the MTA to wait a bit longer.
# Calls <a href="milter_api/smfi_progress.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_progress">
# smfi_progress</a>. # smfi_progress</a>.
# Resets timeouts in the MTA that detect a "hung" milter. # Resets timeouts in the MTA that detect a "hung" milter.
def progress(self): def progress(self):
@@ -693,9 +579,9 @@ class Milter(Base):
## Provide simple logging to sys.stdout ## Provide simple logging to sys.stdout
def log(self,*msg): def log(self,*msg):
print('Milter:',end=None) print 'Milter:',
for i in msg: print(i,end=None) for i in msg: print i,
print() print
@noreply @noreply
def connect(self,hostname,family,hostaddr): def connect(self,hostname,family,hostaddr):
@@ -842,14 +728,9 @@ def runmilter(name,socketname,timeout = 0,rmsock=True):
# 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
# arbitrary keywords without crashing. We do provide envcallback and # arbitrary keywords without crashing. We do provide envcallback and
# dictfromlist to make parsing the ESMTP args convenient. # dictfromlist to make parsing the ESMTP args convenient.
if sys.version < '3.0.0': milter.set_envfrom_callback(lambda ctx,*str: ctx.getpriv().envfrom(*str))
milter.set_envfrom_callback(lambda ctx,*s: ctx.getpriv().envfrom(*s)) milter.set_envrcpt_callback(lambda ctx,*str: ctx.getpriv().envrcpt(*str))
milter.set_envrcpt_callback(lambda ctx,*s: ctx.getpriv().envrcpt(*s)) milter.set_header_callback(lambda ctx,fld,val: ctx.getpriv().header(fld,val))
milter.set_header_callback(lambda ctx,f,v: ctx.getpriv().header(f,v))
else:
milter.set_envfrom_callback(lambda ctx,*b: ctx.getpriv().envfrom_bytes(*b))
milter.set_envrcpt_callback(lambda ctx,*b: ctx.getpriv().envrcpt_bytes(*b))
milter.set_header_callback(lambda ctx,f,v: ctx.getpriv().header_bytes(f,v))
milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh()) milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh())
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())
+5 -8
View File
@@ -46,9 +46,8 @@
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. # Copyright 2001,2002,2003,2004,2005 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.
from __future__ import print_function
import time import time
from Milter.plock import PLock from plock import PLock
class AddrCache(object): class AddrCache(object):
time_format = '%Y%b%d %H:%M:%S %Z' time_format = '%Y%b%d %H:%M:%S %Z'
@@ -132,8 +131,8 @@ class AddrCache(object):
if not ts or ts > too_old: if not ts or ts > too_old:
return res return res
del self.cache[lsender] del self.cache[lsender]
raise KeyError(sender) raise KeyError, sender
except KeyError as x: except KeyError,x:
try: try:
user,host = sender.split('@',1) user,host = sender.split('@',1)
return self.__getitem__(host) return self.__getitem__(host)
@@ -148,8 +147,7 @@ class AddrCache(object):
if not ts: return # already permanent if not ts: return # already permanent
self.cache[lsender] = (None,res) self.cache[lsender] = (None,res)
if not res: if not res:
with open(self.fname,'a') as fp: print >>open(self.fname,'a'),sender
print(sender,file=fp)
def __setitem__(self,sender,res): def __setitem__(self,sender,res):
lsender = sender.lower() lsender = sender.lower()
@@ -157,8 +155,7 @@ class AddrCache(object):
self.cache[lsender] = (now,res) self.cache[lsender] = (now,res)
if not res and self.fname: if not res and self.fname:
s = time.strftime(AddrCache.time_format,time.localtime(now)) s = time.strftime(AddrCache.time_format,time.localtime(now))
with open(self.fname,'a') as fp: print >>open(self.fname,'a'),sender,s # log refreshed senders
print(sender,s,file=fp) # log refreshed senders
def __len__(self): def __len__(self):
return len(self.cache) return len(self.cache)
+6 -11
View File
@@ -1,7 +1,4 @@
try: from ConfigParser import ConfigParser
from configparser import ConfigParser
except:
from ConfigParser import ConfigParser
import os.path import os.path
class MilterConfigParser(ConfigParser): class MilterConfigParser(ConfigParser):
@@ -14,10 +11,10 @@ class MilterConfigParser(ConfigParser):
# which screws up iterating over all options in a section. # which screws up iterating over all options in a section.
# Worse, passing "defaults" with vars= overrides the config file! # Worse, passing "defaults" with vars= overrides the config file!
# So we roll our own defaults. # So we roll our own defaults.
def get(self,sect,opt,fallback=None,**kwds): def get(self,sect,opt):
if not self.has_option(sect,opt) and not fallback and opt in self.defaults: if not self.has_option(sect,opt) and opt in self.defaults:
return self.defaults[opt] return self.defaults[opt]
return ConfigParser.get(self,sect,opt,fallback=fallback,**kwds) return ConfigParser.get(self,sect,opt)
def getlist(self,sect,opt): def getlist(self,sect,opt):
if self.has_option(sect,opt): if self.has_option(sect,opt):
@@ -34,8 +31,7 @@ class MilterConfigParser(ConfigParser):
if q.startswith('file:'): if q.startswith('file:'):
domain = q[5:].lower() domain = q[5:].lower()
fname = os.path.join(dir,domain) fname = os.path.join(dir,domain)
with open(fname,'r') as fp: d[domain] = d.setdefault(domain,[]) + open(fname,'r').read().split()
d[domain] = d.setdefault(domain,[]) + fp.read().split()
else: else:
user,domain = q.split('@') user,domain = q.split('@')
d.setdefault(domain.lower(),[]).append(user) d.setdefault(domain.lower(),[]).append(user)
@@ -53,8 +49,7 @@ class MilterConfigParser(ConfigParser):
addr = addr.strip() addr = addr.strip()
if addr.startswith('file:'): if addr.startswith('file:'):
fname = os.path.join(dir,addr[5:]) fname = os.path.join(dir,addr[5:])
with open(fname,'r') as fp: for a in open(fname,'r').read().split():
for a in fp.read().split():
d[a] = q d[a] = q
else: else:
d[addr] = q d[addr] = q
+5 -7
View File
@@ -1,7 +1,6 @@
## @package Milter.dns ## @package Milter.dns
# Provide a higher level interface to pydns. # Provide a higher level interface to pydns.
from __future__ import print_function
import DNS import DNS
from DNS import DNSError from DNS import DNSError
@@ -26,8 +25,8 @@ def DNSLookup(name, qtype):
# A RR as dotted quad. For consistency, this driver should # A RR as dotted quad. For consistency, this driver should
# return both as binary string. # return both as binary string.
return [((a['name'], a['typename']), a['data']) for a in resp.answers] return [((a['name'], a['typename']), a['data']) for a in resp.answers]
except IOError as x: except IOError, x:
raise DNSError(str(x)) raise DNSError, str(x)
class Session(object): class Session(object):
"""A Session object has a simple cache with no TTL that is valid """A Session object has a simple cache with no TTL that is valid
@@ -74,7 +73,6 @@ class Session(object):
if name.endswith('.'): name = name[:-1] if name.endswith('.'): name = name[:-1]
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True): if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
return [] # invalid DNS name (too long or empty) return [] # invalid DNS name (too long or empty)
name = name.lower()
result = self.cache.get( (name, qtype) ) result = self.cache.get( (name, qtype) )
cname = None cname = None
if result: return result if result: return result
@@ -98,7 +96,7 @@ class Session(object):
#return result # if too many == NX_DOMAIN #return result # if too many == NX_DOMAIN
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME) raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
cnames[name] = cname cnames[name] = cname
if cname.lower().rstrip('.') in cnames: if cname in cnames:
raise DNSError('CNAME loop') raise DNSError('CNAME loop')
result = self.dns(cname, qtype, cnames=cnames) result = self.dns(cname, qtype, cnames=cnames)
if result: if result:
@@ -121,5 +119,5 @@ if __name__ == '__main__':
import sys import sys
s = Session() s = Session()
for n,t in zip(*[iter(sys.argv[1:])]*2): for n,t in zip(*[iter(sys.argv[1:])]*2):
print(n,t) print n,t
print(s.dns(n,t)) print s.dns(n,t)
+8 -12
View File
@@ -69,16 +69,12 @@
# a DSN or use a null MAIL FROM with an email address obtained from # a DSN or use a null MAIL FROM with an email address obtained from
# anywhere else. # anywhere else.
# #
from __future__ import print_function
import smtplib import smtplib
import socket import socket
try: from email.Message import Message
from email.message import Message
except:
from email.Message import Message
import Milter import Milter
import Milter.dns as dns
import time import time
import dns
## Send DSN. ## Send DSN.
# Try the published MX names in order, rejecting obviously bogus entries # Try the published MX names in order, rejecting obviously bogus entries
@@ -145,13 +141,13 @@ def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
if badrcpts: if badrcpts:
return badrcpts return badrcpts
return None # success return None # success
except smtplib.SMTPRecipientsRefused as x: except smtplib.SMTPRecipientsRefused,x:
if len(x.recipients) == 1: if len(x.recipients) == 1:
return x.recipients.values()[0] # permanent error return x.recipients.values()[0] # permanent error
return x.recipients return x.recipients
except smtplib.SMTPSenderRefused as x: except smtplib.SMTPSenderRefused,x:
return x.args[:2] # does not accept DSN return x.args[:2] # does not accept DSN
except smtplib.SMTPDataError as x: except smtplib.SMTPDataError,x:
return x.args # permanent error return x.args # permanent error
except smtplib.SMTPException: except smtplib.SMTPException:
pass # any other error, try next MX pass # any other error, try next MX
@@ -234,6 +230,6 @@ Subject: Test
Test DSN template Test DSN template
""" """
) )
print(msg.as_string()) print msg.as_string()
# print(send_dsn(f,msg.as_string())) # print send_dsn(f,msg.as_string())
# print(send_dsn(q.s,'mail.example.com',msg.as_string())) # print send_dsn(q.s,'mail.example.com',msg.as_string())
+4 -5
View File
@@ -9,7 +9,6 @@
# wiley-268-8196.roadrunner.nf.net at ('205.251.174.46', 4810) # wiley-268-8196.roadrunner.nf.net at ('205.251.174.46', 4810)
# cbl-sd-02-79.aster.com.do at ('200.88.62.79', 4153) # cbl-sd-02-79.aster.com.do at ('200.88.62.79', 4153)
from __future__ import print_function
import re import re
ip3 = re.compile('[0-9]{1,3}') ip3 = re.compile('[0-9]{1,3}')
@@ -54,11 +53,11 @@ def is_dynip(host,addr):
if host.find(addr) >= 0: return True if host.find(addr) >= 0: return True
if addr.find(':') >= 0: return False # IP6 if addr.find(':') >= 0: return False # IP6
a = addr.split('.') a = addr.split('.')
ia = list(map(int,a)) ia = map(int,a)
h = host h = host
m = ip3.findall(host) m = ip3.findall(host)
if m: if m:
g = list(map(int,m))[:4] g = map(int,m)[:4]
ia3 = (ia[1:],ia[:3]) ia3 = (ia[1:],ia[:3])
if g[-3:] in ia3: return True if g[-3:] in ia3: return True
if g[0] == ia[3] and g[1:3] == ia[:2]: return True if g[0] == ia[3] and g[1:3] == ia[:2]: return True
@@ -92,6 +91,6 @@ if __name__ == '__main__':
if ip in seen: continue if ip in seen: continue
seen.add(ip) seen.add(ip)
if is_dynip(host,ip): if is_dynip(host,ip):
print('%s\t%s DYN' % (ip,host)) print '%s\t%s DYN' % (ip,host)
else: else:
print('%s\t%s' % (ip,host)) print '%s\t%s' % (ip,host)
+1 -2
View File
@@ -1,4 +1,3 @@
from __future__ import print_function
import time import time
import shelve import shelve
import thread import thread
@@ -59,7 +58,7 @@ class Greylist(object):
cnt = 0 cnt = 0
dbp = self.dbp dbp = self.dbp
for key, r in dbp.iteritems(): for key, r in dbp.iteritems():
#print(key,r,time.ctime(now)) #print key,r,time.ctime(now)
if now > r.lastseen + self.greylist_retain: if now > r.lastseen + self.greylist_retain:
self.lock.acquire() self.lock.acquire()
try: try:
+1 -4
View File
@@ -2,10 +2,7 @@ import time
import logging import logging
import urllib import urllib
import sqlite3 import sqlite3
try: import thread
import thread
except:
import _thread as thread
from datetime import datetime from datetime import datetime
log = logging.getLogger('milter.greylist') log = logging.getLogger('milter.greylist')
+3 -3
View File
@@ -11,7 +11,7 @@ class PLock(object):
self.basename = basename self.basename = basename
self.fp = None self.fp = None
def lock(self,lockname=None,mode=0o660,strict_perms=False): def lock(self,lockname=None,mode=0660,strict_perms=False):
"Start an update transaction. Return FILE to write new version." "Start an update transaction. Return FILE to write new version."
self.unlock() self.unlock()
if not lockname: if not lockname:
@@ -21,7 +21,7 @@ class PLock(object):
st = os.stat(self.basename) st = os.stat(self.basename)
mode |= st.st_mode mode |= st.st_mode
except OSError: pass except OSError: pass
u = os.umask(0o2) u = os.umask(0002)
try: try:
fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode) fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode)
finally: finally:
@@ -46,7 +46,7 @@ class PLock(object):
def commit(self,backname=None): def commit(self,backname=None):
"Commit update transaction with optional backup file." "Commit update transaction with optional backup file."
if not self.fp: if not self.fp:
raise IOError("File not locked") raise IOError,"File not locked"
self.fp.close() self.fp.close()
self.fp = None self.fp = None
if backname: if backname:
-87
View File
@@ -1,87 +0,0 @@
try:
try:
from berkeleydb import db
except:
from bsddb3 import db
class DB(object):
def open(self,fname,mode):
if mode == 'r': flags = db.DB_RDONLY
else: raise RuntimeException('unsupported mode')
self.f = db.DB()
self.f.open(fname,flags=flags)
def __contains__(self,key):
return not not self.f.get(key)
def __getitem__(self,key):
v = self.f.get(key)
if not v: raise KeyError(key)
return v
def close(self):
self.f.close()
def dbmopen(fname,mode):
f = DB()
f.open(fname,mode)
return f
except ModuleNotFoundError: raise
except:
import anydbm as dbm
dbmopen = dbm.open
class MTAPolicy(object):
"Get SPF policy by result from sendmail style access file."
def __init__(self,sender,conf,access_file=None):
if not access_file:
access_file = conf.access_file
self.use_nulls = conf.access_file_nulls
try:
self.use_colon = conf.access_file_colon
except:
self.use_colon = True
self.sender = sender
self.domain = sender.split('@')[-1].lower()
self.acf = None
self.access_file = access_file
def close(self):
if self.acf:
self.acf.close()
def __enter__(self):
self.acf = None
if self.access_file:
try:
self.acf = dbmopen(self.access_file,'r')
except:
print('%s: Cannot open for reading'%self.access_file)
raise
return self
def __exit__(self,t,v,b): self.close()
def getPolicy(self,pfx):
acf = self.acf
if not acf: return None
if self.use_nulls: sfx = b'\x00'
else: sfx = b''
if self.use_colon:
sep = b':'
else:
sep = b'!'
pfx = pfx.encode() + sep
try: # try with localpart@domain
return acf[pfx + self.sender.encode() + sfx].rstrip(b'\x00').decode()
except KeyError:
try: # try with domain
d = self.domain.encode()
k = pfx + d + sfx
while not k in acf and b'.' in d:
# check partial domains
d = b'.'.join(d.split(b'.')[1:])
k = pfx + b'.' + d + sfx
return acf[k].rstrip(b'\x00').decode()
except KeyError:
try: # try bare prefix
return acf[pfx + sfx].rstrip(b'\x00').decode()
except KeyError:
try:
return acf[pfx[:-1] + sfx].rstrip(b'\x00').decode()
except KeyError:
return None
+3 -4
View File
@@ -6,7 +6,6 @@ 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 it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form. and disclaimer are retained in their original form.
""" """
from __future__ import print_function
import struct import struct
#from spf import RE_IP4 #from spf import RE_IP4
import re import re
@@ -81,11 +80,11 @@ def inet_pton(p):
(0, 0, 0, 0, 0, 65535, 258, 772) (0, 0, 0, 0, 0, 65535, 258, 772)
>>> try: inet_pton('::1.2.3.4.5') >>> try: inet_pton('::1.2.3.4.5')
... except ValueError as x: print(x) ... except ValueError,x: print x
::1.2.3.4.5 ::1.2.3.4.5
""" """
if p == '::': if p == '::':
return b'\0'*16 return '\0'*16
s = p s = p
m = RE_IP4.search(s) m = RE_IP4.search(s)
try: try:
@@ -115,4 +114,4 @@ def inet_pton(p):
return struct.pack('!HHHHHHHH', return struct.pack('!HHHHHHHH',
*[int(s,16) for s in a[0].split(':')]) *[int(s,16) for s in a[0].split(':')])
except ValueError: pass except ValueError: pass
raise ValueError(p) raise ValueError,p
-552
View File
@@ -1,552 +0,0 @@
"""A parser for SGML, using the derived class as a static DTD."""
# XXX This only supports those SGML features used by HTML.
# XXX There should be a way to distinguish between PCDATA (parsed
# character data -- the normal case), RCDATA (replaceable character
# data -- only char and entity references and end tags are special)
# and CDATA (character data -- only end tags are special). RCDATA is
# not supported at all.
from __future__ import print_function
try:
import _markupbase
except:
import markupbase as _markupbase
import re
__all__ = ["SGMLParser", "SGMLParseError"]
# Regular expressions used for parsing
interesting = re.compile('[&<]')
incomplete = re.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|'
'<([a-zA-Z][^<>]*|'
'/([a-zA-Z][^<>]*)?|'
'![^<>]*)?')
entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
charref = re.compile('&#([0-9]+)[^0-9]')
starttagopen = re.compile('<[>a-zA-Z]')
shorttagopen = re.compile('<[a-zA-Z][-.a-zA-Z0-9]*/')
shorttag = re.compile('<([a-zA-Z][-.a-zA-Z0-9]*)/([^/]*)/')
piclose = re.compile('>')
endbracket = re.compile('[<>]')
tagfind = re.compile('[a-zA-Z][-_.a-zA-Z0-9]*')
attrfind = re.compile(
r'\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)(\s*=\s*'
r'(\'[^\']*\'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?')
class SGMLParseError(RuntimeError):
"""Exception raised for all parse errors."""
pass
# SGML parser base class -- find tags and call handler functions.
# Usage: p = SGMLParser(); p.feed(data); ...; p.close().
# The dtd is defined by deriving a class which defines methods
# with special names to handle tags: start_foo and end_foo to handle
# <foo> and </foo>, respectively, or do_foo to handle <foo> by itself.
# (Tags are converted to lower case for this purpose.) The data
# between tags is passed to the parser by calling self.handle_data()
# with some data as argument (the data may be split up in arbitrary
# chunks). Entity references are passed by calling
# self.handle_entityref() with the entity reference as argument.
class SGMLParser(_markupbase.ParserBase):
# Definition of entities -- derived classes may override
entity_or_charref = re.compile('&(?:'
'([a-zA-Z][-.a-zA-Z0-9]*)|#([0-9]+)'
')(;?)')
def __init__(self, verbose=0):
"""Initialize and reset this instance."""
self.verbose = verbose
self.reset()
def reset(self):
"""Reset this instance. Loses all unprocessed data."""
self.__starttag_text = None
self.rawdata = ''
self.stack = []
self.lasttag = '???'
self.nomoretags = 0
self.literal = 0
_markupbase.ParserBase.reset(self)
def setnomoretags(self):
"""Enter literal mode (CDATA) till EOF.
Intended for derived classes only.
"""
self.nomoretags = self.literal = 1
def setliteral(self, *args):
"""Enter literal mode (CDATA).
Intended for derived classes only.
"""
self.literal = 1
def feed(self, data):
"""Feed some data to the parser.
Call this as often as you want, with as little or as much text
as you want (may include '\n'). (This just saves the text,
all the processing is done by goahead().)
"""
self.rawdata = self.rawdata + data
self.goahead(0)
def close(self):
"""Handle the remaining data."""
self.goahead(1)
def error(self, message):
raise SGMLParseError(message)
# Internal -- handle data as far as reasonable. May leave state
# and data to be processed by a subsequent call. If 'end' is
# true, force handling all data as if followed by EOF marker.
def goahead(self, end):
rawdata = self.rawdata
i = 0
n = len(rawdata)
while i < n:
if self.nomoretags:
self.handle_data(rawdata[i:n])
i = n
break
match = interesting.search(rawdata, i)
if match: j = match.start()
else: j = n
if i < j:
self.handle_data(rawdata[i:j])
i = j
if i == n: break
if rawdata[i] == '<':
if starttagopen.match(rawdata, i):
if self.literal:
self.handle_data(rawdata[i])
i = i+1
continue
k = self.parse_starttag(i)
if k < 0: break
i = k
continue
if rawdata.startswith("</", i):
k = self.parse_endtag(i)
if k < 0: break
i = k
self.literal = 0
continue
if self.literal:
if n > (i + 1):
self.handle_data("<")
i = i+1
else:
# incomplete
break
continue
if rawdata.startswith("<!--", i):
# Strictly speaking, a comment is --.*--
# within a declaration tag <!...>.
# This should be removed,
# and comments handled only in parse_declaration.
k = self.parse_comment(i)
if k < 0: break
i = k
continue
if rawdata.startswith("<?", i):
k = self.parse_pi(i)
if k < 0: break
i = i+k
continue
if rawdata.startswith("<!", i):
# This is some sort of declaration; in "HTML as
# deployed," this should only be the document type
# declaration ("<!DOCTYPE html...>").
k = self.parse_declaration(i)
if k < 0: break
i = k
continue
elif rawdata[i] == '&':
if self.literal:
self.handle_data(rawdata[i])
i = i+1
continue
match = charref.match(rawdata, i)
if match:
name = match.group(1)
self.handle_charref(name)
i = match.end(0)
if rawdata[i-1] != ';': i = i-1
continue
match = entityref.match(rawdata, i)
if match:
name = match.group(1)
self.handle_entityref(name)
i = match.end(0)
if rawdata[i-1] != ';': i = i-1
continue
else:
self.error('neither < nor & ??')
# We get here only if incomplete matches but
# nothing else
match = incomplete.match(rawdata, i)
if not match:
self.handle_data(rawdata[i])
i = i+1
continue
j = match.end(0)
if j == n:
break # Really incomplete
self.handle_data(rawdata[i:j])
i = j
# end while
if end and i < n:
self.handle_data(rawdata[i:n])
i = n
self.rawdata = rawdata[i:]
# XXX if end: check for empty stack
# Extensions for the DOCTYPE scanner:
_decl_otherchars = '='
# Internal -- parse processing instr, return length or -1 if not terminated
def parse_pi(self, i):
rawdata = self.rawdata
if rawdata[i:i+2] != '<?':
self.error('unexpected call to parse_pi()')
match = piclose.search(rawdata, i+2)
if not match:
return -1
j = match.start(0)
self.handle_pi(rawdata[i+2: j])
j = match.end(0)
return j-i
def get_starttag_text(self):
return self.__starttag_text
# Internal -- handle starttag, return length or -1 if not terminated
def parse_starttag(self, i):
self.__starttag_text = None
start_pos = i
rawdata = self.rawdata
if shorttagopen.match(rawdata, i):
# SGML shorthand: <tag/data/ == <tag>data</tag>
# XXX Can data contain &... (entity or char refs)?
# XXX Can data contain < or > (tag characters)?
# XXX Can there be whitespace before the first /?
match = shorttag.match(rawdata, i)
if not match:
return -1
tag, data = match.group(1, 2)
self.__starttag_text = '<%s/' % tag
tag = tag.lower()
k = match.end(0)
self.finish_shorttag(tag, data)
self.__starttag_text = rawdata[start_pos:match.end(1) + 1]
return k
# XXX The following should skip matching quotes (' or ")
# As a shortcut way to exit, this isn't so bad, but shouldn't
# be used to locate the actual end of the start tag since the
# < or > characters may be embedded in an attribute value.
match = endbracket.search(rawdata, i+1)
if not match:
return -1
j = match.start(0)
# Now parse the data between i+1 and j into a tag and attrs
attrs = []
if rawdata[i:i+2] == '<>':
# SGML shorthand: <> == <last open tag seen>
k = j
tag = self.lasttag
else:
match = tagfind.match(rawdata, i+1)
if not match:
self.error('unexpected call to parse_starttag')
k = match.end(0)
tag = rawdata[i+1:k].lower()
self.lasttag = tag
while k < j:
match = attrfind.match(rawdata, k)
if not match: break
attrname, rest, attrvalue = match.group(1, 2, 3)
if not rest:
attrvalue = attrname
else:
if (attrvalue[:1] == "'" == attrvalue[-1:] or
attrvalue[:1] == '"' == attrvalue[-1:]):
# strip quotes
attrvalue = attrvalue[1:-1]
attrvalue = self.entity_or_charref.sub(
self._convert_ref, attrvalue)
attrs.append((attrname.lower(), attrvalue))
k = match.end(0)
if rawdata[j] == '>':
j = j+1
self.__starttag_text = rawdata[start_pos:j]
self.finish_starttag(tag, attrs)
return j
# Internal -- convert entity or character reference
def _convert_ref(self, match):
if match.group(2):
return self.convert_charref(match.group(2)) or \
'&#%s%s' % match.groups()[1:]
elif match.group(3):
return self.convert_entityref(match.group(1)) or \
'&%s;' % match.group(1)
else:
return '&%s' % match.group(1)
# Internal -- parse endtag
def parse_endtag(self, i):
rawdata = self.rawdata
match = endbracket.search(rawdata, i+1)
if not match:
return -1
j = match.start(0)
tag = rawdata[i+2:j].strip().lower()
if rawdata[j] == '>':
j = j+1
self.finish_endtag(tag)
return j
# Internal -- finish parsing of <tag/data/ (same as <tag>data</tag>)
def finish_shorttag(self, tag, data):
self.finish_starttag(tag, [])
self.handle_data(data)
self.finish_endtag(tag)
# Internal -- finish processing of start tag
# Return -1 for unknown tag, 0 for open-only tag, 1 for balanced tag
def finish_starttag(self, tag, attrs):
try:
method = getattr(self, 'start_' + tag)
except AttributeError:
try:
method = getattr(self, 'do_' + tag)
except AttributeError:
self.unknown_starttag(tag, attrs)
return -1
else:
self.handle_starttag(tag, method, attrs)
return 0
else:
self.stack.append(tag)
self.handle_starttag(tag, method, attrs)
return 1
# Internal -- finish processing of end tag
def finish_endtag(self, tag):
if not tag:
found = len(self.stack) - 1
if found < 0:
self.unknown_endtag(tag)
return
else:
if tag not in self.stack:
try:
method = getattr(self, 'end_' + tag)
except AttributeError:
self.unknown_endtag(tag)
else:
self.report_unbalanced(tag)
return
found = len(self.stack)
for i in range(found):
if self.stack[i] == tag: found = i
while len(self.stack) > found:
tag = self.stack[-1]
try:
method = getattr(self, 'end_' + tag)
except AttributeError:
method = None
if method:
self.handle_endtag(tag, method)
else:
self.unknown_endtag(tag)
del self.stack[-1]
# Overridable -- handle start tag
def handle_starttag(self, tag, method, attrs):
method(attrs)
# Overridable -- handle end tag
def handle_endtag(self, tag, method):
method()
# Example -- report an unbalanced </...> tag.
def report_unbalanced(self, tag):
if self.verbose:
print('*** Unbalanced </' + tag + '>')
print('*** Stack:', self.stack)
def convert_charref(self, name):
"""Convert character reference, may be overridden."""
try:
n = int(name)
except ValueError:
return
if not 0 <= n <= 127:
return
return self.convert_codepoint(n)
def convert_codepoint(self, codepoint):
return chr(codepoint)
def handle_charref(self, name):
"""Handle character reference, no need to override."""
replacement = self.convert_charref(name)
if replacement is None:
self.unknown_charref(name)
else:
self.handle_data(replacement)
# Definition of entities -- derived classes may override
entitydefs = \
{'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': '\''}
def convert_entityref(self, name):
"""Convert entity references.
As an alternative to overriding this method; one can tailor the
results by setting up the self.entitydefs mapping appropriately.
"""
table = self.entitydefs
if name in table:
return table[name]
else:
return
def handle_entityref(self, name):
"""Handle entity references, no need to override."""
replacement = self.convert_entityref(name)
if replacement is None:
self.unknown_entityref(name)
else:
self.handle_data(replacement)
# Example -- handle data, should be overridden
def handle_data(self, data):
pass
# Example -- handle comment, could be overridden
def handle_comment(self, data):
pass
# Example -- handle declaration, could be overridden
def handle_decl(self, decl):
pass
# Example -- handle processing instruction, could be overridden
def handle_pi(self, data):
pass
# To be overridden -- handlers for unknown objects
def unknown_starttag(self, tag, attrs): pass
def unknown_endtag(self, tag): pass
def unknown_charref(self, ref): pass
def unknown_entityref(self, ref): pass
class TestSGMLParser(SGMLParser):
def __init__(self, verbose=0):
self.testdata = ""
SGMLParser.__init__(self, verbose)
def handle_data(self, data):
self.testdata = self.testdata + data
if len(repr(self.testdata)) >= 70:
self.flush()
def flush(self):
data = self.testdata
if data:
self.testdata = ""
print('data:', repr(data))
def handle_comment(self, data):
self.flush()
r = repr(data)
if len(r) > 68:
r = r[:32] + '...' + r[-32:]
print('comment:', r)
def unknown_starttag(self, tag, attrs):
self.flush()
if not attrs:
print('start tag: <' + tag + '>')
else:
print('start tag: <' + tag, end=' ')
for name, value in attrs:
print(name + '=' + '"' + value + '"', end=' ')
print('>')
def unknown_endtag(self, tag):
self.flush()
print('end tag: </' + tag + '>')
def unknown_entityref(self, ref):
self.flush()
print('*** unknown entity ref: &' + ref + ';')
def unknown_charref(self, ref):
self.flush()
print('*** unknown char ref: &#' + ref + ';')
def unknown_decl(self, data):
self.flush()
print('*** unknown decl: [' + data + ']')
def close(self):
SGMLParser.close(self)
self.flush()
def test(args = None):
import sys
if args is None:
args = sys.argv[1:]
if args and args[0] == '-s':
args = args[1:]
klass = SGMLParser
else:
klass = TestSGMLParser
if args:
file = args[0]
else:
file = 'test.html'
if file == '-':
f = sys.stdin
else:
try:
f = open(file, 'r')
except IOError as msg:
print(file, ":", msg)
sys.exit(1)
data = f.read()
if f is not sys.stdin:
f.close()
x = klass()
for c in data:
x.feed(c)
x.close()
if __name__ == '__main__':
test()
+40 -95
View File
@@ -1,12 +1,8 @@
## @package Milter.test ## @package Milter.test
# A test framework for milters # A test framework for milters
from __future__ import print_function import rfc822
import mime import StringIO
try:
from io import BytesIO
except:
from StringIO import StringIO as BytesIO
import Milter import Milter
Milter.NOREPLY = Milter.CONTINUE Milter.NOREPLY = Milter.CONTINUE
@@ -14,15 +10,12 @@ Milter.NOREPLY = Milter.CONTINUE
## Test mixin for unit testing %milter applications. ## Test mixin for unit testing %milter applications.
# This mixin overrides many Milter.MilterBase methods # This mixin overrides many Milter.MilterBase methods
# with stub versions that simply record what was done. # with stub versions that simply record what was done.
# @deprecated Use Milter.test.TestCtx
# @since 0.9.8 # @since 0.9.8
class TestBase(object): class TestBase(object):
def __init__(self,logfile='test/milter.log'): def __init__(self,logfile='test/milter.log'):
self._protocol = 0 self._protocol = 0
self.logfp = open(logfile,"a") self.logfp = open(logfile,"a")
## The MAIL FROM for the current email being fed to the %milter
self._sender = None
## List of recipients deleted ## List of recipients deleted
self._delrcpt = [] self._delrcpt = []
## List of recipients added ## List of recipients added
@@ -35,25 +28,15 @@ class TestBase(object):
self._bodyreplaced = False self._bodyreplaced = False
## True if the %milter changed any headers. ## True if the %milter changed any headers.
self._headerschanged = False self._headerschanged = False
## True if the %milter changed the envelope from.
self._envfromchanged = False
## Reply codes and messages set by the %milter ## Reply codes and messages set by the %milter
self._reply = None self._reply = None
## The rfc822 message object for the current email being fed to the %milter. ## The rfc822 message object for the current email being fed to the %milter.
self._msg = None self._msg = None
## The protocol stage for macros returned
self._stage = None
## The macros returned by protocol stage
self._symlist = [ None, None, None, None, None, None, None ] self._symlist = [ None, None, None, None, None, None, None ]
def _close(self):
if self.logfp:
self.logfp.close()
self.logfp = None
def log(self,*msg): def log(self,*msg):
for i in msg: print(i,file=self.logfp,end=None) for i in msg: print >>self.logfp, i,
print(file=self.logfp) print >>self.logfp
## Set a macro value. ## Set a macro value.
# These are retrieved by the %milter with getsymval. # These are retrieved by the %milter with getsymval.
@@ -63,40 +46,21 @@ class TestBase(object):
self._macros[name] = val self._macros[name] = val
def getsymval(self,name): def getsymval(self,name):
stage = self._stage # FIXME: track stage, and use _symlist
if stage is not None and stage >= 0: return self._macros.get(name,'')
syms = self._symlist[stage]
if syms is not None and name not in syms:
return None
return self._macros.get(name,None)
def replacebody(self,chunk): def replacebody(self,chunk):
if self._body: if self._body:
self._body.write(chunk) self._body.write(chunk)
self._bodyreplaced = True self._bodyreplaced = True
else: else:
raise IOError("replacebody not called from eom()") raise IOError,"replacebody not called from eom()"
def chgfrom(self,sender,params=None):
if not self._body:
raise IOError("chgfrom not called from eom()")
self.log('chgfrom: sender=%s' % (sender))
self._envfromchanged = True
self._sender = sender
# TODO: write implement quarantine()
def quarantine(self,reason):
raise NotImplemented
# TODO: measure time between milter calls
def progress(self):
pass
# FIXME: rfc822 indexing does not really reflect the way chg/add header # FIXME: rfc822 indexing does not really reflect the way chg/add header
# work for a %milter # work for a %milter
def chgheader(self,field,idx,value): def chgheader(self,field,idx,value):
if not self._body: if not self._body:
raise IOError("chgheader not called from eom()") raise IOError,"chgheader not called from eom()"
self.log('chgheader: %s[%d]=%s' % (field,idx,value)) self.log('chgheader: %s[%d]=%s' % (field,idx,value))
if value == '': if value == '':
del self._msg[field] del self._msg[field]
@@ -106,19 +70,19 @@ class TestBase(object):
def addheader(self,field,value,idx=-1): def addheader(self,field,value,idx=-1):
if not self._body: if not self._body:
raise IOError("addheader not called from eom()") raise IOError,"addheader not called from eom()"
self.log('addheader: %s=%s' % (field,value)) self.log('addheader: %s=%s' % (field,value))
self._msg[field] = value self._msg[field] = value
self._headerschanged = True self._headerschanged = True
def delrcpt(self,rcpt): def delrcpt(self,rcpt):
if not self._body: if not self._body:
raise IOError("delrcpt not called from eom()") raise IOError,"delrcpt not called from eom()"
self._delrcpt.append(rcpt) self._delrcpt.append(rcpt)
def addrcpt(self,rcpt): def addrcpt(self,rcpt):
if not self._body: if not self._body:
raise IOError("addrcpt not called from eom()") raise IOError,"addrcpt not called from eom()"
self._addrcpt.append(rcpt) self._addrcpt.append(rcpt)
## Save the reply codes and messages in self._reply. ## Save the reply codes and messages in self._reply.
@@ -126,10 +90,7 @@ class TestBase(object):
self._reply = (rcode,xcode) + msg self._reply = (rcode,xcode) + msg
def setsymlist(self,stage,macros): def setsymlist(self,stage,macros):
if not self._actions & Milter.SETSYMLIST: if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
raise DisabledAction("SETSYMLIST")
if self._stage != -1:
raise RuntimeError("setsymlist may only be called from negotiate")
# not used yet, but just for grins we save the data # not used yet, but just for grins we save the data
a = [] a = []
for m in macros: for m in macros:
@@ -137,14 +98,9 @@ class TestBase(object):
m = m.encode('utf8') m = m.encode('utf8')
except: pass except: pass
try: try:
m = m.split(b' ') m = m.split(' ')
except: pass except: pass
a += m a += m
if len(a) > 5:
raise ValueError('setsymlist limited to 5 macros by MTA')
if self._symlist[stage] is not None:
raise ValueError('setsymlist already called for stage:'+stage)
print('setsymlist',stage,a)
self._symlist[stage] = set(a) self._symlist[stage] = set(a)
## Feed a file like object to the %milter. Calls envfrom, envrcpt for ## Feed a file like object to the %milter. Calls envfrom, envrcpt for
@@ -163,54 +119,49 @@ class TestBase(object):
self._bodyreplaced = False self._bodyreplaced = False
self._headerschanged = False self._headerschanged = False
self._reply = None self._reply = None
self._sender = '<%s>'%sender msg = rfc822.Message(fp)
msg = mime.message_from_file(fp) rc = self.envfrom('<%s>'%sender)
# envfrom
self._stage = Milter.M_ENVFROM
rc = self.envfrom(self._sender)
self._stage = None
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
# envrcpt
for rcpt in (rcpt,) + rcpts: for rcpt in (rcpt,) + rcpts:
self._stage = Milter.M_ENVRCPT
rc = self.envrcpt('<%s>'%rcpt) rc = self.envrcpt('<%s>'%rcpt)
self._stage = None
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
# data line = None
self._stage = Milter.M_DATA for h in msg.headers:
rc = self.data() if h[:1].isspace():
self._stage = None line = line + h
continue
if not line:
line = h
continue
s = line.split(': ',1)
if len(s) > 1: val = s[1].strip()
else: val = ''
rc = self.header(s[0],val)
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
# header line = h
for h,val in msg.items(): if line:
rc = self.header_bytes(h,val.encode()) s = line.split(': ',1)
rc = self.header(s[0],s[1])
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
# eoh
self._stage = Milter.M_EOH
rc = self.eoh() rc = self.eoh()
self._stage = None
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
# body
header,body = msg.as_bytes().split(b'\n\n',1)
bfp = BytesIO(body)
while 1: while 1:
buf = bfp.read(8192) buf = fp.read(8192)
if len(buf) == 0: break if len(buf) == 0: break
rc = self.body(buf) rc = self.body(buf)
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
self._msg = msg self._msg = msg
self._body = BytesIO() self._body = StringIO.StringIO()
self._stage = Milter.M_EOM
rc = self.eom() rc = self.eom()
self._stage = None
if self._bodyreplaced: if self._bodyreplaced:
body = self._body.getvalue() body = self._body.getvalue()
self._body = BytesIO() else:
self._body.write(header) msg.rewindbody()
self._body.write(b'\n\n') body = msg.fp.read()
self._body = StringIO.StringIO()
self._body.writelines(msg.headers)
self._body.write('\n')
self._body.write(body) self._body.write(body)
self.close()
self._close()
return rc return rc
## Feed an email contained in a file to the %milter. ## Feed an email contained in a file to the %milter.
@@ -218,7 +169,7 @@ class TestBase(object):
# @param sender MAIL FROM # @param sender MAIL FROM
# @param rcpts RCPT TO, multiple recipients may be supplied # @param rcpts RCPT TO, multiple recipients may be supplied
def feedMsg(self,fname,sender="spam@adv.com",*rcpts): def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
with open('test/'+fname,'rb') as fp: with open('test/'+fname,'r') as fp:
return self.feedFile(fp,sender,*rcpts) return self.feedFile(fp,sender,*rcpts)
## Call the connect and helo callbacks. ## Call the connect and helo callbacks.
@@ -229,19 +180,13 @@ class TestBase(object):
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'): def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
self._body = None self._body = None
self._bodyreplaced = False self._bodyreplaced = False
self._setctx(None)
opts = [ Milter.CURR_ACTS,~0,0,0 ] opts = [ Milter.CURR_ACTS,~0,0,0 ]
self._stage = -1
rc = self.negotiate(opts) rc = self.negotiate(opts)
self._stage = Milter.M_CONNECT
rc = super(TestBase,self).connect(host,1,(ip,1234)) rc = super(TestBase,self).connect(host,1,(ip,1234))
if rc != Milter.CONTINUE: if rc != Milter.CONTINUE:
self._stage = None
self.close() self.close()
return rc return rc
self._stage = Milter.M_HELO
rc = self.hello(helo) rc = self.hello(helo)
self._stage = None
if rc != Milter.CONTINUE: if rc != Milter.CONTINUE:
self.close() self.close()
return rc return rc
-312
View File
@@ -1,312 +0,0 @@
## @package Milter.testctx
# A test framework for milters that replaces milterContext rather
# than Milter.Base. Since miltermodule.c doesn't currently export
# a way to query callbacks set (and we might want to run without
# loading milter), we assume the callbacks set by Milter.runmilter().
from __future__ import print_function
from socket import AF_INET,AF_INET6
from sys import version as VERSION
import time
import mime
try:
from io import BytesIO
except:
from StringIO import StringIO as BytesIO
import Milter
from Milter import utils
## Milter context for unit testing %milter applications.
# A substitute for milter.milterContext that can be passed to
# Milter.Base._setctx().
# @since 1.0.3
class TestCtx(object):
default_opts = [Milter.CURR_ACTS,0x1fffff,0,0]
def __init__(self,logfile='test/milter.log'):
## Usually the Milter application derived from Milter.Base
self._priv = None
## List of recipients deleted
self._delrcpt = []
## List of recipients added
self._addrcpt = []
## Macros defined
self._macros = { }
## Reply codes and messages set by the %milter
self._reply = None
## The macros returned by protocol stage
self._symlist = [ None, None, None, None, None, None, None ]
## The message body.
self._body = None
## True if the %milter replaced the message body.
self._bodyreplaced = False
## True if the %milter changed any headers.
self._headerschanged = False
## The rfc822 message object for the current email being fed to the %milter.
self._msg = None
## The MAIL FROM for the current email being fed to the %milter
self._sender = None
## True if the %milter changed the envelope from.
self._envfromchanged = False
## List of recipients added
self._addrcpt = []
## Negotiated options
self._opts = TestCtx.default_opts
## Last activity
self._activity = time.time()
def getpriv(self):
return self._priv
def setpriv(self,priv):
self._priv = priv
def getsymval(self,name):
stage = self._stage
if stage >= 0:
try:
s = name.encode('utf8')
except: pass
syms = self._symlist[stage]
if syms is not None and s not in syms:
return None
return self._macros.get(name,None)
def _setsymval(self,name,val):
self._macros[name] = val
def setreply(self,rcode,xcode,*msg):
self._reply = (rcode,xcode) + msg
def setsymlist(self,stage,macros):
if self._stage != -1:
raise RuntimeError("setsymlist may only be called from negotiate")
# Records which macros are available to getsymval()
m = macros
try:
m = m.encode('utf8')
except: pass
try:
m = m.split(b' ')
except: pass
if len(m) > 5:
raise ValueError('setsymlist limited to 5 macros by MTA')
if self._symlist[stage] is not None:
raise ValueError('setsymlist already called for stage:'+stage)
if not m:
raise ValueError('setsymlist with empty list for stage:'+stage)
self._symlist[stage] = set(m)
def addheader(self,field,value,idx):
if not self._body:
raise IOError("addheader not called from eom()")
self._msg[field] = value
self._headerschanged = True
def chgheader(self,field,idx,value):
if not self._body:
raise IOError("chgheader not called from eom()")
if value == '':
del self._msg[field]
else:
self._msg[field] = value
self._headerschanged = True
def addrcpt(self,rcpt,params):
if not self._body:
raise IOError("addrcpt not called from eom()")
self._addrcpt.append((rcpt,params))
def delrcpt(self,rcpt):
if not self._body:
raise IOError("delrcpt not called from eom()")
self._delrcpt.append(rcpt)
def replacebody(self,chunk):
if self._body:
self._body.write(chunk)
self._bodyreplaced = True
else:
raise IOError("replacebody not called from eom()")
def chgfrom(self,sender,params=None):
if not self._body:
raise IOError("chgfrom not called from eom()")
self._envfromchanged = True
self._sender = sender
def quarantine(self,reason):
raise NotImplemented
## Reset activity timer.
def progress(self):
self._activity = time.time()
def _abort(self):
"What Milter sets for abort_callback"
self._priv.abort()
self._close()
def _close(self):
Milter.close_callback(self)
def _negotiate(self):
self._body = None
self._bodyreplaced = False
self._priv = None
self._opts = TestCtx.default_opts
self._stage = -1
rc = Milter.negotiate_callback(self,self._opts)
if rc == Milter.ALL_OPTS:
self._opts = TestCtx.default_opts
elif rc != Milter.CONTINUE:
self._abort()
self._close()
self._protocol = self._opts[1]
return rc
def _connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
rc = self._negotiate()
# FIXME: what if not CONTINUE or ALL_OPTS?
if self._protocol & Milter.P_NOCONNECT:
return Milter.CONTINUE
if utils.ip4re.match(ip):
af = AF_INET
elif utils.ip6re.match(ip):
af = AF_INET6
else:
raise ValueError('TestCtx.connect: invalid ip address: '+ip)
self._stage = Milter.M_CONNECT
rc = Milter.connect_callback(self,host,af,ip)
self._stage = None
if rc != Milter.CONTINUE:
self._close()
return rc
return self._helo(helo)
def _helo(self,helo):
if self._protocol & Milter.P_NOHELO:
return Milter.CONTINUE
self._stage = Milter.M_HELO
rc = self._priv.hello(helo)
self._stage = None
if rc != Milter.CONTINUE:
self._close()
return rc
def _envfrom(self,*s):
self._sender = s[0]
if self._protocol & Milter.P_NOMAIL:
return Milter.CONTINUE
self._stage = Milter.M_ENVFROM
rc = self._priv.envfrom(*s)
self._stage = None
return rc
def _envrcpt(self,s):
if self._protocol & Milter.P_NORCPT:
return Milter.CONTINUE
self._stage = Milter.M_ENVRCPT
rc = self._priv.envrcpt(s)
self._stage = None
return rc
def _data(self):
if self._protocol & Milter.P_NODATA:
return Milter.CONTINUE
self._stage = Milter.M_DATA
rc = self._priv.data()
self._stage = None
return rc
def _header(self,fld,val):
if VERSION < '3.0.0':
return self._priv.header(fld,val)
# email.message_from_binary_file uses surrogateescape to
# preserve original bytes in unicode string for decoding errors.
# convert str or Header back to original bytes
if hasattr(val, '_chunks'):
# val is a Header object for invalid header values
v = b''
for s,charset in val._chunks:
# recover the original bytes
b = s.encode(encoding='ascii',errors='surrogateescape')
v += b
else:
v = val.encode(encoding='ascii',errors='surrogateescape')
# invoke the Milter header_callback
return self._priv.header_bytes(fld,v)
def _eoh(self):
if self._protocol & Milter.P_NOEOH:
return Milter.CONTINUE
self._stage = Milter.M_EOH
rc = self._priv.eoh()
self._stage = None
return rc
def _feed_body(self,bfp):
if self._protocol & Milter.P_NOBODY:
return Milter.CONTINUE
while True:
buf = bfp.read(8192)
if len(buf) == 0: break
rc = self._priv.body(buf)
if rc != Milter.CONTINUE: return rc
return Milter.CONTINUE
def _eom(self):
self._body = BytesIO()
self._stage = Milter.M_EOM
rc = self._priv.eom()
self._stage = None
return rc
## Feed a file like object to the ctx. Calls the callbacks in
# the same sequence as libmilter.
# @param fp the file with rfc2822 message stream
# @param sender the MAIL FROM
# @param rcpt RCPT TO - additional recipients may follow
def _feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
self._body = None
self._bodyreplaced = False
self._headerschanged = False
self._reply = None
msg = mime.message_from_file(fp)
self._msg = msg
# envfrom
rc = self._envfrom('<%s>'%sender)
if rc != Milter.CONTINUE: return rc
# envrcpt
for rcpt in (rcpt,) + rcpts:
rc = self._envrcpt('<%s>'%rcpt)
if rc != Milter.CONTINUE: return rc
# data
rc = self._data()
if rc != Milter.CONTINUE: return rc
# header
for h,val in msg.items():
rc = self._header(h,val)
if rc != Milter.CONTINUE: return rc
# eoh
rc = self._eoh()
if rc != Milter.CONTINUE: return rc
# body
header,body = msg.as_bytes().split(b'\n\n',1)
rc = self._feed_body(BytesIO(body))
if rc != Milter.CONTINUE: return rc
rc = self._eom()
if self._bodyreplaced:
body = self._body.getvalue()
self._body = BytesIO()
self._body.write(header)
self._body.write(b'\n\n')
self._body.write(body)
return rc
## Feed an email contained in a file to the %milter.
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
# @param sender MAIL FROM
# @param rcpts RCPT TO, multiple recipients may be supplied
def _feedMsg(self,fname,sender="spam@adv.com",*rcpts):
with open('test/'+fname,'rb') as fp:
return self._feedFile(fp,sender,*rcpts)
+19 -46
View File
@@ -5,14 +5,12 @@
import re import re
import struct import struct
import socket import socket
import email.errors import email.Errors
from email.header import decode_header
import email.base64mime
import email.utils
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from binascii import a2b_base64 from email.Header import decode_header
#import email.Utils
import rfc822
dnsre = re.compile(r'^[a-z][-a-z\d.]+$', re.IGNORECASE)
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4) PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
ip4re = re.compile(PAT_IP4+'$') ip4re = re.compile(PAT_IP4+'$')
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$' ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
@@ -55,8 +53,8 @@ if hasattr(socket,'has_ipv6') and socket.has_ipv6:
else: else:
from pyip6 import inet_ntop, inet_pton from pyip6 import inet_ntop, inet_pton
MASK = 0xFFFFFFFF MASK = 0xFFFFFFFFL
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
def cidr(i,n,mask=MASK): def cidr(i,n,mask=MASK):
return ~(mask >> n) & mask & i return ~(mask >> n) & mask & i
@@ -69,12 +67,6 @@ def iniplist(ipaddr,iplist):
True True
>>> iniplist('192.168.0.45',['192.168.0.*']) >>> iniplist('192.168.0.45',['192.168.0.*'])
True True
>>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
True
>>> iniplist('2606:2800:220:1::',['example.com/40'])
True
>>> iniplist('4.2.2.2',['nothing.example.com'])
False
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48']) >>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
True True
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48']) >>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
@@ -83,10 +75,8 @@ def iniplist(ipaddr,iplist):
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3 ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
""" """
if ip4re.match(ipaddr): if ip4re.match(ipaddr):
fam = socket.AF_INET
ipnum = addr2bin(ipaddr) ipnum = addr2bin(ipaddr)
elif ip6re.match(ipaddr): elif ip6re.match(ipaddr):
fam = socket.AF_INET6
ipnum = bin2long6(inet_pton(ipaddr)) ipnum = bin2long6(inet_pton(ipaddr))
else: else:
raise ValueError('Invalid ip syntax:'+ipaddr) raise ValueError('Invalid ip syntax:'+ipaddr)
@@ -106,21 +96,13 @@ def iniplist(ipaddr,iplist):
n = 128 n = 128
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6): if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
return True return True
elif dnsre.match(p[0]):
try:
sfx = '/'.join(['']+p[1:])
addrlist = [r[4][0]+sfx for r in socket.getaddrinfo(p[0],25,fam)]
if iniplist(ipaddr,addrlist):
return True
except socket.gaierror: pass
elif fnmatchcase(ipaddr,pat): elif fnmatchcase(ipaddr,pat):
return True return True
return False return False
## Split email into Fullname and address. ## Split email into Fullname and address.
# This replaces <code>email.utils.parseaddr</code> but fixes # This replaces <code>email.Utils.parseaddr</code> but fixes
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>. # some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
# Additional tricky cases are still broken. Patches welcome.
# #
def parseaddr(t): def parseaddr(t):
"""Split email into Fullname and address. """Split email into Fullname and address.
@@ -134,10 +116,12 @@ def parseaddr(t):
>>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>') >>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>')
('God@heaven', 'jeff@spec.org') ('God@heaven', 'jeff@spec.org')
>>> parseaddr('Real Name ((comment)) <addr...@example.com>') >>> parseaddr('Real Name ((comment)) <addr...@example.com>')
('Real Name (comment)', 'addr...@example.com') ('Real Name', 'addr...@example.com')
>>> parseaddr('a(WRONG)@b')
('WRONG', 'a@b')
""" """
#return email.utils.parseaddr(t) #return email.Utils.parseaddr(t)
res = email.utils.parseaddr(t) res = rfc822.parseaddr(t)
# dirty fix for some broken cases # dirty fix for some broken cases
if not res[0]: if not res[0]:
pos = t.find('<') pos = t.find('<')
@@ -146,7 +130,7 @@ def parseaddr(t):
pos1 = addrspec.rfind(':') pos1 = addrspec.rfind(':')
if pos1 > 0: if pos1 > 0:
addrspec = addrspec[pos1+1:] addrspec = addrspec[pos1+1:]
return email.utils.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec)) return rfc822.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
if not res[1]: if not res[1]:
pos = t.find('<') pos = t.find('<')
if pos > 0 and t[-1] == '>': if pos > 0 and t[-1] == '>':
@@ -154,19 +138,9 @@ def parseaddr(t):
pos1 = addrspec.rfind(':') pos1 = addrspec.rfind(':')
if pos1 > 0: if pos1 > 0:
addrspec = addrspec[pos1+1:] addrspec = addrspec[pos1+1:]
return email.utils.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec)) return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
return res return res
## Fix email.base64mime.decode to add any missing padding
def decode(s, convert_eols=None):
if not s: return s
while len(s) % 4: s += '=' # add missing padding
dec = a2b_base64(s)
if convert_eols:
return dec.replace(CRLF, convert_eols)
return dec
email.base64mime.decode = decode
def parse_addr(t): def parse_addr(t):
"""Split email into user,domain. """Split email into user,domain.
@@ -211,19 +185,18 @@ def parse_header(val):
for s,enc in h: for s,enc in h:
if enc: if enc:
try: try:
u.append(s.decode(enc,'replace')) u.append(unicode(s,enc,'replace'))
except LookupError: except LookupError:
u.append(s.decode()) u.append(unicode(s))
else: else:
u.append(s.decode()) u.append(unicode(s))
u = ''.join(u) u = ''.join(u)
if type(u) is str: return u for enc in ('us-ascii','iso-8859-1','utf8'):
for enc in ('us-ascii','iso-8859-1','utf-8'):
try: try:
return u.encode(enc) return u.encode(enc)
except UnicodeError: continue except UnicodeError: continue
except UnicodeDecodeError: pass except UnicodeDecodeError: pass
except LookupError: pass except LookupError: pass
except ValueError: pass except ValueError: pass
except email.errors.HeaderParseError: pass except email.Errors.HeaderParseError: pass
return val return val
+91 -41
View File
@@ -1,4 +1,5 @@
# Abstract Abstract
--------
This is a python extension module to enable python scripts to attach to This is a python extension module to enable python scripts to attach to
Sendmail's libmilter API, enabling filtering of messages as they arrive. Sendmail's libmilter API, enabling filtering of messages as they arrive.
@@ -6,44 +7,64 @@ Since it's a script, you can do anything you want to the message - screen
out viruses, collect statistics, add or modify headers, etc. You can, at out viruses, collect statistics, add or modify headers, etc. You can, at
any point, tell Sendmail to reject, discard, or accept the message. any point, tell Sendmail to reject, discard, or accept the message.
Additional python modules provide for navigating and modifying MIME parts, and
sending DSNs or doing CBVs.
# Requirements Requirements
------------
Python milter extension: https://pypi.org/project/pymilter/ This python milter extension: http://www.bmsi.com/python/milter.html
Python: http://www.python.org Python: http://www.python.org
Sendmail: http://www.sendmail.org Sendmail: http://www.sendmail.org
or NB: From Sendmail's libmilter/README:
Postfix: http://www.postfix.org/MILTER_README.html
# Quick Installation libmilter requires pthread support in the operating system. Moreover, it
requires that the library functions it uses are thread safe; which is true
for the operating systems libmilter has been developed and tested on. On
some operating systems this requires special compile time options (e.g.,
not just -pthread). libmilter is currently known to work on (modulo
problems in the pthread support of some specific versions):
1. Build and install Sendmail, enabling libmilter (see libmilter/README). FreeBSD 3.x, 4.x
2. Build and install Python, enabling threading. SunOS 5.x (x >= 5)
3. Install this module: python setup.py --help AIX 4.3.x
4. Add these two lines to sendmail.cf[a]: HP UX 11.x
``` Linux (recent versions/distributions)
O InputMailFilters=pythonfilter OpenBSD
Xpythonfilter, S=local:/home/username/pythonsock AIX 4.1.5
```
5. Run the sample.py example milter with: python sample.py libmilter is currently not supported on:
Note that milters should almost certainly not run as root.
IRIX 6.x
Ultrix
Quick Installation
------------------
1. Build and install Sendmail, enabling libmilter (see libmilter/README).
2. Build and install Python, enabling threading.
3. Install this module: python setup.py --help
4. Add these two lines to sendmail.cf[*]:
O InputMailFilters=pythonfilter
Xpythonfilter, S=local:/home/username/pythonsock
5. Run the sample.py example milter with: python sample.py
Note that milters should almost certainly not run as root.
That's it. Incoming mail will cause the milter to print some things, and That's it. Incoming mail will cause the milter to print some things, and
some email will be rejected (see the "header" method). Edit and play. some email will be rejected (see the "header" method). Edit and play.
See spfmilter.py for a functional SPF milter, or see bms.py for an complex See spfmilter.py for a functional SPF milter, or see bms.py for an complex
milter used in production. milter used in production.
[a] This is for a quick test. Your sendmail.cf in most distros will get [*] This is for a quick test. Your sendmail.cf in most distros will get
overwritten whenever sendmail.mc is updated. To make a milter permanent, overwritten whenever sendmail.mc is updated. To make a milter permanent,
add something like: add something like:
```
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m') INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
```
to sendmail.mc instead. to sendmail.mc instead.
# Not-so-quick Installation Not-so-quick Installation
-------------------------
First install Sendmail. Make sure you read libmilter/README in the Sendmail First install Sendmail. Make sure you read libmilter/README in the Sendmail
source directory, and make sure you enable libmilter before you build. The source directory, and make sure you enable libmilter before you build. The
@@ -56,13 +77,18 @@ Install this miltermodule package; DistUtils Automatic Installation:
$ python setup.py --help $ python setup.py --help
For versions of python prior to 2.0, you will need to download distutils
separately or build manually. You will need to download unittest
separately to run the test programs. The bdist_rpm distutils option seems
not to work for python 2.0; upgrade to at least 2.1.1.
Now that everything is installed, we need to tell sendmail that we're going Now that everything is installed, we need to tell sendmail that we're going
to filter incoming email. Add lines similar to the following to to filter incoming email. Add lines similar to the following to
sendmail.cf: sendmail.cf:
```
O InputMailFilters=pythonfilter O InputMailFilters=pythonfilter
Xpythonfilter, S=local:/home/username/pythonsock Xpythonfilter, S=local:/home/username/pythonsock
```
The "O" line tells sendmail which filters to use in what order; here we're The "O" line tells sendmail which filters to use in what order; here we're
telling sendmail to use the filter named "pythonfilter". telling sendmail to use the filter named "pythonfilter".
@@ -76,24 +102,47 @@ NB: The name is specified in two places: here, in sendmail's cf file, and
in the milter itself. Make sure the two match. in the milter itself. Make sure the two match.
NB: The above lines can be added in your .mc file with this line: NB: The above lines can be added in your .mc file with this line:
```
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock') INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock')
```
For versions of sendmail prior to 8.12, you will need to enable For versions of sendmail prior to 8.12, you will need to enable
`_FFR_MILTER` for the cf macros. For example, _FFR_MILTER for the cf macros. For example,
```
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
```
# IPv6 Notes
RedHat 6.2 Notes
----------------
The Redhat 6.2 sendmail RPM does not enable milter. You can obtain a
modified spec file at
http://www.bmsi.com/linux/rh62/sendmail-rhmilter.spec
use it to rebuild the Redhat 7.2 SRPM. The RH6.2 SRPM does not have
recent sendmail security patches.
RedHat 7.2 Notes
----------------
The Redhat 7.2 sendmail RPM enables milter in sendmail - but does not include
the headers needed for compiling a milter. You can obtain a modified spec
file with a sendmail-devel package that includes the needed static libraries
and headers at
http://www.bmsi.com/linux/sendmail-rh72.spec
IPv6 Notes
----------
The IPv6 protocol is supported if your operation system supports it The IPv6 protocol is supported if your operation system supports it
and if sendmail was compiled with IPv6 support. To determine if your and if sendmail was compiled with IPv6 support. To determine if your
sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6 sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6
compilation option. To compile sendmail with IPv6 support, add this compilation option. To compile sendmail with IPv6 support, add this
declaration to your site.config.m4 before building it: declaration to your site.config.m4 before building it:
```
APPENDDEF(`confENVDEF', `-DNETINET6=1') APPENDDEF(`confENVDEF', `-DNETINET6=1')
```
IPv6 support can show up in two places; the communications socket IPv6 support can show up in two places; the communications socket
between the milter and sendmail processes and in the host address between the milter and sendmail processes and in the host address
argument to the connect() callback method. argument to the connect() callback method.
@@ -104,26 +153,26 @@ want to allow both IPv4 and IPv6 connections, some operating systems
will require that each listens to different port numbers. For an will require that each listens to different port numbers. For an
IPv6-only setup, your sendmail configuration should contain a line IPv6-only setup, your sendmail configuration should contain a line
similar to (first line is for sendmail.mc, second is sendmail.cf): similar to (first line is for sendmail.mc, second is sendmail.cf):
```
DAEMON_OPTIONS(`Name=MTA-v6, Family=inet6, Modify=C, Port=25') DAEMON_OPTIONS(`Name=MTA-v6, Family=inet6, Modify=C, Port=25')
O DaemonPortOptions=Name=MTA-v6, Family=inet6, Modify=C, Port=25 O DaemonPortOptions=Name=MTA-v6, Family=inet6, Modify=C, Port=25
```
To allow sendmail and the milter process to communicate with each To allow sendmail and the milter process to communicate with each
other over IPv6, you may use the "inet6" socket name prefix, as in: other over IPv6, you may use the "inet6" socket name prefix, as in:
```
Xpythonfilter, S=inet6:1234@fec0:0:0:7::5c Xpythonfilter, S=inet6:1234@fec0:0:0:7::5c
```
The connect() callback method in the milter class will pass the The connect() callback method in the milter class will pass the
IPv6-specific information in the 'hostaddr' argument as a tuple. Note IPv6-specific information in the 'hostaddr' argument as a tuple. Note
that the type of this value is dependent upon the protocol family, and that the type of this value is dependent upon the protocol family, and
is not compatible with IPv4 connections. Therefore you should always is not compatible with IPv4 connections. Therefore you should always
check the family argument before attempting to use the hostaddr check the family argument before attempting to use the hostaddr
argument. A quick example showing this follows: argument. A quick example showing this follows:
```
import socket import socket
...
class ipv6awareMilter(Milter.Milter): class ipv6awareMilter(Milter.Milter):
...
def connect(self,hostname,family,hostaddr): def connect(self,hostname,family,hostaddr):
if family==socket.AF_INET: if family==socket.AF_INET:
ipaddress, port = hostaddr ipaddress, port = hostaddr
@@ -131,7 +180,7 @@ argument. A quick example showing this follows:
ip6address, port, flowinfo, scopeid = hostaddr ip6address, port, flowinfo, scopeid = hostaddr
elif family==socket.AF_UNIX: elif family==socket.AF_UNIX:
socketpath = hostaddr socketpath = hostaddr
```
The hostname argument is always safe to use without interpreting the The hostname argument is always safe to use without interpreting the
protocol family. For IPv6 connections for which the hostname can not protocol family. For IPv6 connections for which the hostname can not
be determined the hostname will appear similar to the string be determined the hostname will appear similar to the string
@@ -139,7 +188,8 @@ be determined the hostname will appear similar to the string
RFC 2553 for information on interpreting and using the flowinfo and RFC 2553 for information on interpreting and using the flowinfo and
scopeid socket attributes, both of which are integers. scopeid socket attributes, both of which are integers.
# Authors Authors
-------
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
-2
View File
@@ -1,4 +1,2 @@
Test case for Milter/dsn.py
Lookup exact RFC syntax of real name / email and make Lookup exact RFC syntax of real name / email and make
Milter.utils.parse_addr() pass all unit tests. Milter.utils.parse_addr() pass all unit tests.
+3 -45
View File
@@ -1,7 +1,7 @@
## @mainpage Writing Milters in Python ## @mainpage Writing Milters in Python
# #
# At the lowest level, the <code>milter</code> module provides a thin wrapper # At the lowest level, the <code>milter</code> module provides a thin wrapper
# around the <a href="milter_api/index.html"> sendmail # 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 # libmilter API</a>. This API lets you register callbacks for a number of
# events in the process of sendmail receiving a message via SMTP. These # events in the process of sendmail receiving a message via SMTP. These
# events include the initial connection from a MTA, the envelope sender and # events include the initial connection from a MTA, the envelope sender and
@@ -39,10 +39,10 @@
# @section threading # @section threading
# #
# The libmilter library which pymilter wraps # The libmilter library which pymilter wraps
# <a href="milter_overview#SignalHandling">handles # <a href="https://www.milter.org/developers/overview#SignalHandling">handles
# all signals</a> itself, and expects to be called from a single main thread. # all signals</a> itself, and expects to be called from a single main thread.
# It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to # It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to
# <a href="milter_api/smfi_stop.html">smfi_stop</a> # <a href="https://www.milter.org/developers/api/smfi_stop">smfi_stop</a>
# and the last to an internal ABORT. # and the last to an internal ABORT.
# #
# If you use python threads or threading modules, then signal handling gets # If you use python threads or threading modules, then signal handling gets
@@ -54,45 +54,3 @@
# multiprocessing</a> module useful. It can be a drop-in # multiprocessing</a> module useful. It can be a drop-in
# replacement for threading as illustrated in # replacement for threading as illustrated in
# <a href="milter-template_8py-example.html">milter-template.py</a>. # <a href="milter-template_8py-example.html">milter-template.py</a>.
#
# @section Useful python packages for milters
#
# <a href="https://github.com/sdgathman/pymilter">pymilter</a> - this package.
#
# <a href="https://github.com/sdgathman/pyspf">pyspf</a> checks the
# SMTP envelope sender (MAIL FROM, passed to the Milter.Base.envfrom callback)
# against a Sender Policy published in DNS by the sending domain. This
# can prevent forgery of the MAIL FROM. SPF is Sender Policy Framework.
#
# <a href="https://launchpad.net/dkimpy">pydkim</a> checks a DKIM signature
# of the email body and headers against a public key published in DNS by
# the signing domain. DKIM is DomainKeys Identified Mail.
#
# The <a href="https://pypi.python.org/pypi/authres/">authres</a> module
# parses and formats the Authentication-Results email header, providing
# a standard place to summarize the results from DKIM, SPF, rDNS, SMTP AUTH,
# and other email authentication methods.
#
# <a href="https://github.com/sdgathman/pydspam/">pydspam</a> wraps
# the libdspam API of the <a href="http://dspam.sourceforge.net/">DSPAM</a>
# project.
#
# <a href="https://github.com/sdgathman/pysrs/">pysrs</a> rewrites
# MAIL FROM to include a timestamped signature so that "bounce spam"
# can be immediately rejected.
#
# <a href="https://github.com/sdgathman/pygossip/">pygossip</a> is a
# system to track reputation by domain and authentication level and type,
# and a simple protocol to gossip about reputations with other mail servers.
#
# @section Milters written with pymilter
#
# <a href="https://github.com/croessner/vrfydmn">Verify Domain</a> is a
# Postfix milter that rejects/fixes manipulated From: header
# on a mail host with multiple virtual domains.
#
# <a href="https://github.com/sdgathman/milter/">BMS Milter</a> has several
# milters, a big complicated spam filter that integrates multiple
# authentication protocols with pydspam, and two simple ones: spfmilter.py and
# dkim-milter.py.
#
+20 -20
View File
@@ -75,44 +75,44 @@ NOREPLY = 6
# and converts function callbacks to instance method invocations. # and converts function callbacks to instance method invocations.
# #
class milterContext(object): class milterContext(object):
## Calls <a href="milter_api/smfi_getsymval.html">smfi_getsymval</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_getsymval">smfi_getsymval</a>.
def getsymval(self,sym): pass def getsymval(self,sym): pass
## Calls <a href="milter_api/smfi_setreply.html"> ## Calls <a href="https://www.milter.org/developers/api/smfi_setreply">
# smfi_setreply</a> or # smfi_setreply</a> or
# <a href="milter_api/smfi_setmlreply.html"> # <a href="https://www.milter.org/developers/api/smfi_setmlreply">
# smfi_setmlreply</a>. # smfi_setmlreply</a>.
# @param rcode SMTP response code # @param rcode SMTP response code
# @param xcode extended SMTP response code # @param xcode extended SMTP response code
# @param msg one or more message lines. If the MTA does not support # @param msg one or more message lines. If the MTA does not support
# multiline messages, only the first is used. # multiline messages, only the first is used.
def setreply(self,rcode,xcode,*msg): pass def setreply(self,rcode,xcode,*msg): pass
## Calls <a href="milter_api/smfi_addheader.html">smfi_addheader</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_addheader">smfi_addheader</a>.
def addheader(self,name,value,idx=-1): pass def addheader(self,name,value,idx=-1): pass
## Calls <a href="milter_api/smfi_chgheader.html">smfi_chgheader</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">smfi_chgheader</a>.
def chgheader(self,name,idx,value): pass def chgheader(self,name,idx,value): pass
## Calls <a href="milter_api/smfi_addrcpt.html">smfi_addrcpt</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">smfi_addrcpt</a>.
def addrcpt(self,rcpt,params=None): pass def addrcpt(self,rcpt,params=None): pass
## Calls <a href="milter_api/smfi_delrcpt.html">smfi_delrcpt</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">smfi_delrcpt</a>.
def delrcpt(self,rcpt): pass def delrcpt(self,rcpt): pass
## Calls <a href="milter_api/smfi_replacebody.html">smfi_replacebody</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">smfi_replacebody</a>.
def replacebody(self,data): pass def replacebody(self,data): pass
## Attach a Python object to this connection context. ## Attach a Python object to this connection context.
# @return the old value or None # @return the old value or None
def setpriv(self,priv): pass def setpriv(self,priv): pass
## Return the Python object attached to this connection context. ## Return the Python object attached to this connection context.
def getpriv(self): pass def getpriv(self): pass
## Calls <a href="milter_api/smfi_quarantine.html">smfi_quarantine</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">smfi_quarantine</a>.
def quarantine(self,reason): pass def quarantine(self,reason): pass
## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_progress">smfi_progress</a>.
def progress(self): pass def progress(self): pass
## Calls <a href="milter_api/smfi_chgfrom.html">smfi_chgfrom</a>. ## Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">smfi_chgfrom</a>.
def chgfrom(self,sender,param=None): pass def chgfrom(self,sender,param=None): pass
## Tell the MTA which macro values we are interested in for a given stage. ## Tell the MTA which macro values we are interested in for a given stage.
# Of interest only when you need to squeeze a few more bytes of bandwidth. # Of interest only when you need to squeeze a few more bytes of bandwidth.
# It may only be called from the negotiate callback. # It may only be called from the negotiate callback.
# The protocol stages are # The protocol stages are
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH. # M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
# Calls <a href="milter_api/smfi_setsymlist.html">smfi_setsymlist</a>. # Calls <a href="https://www.milter.org/developers/api/smfi_setsymlist">smfi_setsymlist</a>.
# @param stage protocol stage in which the macro list should be used # @param stage protocol stage in which the macro list should be used
# @param macrolist a space separated list of macro names # @param macrolist a space separated list of macro names
def setsymlist(self,stage,macrolist): pass def setsymlist(self,stage,macrolist): pass
@@ -160,15 +160,15 @@ def set_exception_policy(code): pass
# member functions are actually overridden by an application class. # member functions are actually overridden by an application class.
# @param name the %milter name by which the MTA finds us # @param name the %milter name by which the MTA finds us
# @param negotiate the # @param negotiate the
# <a href="milter_api/xxfi_negotiate.html"> # <a href="https://www.milter.org/developers/api/xxfi_negotiate">
# xxfi_negotiate</a> callback, called to negotiate supported # xxfi_negotiate</a> callback, called to negotiate supported
# actions, callbacks, and protocol steps. # actions, callbacks, and protocol steps.
# @param unknown the # @param unknown the
# <a href="milter_api/xxfi_unknown.html"> # <a href="https://www.milter.org/developers/api/xxfi_unknown">
# xxfi_unknown</a> callback, called when for SMTP commands # xxfi_unknown</a> callback, called when for SMTP commands
# not recognized by the MTA. (Extend SMTP in your milter!) # not recognized by the MTA. (Extend SMTP in your milter!)
# @param data the # @param data the
# <a href="milter_api/xxfi_data.html"> # <a href="https://www.milter.org/developers/api/xxfi_data">
# xxfi_data</a> callback, called when the DATA # xxfi_data</a> callback, called when the DATA
# SMTP command is received. # SMTP command is received.
def register(name,negotiate=None,unknown=None,data=None): pass def register(name,negotiate=None,unknown=None,data=None): pass
@@ -178,19 +178,19 @@ def register(name,negotiate=None,unknown=None,data=None): pass
# call to milter.setconn() which will be the interface between MTAs and the # call to milter.setconn() which will be the interface between MTAs and the
# %milter. This allows the calling application to ensure that the socket can be # %milter. This allows the calling application to ensure that the socket can be
# created. If this is not called, milter.main() will do so implicitly. # created. If this is not called, milter.main() will do so implicitly.
# Calls <a href="milter_api/smfi_opensocket.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_opensocket">
# smfi_opensocket</a>. While not documented for libmilter, my experiments # smfi_opensocket</a>. While not documented for libmilter, my experiments
# indicate that you must call register() before calling opensocket(). # indicate that you must call register() before calling opensocket().
# @param rmsock Try to remove an existing unix domain socket if true. # @param rmsock Try to remove an existing unix domain socket if true.
def opensocket(rmsock): pass def opensocket(rmsock): pass
## Transfer control to libmilter. ## Transfer control to libmilter.
# Calls <a href="milter_api/smfi_main.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_main">
# smfi_main</a>. # smfi_main</a>.
def main(): pass def main(): pass
## Set the libmilter debugging level. ## Set the libmilter debugging level.
# <a href="milter_api/smfi_setdbg.html">smfi_setdbg</a> # <a href="https://www.milter.org/developers/api/smfi_setdbg">smfi_setdbg</a>
# sets the %milter library's internal debugging level to a new level # sets the %milter library's internal debugging level to a new level
# so that code details may be traced. A level of zero turns off debugging. The # so that code details may be traced. A level of zero turns off debugging. The
# greater (more positive) the level the more detailed the debugging. Six is the # greater (more positive) the level the more detailed the debugging. Six is the
@@ -198,12 +198,12 @@ def main(): pass
def setdbg(lev): pass def setdbg(lev): pass
## Set timeout for MTA communication. ## Set timeout for MTA communication.
# Calls <a href="milter_api/smfi_settimeout.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_settimeout">
# smfi_settimeout</a>. Must be called before calling main(). # smfi_settimeout</a>. Must be called before calling main().
def settimeout(secs): pass def settimeout(secs): pass
## Set socket backlog. ## Set socket backlog.
# Calls <a href="milter_api/smfi_setbacklog.html"> # Calls <a href="https://www.milter.org/developers/api/smfi_setbacklog">
# smfi_setbacklog</a>. Must be called before calling main(). # smfi_setbacklog</a>. Must be called before calling main().
def setbacklog(n): pass def setbacklog(n): pass
+7 -9
View File
@@ -1,18 +1,16 @@
web: web:
doxygen doxygen
test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-milter-devel doc/html/milter_api
rsync -ravKk doc/html/ pymilter.org:/var/www/html/milter/pymilter
cd doc/html; zip -r ../../doc . cd doc/html; zip -r ../../doc .
rsync -ravK doc/html/ spidey2.bmsi.com:/Public/pymilter
VERSION=1.0.5 VERSION=1.0
CVSTAG=pymilter-1_0
PKG=pymilter-$(VERSION) PKG=pymilter-$(VERSION)
SRCTAR=$(PKG).tar.gz SRCTAR=$(PKG).tar.gz
$(SRCTAR): $(SRCTAR):
git archive --format=tar.gz --prefix=$(PKG)/ -o $(SRCTAR) $(PKG) cvs export -r$(CVSTAG) -d $(PKG) pymilter
tar cvfz $(PKG).tar.gz $(PKG)
rm -r $(PKG)
# add extra copy of name like github so annoyingly does... cvstar: $(SRCTAR)
github:
git archive --format=tar.gz --prefix=pymilter-$(PKG)/ -o $(SRCTAR) $(PKG)
gittar: $(SRCTAR)
+4 -5
View File
@@ -1,8 +1,7 @@
## A very simple sample milter to prevent mixing of internal and external mail. ## A very simple milter to prevent mixing of internal and external mail.
# Internal is defined as using one of a list of internal top level domains. # Internal is defined as using one of a list of internal top level domains.
# This code is open-source on the same terms as Python. # This code is open-source on the same terms as Python.
from __future__ import print_function
import Milter import Milter
import time import time
import sys import sys
@@ -14,7 +13,7 @@ internal_tlds = ["corp", "personal"]
# True if internal, False otherwise # True if internal, False otherwise
def is_internal(hostname): def is_internal(hostname):
components = hostname.split(".") components = hostname.split(".")
return components.pop() in internal_tlds return components.pop() in internal_tlds:
# Determine if internal and external hosts are mixed based on a list # Determine if internal and external hosts are mixed based on a list
# of hostnames # of hostnames
@@ -69,12 +68,12 @@ def main():
timeout = 600 timeout = 600
# Register to have the Milter factory create instances of your class: # Register to have the Milter factory create instances of your class:
Milter.factory = NoMixMilter Milter.factory = NoMixMilter
print("%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')) print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter("nomixfilter",socketname,timeout) Milter.runmilter("nomixfilter",socketname,timeout)
logq.put(None) logq.put(None)
bt.join() bt.join()
print("%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')) print "%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+18 -52
View File
@@ -1,36 +1,26 @@
## To roll your own milter, create a class that extends Milter. ## To roll your own milter, create a class that extends Milter.
# This is a useless example to show basic features of Milter. # See the pymilter project at http://bmsi.com/python/milter.html
# See the pymilter project at https://pymilter.org based # based on Sendmail's milter API http://www.milter.org/milter_api/api.html
# on Sendmail's milter API
# This code is open-source on the same terms as Python. # This code is open-source on the same terms as Python.
## Milter calls methods of your class at milter events. ## Milter calls methods of your class at milter events.
## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message. ## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
## You can also add/del recipients, replacebody, add/del headers, etc. ## You can also add/del recipients, replacebody, add/del headers, etc.
from __future__ import print_function
import Milter import Milter
try: import StringIO
from StringIO import StringIO as BytesIO
except:
from io import BytesIO
import time import time
import email import email
from email import message_from_binary_file
from email import policy
import mimetypes
import os
import sys import sys
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
from Milter.utils import parse_addr from Milter.utils import parse_addr
if True: if True:
# for logging process - usually not needed
from multiprocessing import Process as Thread, Queue from multiprocessing import Process as Thread, Queue
else: else:
from threading import Thread from threading import Thread
from Queue import Queue from Queue import Queue
logq = None logq = Queue(maxsize=4)
class myMilter(Milter.Base): class myMilter(Milter.Base):
@@ -84,10 +74,9 @@ class myMilter(Milter.Base):
# NOTE: self.fp is only an *internal* copy of message data. You # NOTE: self.fp is only an *internal* copy of message data. You
# must use addheader, chgheader, replacebody to change the message # must use addheader, chgheader, replacebody to change the message
# on the MTA. # on the MTA.
self.fp = BytesIO() self.fp = StringIO.StringIO()
self.canon_from = '@'.join(parse_addr(mailfrom)) self.canon_from = '@'.join(parse_addr(mailfrom))
self.fp.write(b'From %s %s\n' % (self.canon_from.encode(), self.fp.write('From %s %s\n' % (self.canon_from,time.ctime()))
time.ctime().encode()))
return Milter.CONTINUE return Milter.CONTINUE
@@ -102,12 +91,12 @@ class myMilter(Milter.Base):
@Milter.noreply @Milter.noreply
def header(self, name, hval): def header(self, name, hval):
self.fp.write(b'%s: %s\n' % (name.encode(),hval.encode())) # add header to buffer self.fp.write("%s: %s\n" % (name,hval)) # add header to buffer
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @Milter.noreply
def eoh(self): def eoh(self):
self.fp.write(b'\n') # terminate headers self.fp.write("\n") # terminate headers
return Milter.CONTINUE return Milter.CONTINUE
@Milter.noreply @Milter.noreply
@@ -117,16 +106,7 @@ class myMilter(Milter.Base):
def eom(self): def eom(self):
self.fp.seek(0) self.fp.seek(0)
msg = email.message_from_binary_file(self.fp, policy=policy.default) msg = email.message_from_file(self.fp)
#example on how to iterate through attachments
for attachment in msg.iter_attachments():
#attachment holds the attachment object so that it can be used with a new MIMEMultipart() message
self.log("Attachment filename is %s" % (attachment.get_filename(),))
self.log("Attachment content/type is %s" % (attachment.get_content_type(),))
data = attachment.get_content()
self.log("Attachment content is %s" % (data,))
# many milter functions can only be called from eom() # many milter functions can only be called from eom()
# example of adding a Bcc: # example of adding a Bcc:
self.addrcpt('<%s>' % 'spy@example.com') self.addrcpt('<%s>' % 'spy@example.com')
@@ -144,35 +124,24 @@ class myMilter(Milter.Base):
## === Support Functions === ## === Support Functions ===
def log(self,*msg): def log(self,*msg):
t = (msg,self.id,time.time()) logq.put((msg,self.id,time.time()))
if logq:
logq.put(t)
else:
# logmsg(*t)
pass
def logmsg(msg,id,ts):
print("%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
end=None)
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print(i,end=None)
print()
sys.stdout.flush()
def background(): def background():
while True: while True:
t = logq.get() t = logq.get()
if not t: break if not t: break
logmsg(*t) msg,id,ts = t
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
for i in msg: print i,
print
## === ## ===
def main(): def main():
bt = Thread(target=background) bt = Thread(target=background)
bt.start() bt.start()
# This is NOT a good socket location for production, it is for socketname = "/home/stuart/pythonsock"
# playing around. I suggest /var/run/milter/myappnamesock for production.
socketname = os.path.expanduser('~/pythonsock')
timeout = 600 timeout = 600
# Register to have the Milter factory create instances of your class: # Register to have the Milter factory create instances of your class:
Milter.factory = myMilter Milter.factory = myMilter
@@ -180,15 +149,12 @@ def main():
flags += Milter.ADDRCPT flags += Milter.ADDRCPT
flags += Milter.DELRCPT flags += Milter.DELRCPT
Milter.set_flags(flags) # tell Sendmail which features we use Milter.set_flags(flags) # tell Sendmail which features we use
print("%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')) print "%s milter startup" % time.strftime('%Y%b%d %H:%M:%S')
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,timeout) Milter.runmilter("pythonfilter",socketname,timeout)
logq.put(None) logq.put(None)
bt.join() bt.join()
print("%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')) print "%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
if __name__ == "__main__": if __name__ == "__main__":
# You probably do not need a logging process, but if you do, this
# is one way to do it.
logq = Queue(maxsize=4)
main() main()
+233 -109
View File
@@ -1,6 +1,6 @@
/* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org) /* Copyright (C) 2001 James Niemira (niemira@colltech.com, urmane@urmane.org)
* Portions Copyright (C) 2001,2002,2003,2004,2005,2006,2007 * Portions Copyright (C) 2001,2002,2003,2004,2005,2006,2007
* Stuart Gathman (stuart@gathman.org) * Stuart Gathman (stuart@bmsi.com)
* *
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the * under the terms of the GNU General Public License as published by the
@@ -34,6 +34,217 @@ $ python setup.py help
libraries=["milter","smutil","resolv"] libraries=["milter","smutil","resolv"]
* $Log$
* Revision 1.35 2013/03/14 22:11:25 customdesigned
* Release 0.9.8
*
* Revision 1.34 2013/03/09 05:42:14 customdesigned
* Make TestBase members private, fix getsymlist misspelling.
*
* Revision 1.33 2013/03/09 00:25:23 customdesigned
* Better untrapped exception message. const char for doc comments.
*
* Revision 1.32 2013/01/13 01:46:16 customdesigned
* Doc updates.
*
* Revision 1.31 2012/04/12 23:32:50 customdesigned
* Replace redundant callback array with macros. If this doesn't break anything,
* macros can be eliminated with code changes.
*
* Revision 1.30 2012/04/12 23:08:06 customdesigned
* Support RFC2553 on BSD
*
* Revision 1.29 2011/06/09 15:45:27 customdesigned
* Print callback name for non-int return error.
*
* Revision 1.28 2011/06/08 23:13:48 customdesigned
* Generate special exception when callback return not int.
*
* Revision 1.27 2009/07/28 21:45:54 customdesigned
* Add getversion() to return runtime version.
*
* Revision 1.26 2009/07/28 21:08:20 customdesigned
* Increment del count.
*
* Revision 1.25 2009/07/28 20:58:55 customdesigned
* getdiag method
*
* Revision 1.24 2009/06/09 01:54:44 customdesigned
* Forgot to initialize optional parameter.
*
* Revision 1.23 2009/05/29 20:44:58 customdesigned
* Typo SMFIP_NO constants.
*
* Revision 1.22 2009/05/29 19:53:36 customdesigned
* 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
* Doc updates.
*
* Revision 1.13 2008/11/23 03:06:47 customdesigned
* Milter support for chgfrom.
*
* Revision 1.12 2008/11/21 20:42:52 customdesigned
* Support smfi_chgfrom and smfi_addrcpt_par.
*
* Revision 1.11 2007/09/25 02:26:29 customdesigned
* Update license.
*
* Revision 1.10 2006/02/12 02:00:42 customdesigned
* Resolve FIXME for wrap_close.
*
* Revision 1.9 2005/12/23 21:46:36 customdesigned
* Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER)
*
* Revision 1.8 2005/10/20 23:23:36 customdesigned
* Include smfi_progress is SMFIR_PROGRESS defined
*
* Revision 1.7 2005/10/20 23:04:46 customdesigned
* Add optional idx for position of added header.
*
* Revision 1.6 2005/07/15 22:18:17 customdesigned
* Support callback exception policy
*
* Revision 1.5 2005/06/24 04:20:07 customdesigned
* Report context allocation error.
*
* Revision 1.4 2005/06/24 04:12:43 customdesigned
* Remove unused name argument to generic wrappers.
*
* Revision 1.3 2005/06/24 03:57:35 customdesigned
* Handle close called before connect.
*
* Revision 1.2 2005/06/02 04:18:55 customdesigned
* Update copyright notices after reading article on /.
*
* Revision 1.1.1.2 2005/05/31 18:09:06 customdesigned
* Release 0.7.1
*
* Revision 2.31 2004/08/23 02:24:36 stuart
* Support setbacklog
*
* Revision 2.30 2004/08/21 20:29:53 stuart
* Support option of 11 lines max for mlreply.
*
* Revision 2.29 2004/08/21 04:14:29 stuart
* mlreply support
*
* Revision 2.28 2004/08/21 02:45:21 stuart
* Don't leak int constants if module unloaded.
*
* Revision 2.27 2004/04/06 03:19:59 stuart
* Release 0.6.8
*
* Revision 2.26 2004/03/04 21:43:06 stuart
* Fix memory leak by removing unused dynamic template buffer,
* thanks again to Alexander Kourakos.
*
* Revision 2.25 2004/03/01 19:45:03 stuart
* Release 0.6.5
*
* Revision 2.24 2004/03/01 18:56:50 stuart
* Support progress reporting.
*
* Revision 2.23 2004/03/01 18:36:09 stuart
* Plug memory leak. Thanks to Alexander Kourakos.
*
* Revision 2.22 2003/11/02 03:01:46 stuart
* Adjust SMTP error codes after careful reading of standard.
*
* Revision 2.21 2003/06/24 19:57:04 stuart
* Allow removing a python milter callback by setting to None.
*
* Revision 2.20 2003/02/13 17:08:57 stuart
* IPV6 support
*
* Revision 2.19 2003/02/13 16:58:29 stuart
* Support passing None to setreply and chgheader.
*
* Revision 2.18 2002/12/11 16:44:06 stuart
* Support QUARANTINE if supported by libmilter.
*
* Revision 2.17 2002/04/18 20:20:35 stuart
* Fix for NULL hostaddr in connect callback from Jason Erickson.
*
* Revision 2.16 2001/09/26 13:29:09 stuart
* sa_len not supported by linux.
*
* Revision 2.15 2001/09/25 17:28:40 stuart
* Copyrights, documentation, release 0.3.1
*
* Revision 2.14 2001/09/25 00:36:57 stuart
* Pass hostaddr to python code in format used by standard socket module.
*
* Revision 2.13 2001/09/24 23:44:55 stuart
* Return old callback from setcallback functions.
*
* Revision 2.12 2001/09/24 20:02:30 stuart
* Remove redundant setpriv
*
* Revision 2.11 2001/09/23 22:26:35 stuart
* Update docs. Streamline Milter.py
* update testbms.py to reflect actual sendmail behaviour with multiple
* messages per connection.
*
* Revision 2.10 2001/09/22 15:33:42 stuart
* More doc comment updates.
*
* Revision 2.9 2001/09/22 14:52:27 stuart
* Actually return retval in _generic_return.
* Go over doc comments.
*
* Revision 2.8 2001/09/22 01:59:32 stuart
* Prevent reentrant call of milter_main, which libmilter doesn't support.
*
* Revision 2.7 2001/09/22 01:47:37 stuart
* Forgot to set milter interp.
*
* Revision 2.6 2001/09/22 01:23:53 stuart
* Added proper threading after research in python docs.
*
* Revision 2.5 2001/09/21 20:08:51 stuart
* Release 0.2.3
*
* Revision 2.4 2001/09/20 16:18:16 stuart
* libmilter checks in_eom state, so we don't have to.
*
* Revision 2.3 2001/09/19 06:02:33 stuart
* Make more stuff static.
*
* Revision 2.1 2001/09/19 04:24:13 stuart
* Use extension type to track context in python.
*
* Revision 1.4 2001/09/18 18:48:28 stuart
* clear private data reference in _clear_context
*
* Revision 1.3 2001/09/15 04:19:37 stuart
* nasty off by 1 mem overwrite bugs in wrap_env
* generic_set_callback
*
* Revision 1.2 2001/09/15 03:15:39 stuart
* several bugs fixed, works smoothly
*
*/ */
#ifndef MAX_ML_REPLY #ifndef MAX_ML_REPLY
@@ -43,7 +254,6 @@ $ python setup.py help
#error MAX_ML_REPLY must be 1 or 11 or 32 #error MAX_ML_REPLY must be 1 or 11 or 32
#endif #endif
#define _FFR_MULTILINE (MAX_ML_REPLY > 1) #define _FFR_MULTILINE (MAX_ML_REPLY > 1)
#define PY_SSIZE_T_CLEAN
//#include <pthread.h> // shouldn't be needed - use Python API //#include <pthread.h> // shouldn't be needed - use Python API
#include <Python.h> // Python C API #include <Python.h> // Python C API
@@ -72,7 +282,7 @@ $ python setup.py help
* published. Unfortunately I know of no good way to do this * published. Unfortunately I know of no good way to do this
* other than with OS-specific tests. * other than with OS-specific tests.
*/ */
#if defined(__FreeBSD__) || defined(__linux__) || defined(__sun__) || defined(__GLIBC__) || (defined(__APPLE__) && defined(__MACH__)) #if defined(__FreeBSD_kernel__) || defined(__linux__)
#define HAVE_IPV6_RFC2553 #define HAVE_IPV6_RFC2553
#include <arpa/inet.h> #include <arpa/inet.h>
#endif #endif
@@ -133,13 +343,7 @@ static struct MilterCallback {
{ NULL , NULL } { NULL , NULL }
}; };
#if PY_MAJOR_VERSION >= 3 staticforward struct smfiDesc description; /* forward declaration */
static struct smfiDesc description; /* forward declaration */
static PyTypeObject milter_ContextType;
#else
staticforward struct smfiDesc description; /* forward declaration */
staticforward PyTypeObject milter_ContextType;
#endif
static PyObject *MilterError; static PyObject *MilterError;
/* The interpreter instance that called milter.main */ /* The interpreter instance that called milter.main */
@@ -151,6 +355,8 @@ typedef struct {
static milter_Diag diag; static milter_Diag diag;
staticforward PyTypeObject milter_ContextType;
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
SMFICTX *ctx; /* libmilter thread state */ SMFICTX *ctx; /* libmilter thread state */
@@ -251,7 +457,7 @@ static const char milter_set_flags__doc__[] =
Set flags for filter capabilities; OR of one or more of:\n\ Set flags for filter capabilities; OR of one or more of:\n\
ADDHDRS - filter may add headers\n\ ADDHDRS - filter may add headers\n\
CHGBODY - filter may replace body\n\ CHGBODY - filter may replace body\n\
CHGFROM - filter may replace sender\n\ CHGFROM - filter may replace body\n\
ADDRCPT - filter may add recipients\n\ ADDRCPT - filter may add recipients\n\
DELRCPT - filter may delete recipients\n\ DELRCPT - filter may delete recipients\n\
CHGHDRS - filter may change/delete headers"; CHGHDRS - filter may change/delete headers";
@@ -380,7 +586,7 @@ Sets the Python function invoked for each body chunk. There may\n\
be multiple body chunks passed to the filter. End-of-lines are\n\ be multiple body chunks passed to the filter. End-of-lines are\n\
represented as received from SMTP (normally Carriage-Return/Line-Feed).\n\ represented as received from SMTP (normally Carriage-Return/Line-Feed).\n\
Function takes args (ctx, chunk) -> int\n\ Function takes args (ctx, chunk) -> int\n\
chunk -> bytes - body data"; chunk -> String - body data";
static PyObject * static PyObject *
milter_set_body_callback(PyObject *self, PyObject *args) { milter_set_body_callback(PyObject *self, PyObject *args) {
@@ -491,14 +697,10 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
int retval; int retval;
if (arglist == NULL) return _report_exception(self); if (arglist == NULL) return _report_exception(self);
result = PyObject_CallObject(cb, arglist); result = PyEval_CallObject(cb, arglist);
Py_DECREF(arglist); Py_DECREF(arglist);
if (result == NULL) return _report_exception(self); if (result == NULL) return _report_exception(self);
#if PY_MAJOR_VERSION >= 3
if (!PyLong_Check(result)) {
#else
if (!PyInt_Check(result)) { if (!PyInt_Check(result)) {
#endif
const struct MilterCallback *p; const struct MilterCallback *p;
const char *cbname = "milter"; const char *cbname = "milter";
char buf[40]; char buf[40];
@@ -513,11 +715,7 @@ _generic_wrapper(milter_ContextObject *self, PyObject *cb, PyObject *arglist) {
PyErr_SetString(MilterError,buf); PyErr_SetString(MilterError,buf);
return _report_exception(self); return _report_exception(self);
} }
#if PY_MAJOR_VERSION >= 3
retval = PyLong_AS_LONG(result);
#else
retval = PyInt_AS_LONG(result); retval = PyInt_AS_LONG(result);
#endif
Py_DECREF(result); Py_DECREF(result);
_release_thread(self->t); _release_thread(self->t);
return retval; return retval;
@@ -534,11 +732,7 @@ makeipaddr(struct sockaddr_in *addr) {
sprintf(buf, "%d.%d.%d.%d", sprintf(buf, "%d.%d.%d.%d",
(int) (x>>24) & 0xff, (int) (x>>16) & 0xff, (int) (x>>24) & 0xff, (int) (x>>16) & 0xff,
(int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff); (int) (x>> 8) & 0xff, (int) (x>> 0) & 0xff);
#if PY_MAJOR_VERSION >= 3
return PyUnicode_FromString(buf);
#else
return PyString_FromString(buf); return PyString_FromString(buf);
#endif
} }
#ifdef HAVE_IPV6_SUPPORT #ifdef HAVE_IPV6_SUPPORT
@@ -546,13 +740,8 @@ static PyObject *
makeip6addr(struct sockaddr_in6 *addr) { makeip6addr(struct sockaddr_in6 *addr) {
char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */ char buf[100]; /* must be at least INET6_ADDRSTRLEN + 1 */
const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf); const char *s = inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof buf);
#if PY_MAJOR_VERSION >= 3
if (s) return PyUnicode_FromString(s);
return PyUnicode_FromString("inet6:unknown");
#else
if (s) return PyString_FromString(s); if (s) return PyString_FromString(s);
return PyString_FromString("inet6:unknown"); return PyString_FromString("inet6:unknown");
#endif
} }
#endif #endif
@@ -643,11 +832,7 @@ generic_env_wrapper(SMFICTX *ctx, PyObject*cb, char **argv) {
for (i=0;i<count;i++) { for (i=0;i<count;i++) {
/* There's some error checking performed in do_mkvalue() for a string */ /* There's some error checking performed in do_mkvalue() for a string */
/* that's not currently done here - it probably should be */ /* that's not currently done here - it probably should be */
#if PY_MAJOR_VERSION >= 3
PyObject *o = PyBytes_FromStringAndSize(argv[i], strlen(argv[i]));
#else
PyObject *o = PyString_FromStringAndSize(argv[i], strlen(argv[i])); PyObject *o = PyString_FromStringAndSize(argv[i], strlen(argv[i]));
#endif
if (o == NULL) { /* out of memory */ if (o == NULL) { /* out of memory */
Py_DECREF(arglist); Py_DECREF(arglist);
return _report_exception(self); return _report_exception(self);
@@ -675,12 +860,7 @@ milter_wrap_header(SMFICTX *ctx, char *headerf, char *headerv) {
if (header_callback == NULL) return SMFIS_CONTINUE; if (header_callback == NULL) return SMFIS_CONTINUE;
c = _get_context(ctx); c = _get_context(ctx);
if (!c) return SMFIS_TEMPFAIL; if (!c) return SMFIS_TEMPFAIL;
#if PY_MAJOR_VERSION >= 3
/* pass val as bytes so Milter.Base.header_bytes can do surrogate escape. */
arglist = Py_BuildValue("(Osy)", c, headerf, headerv);
#else
arglist = Py_BuildValue("(Oss)", c, headerf, headerv); arglist = Py_BuildValue("(Oss)", c, headerf, headerv);
#endif
return _generic_wrapper(c, header_callback, arglist); return _generic_wrapper(c, header_callback, arglist);
} }
@@ -709,11 +889,7 @@ milter_wrap_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen) {
c = _get_context(ctx); c = _get_context(ctx);
if (!c) return SMFIS_TEMPFAIL; if (!c) return SMFIS_TEMPFAIL;
/* Unclear whether this should be s#, z#, or t# */ /* Unclear whether this should be s#, z#, or t# */
#if PY_MAJOR_VERSION >= 3 arglist = Py_BuildValue("(Os#)", c, bodyp, bodylen);
arglist = Py_BuildValue("(Oy#)", c, bodyp, (Py_ssize_t)bodylen);
#else
arglist = Py_BuildValue("(Os#)", c, bodyp, (Py_ssize_t)bodylen);
#endif
return _generic_wrapper(c, body_callback, arglist); return _generic_wrapper(c, body_callback, arglist);
} }
@@ -774,17 +950,20 @@ milter_wrap_negotiate(SMFICTX *ctx,
rc = _generic_wrapper(c, negotiate_callback, arglist); rc = _generic_wrapper(c, negotiate_callback, arglist);
c->t = t; c->t = t;
if (rc == SMFIS_CONTINUE) { 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 *pa[4] = { pf0,pf1,pf2,pf3 };
unsigned long fa[4] = { f0,f1,f2,f3 }; unsigned long fa[4] = { f0,f1,f2,f3 };
int len = PyList_Size(optlist); int len = PyList_Size(optlist);
int i; int i;
for (i = 0; i < 4; ++i) { for (i = 0; i < 4; ++i) {
*pa[i] = (i <= len) *pa[i] = (i <= len)
#if PY_MAJOR_VERSION >= 3
? PyLong_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
#else
? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i)) ? PyInt_AsUnsignedLongMask(PyList_GET_ITEM(optlist,i))
#endif
: fa[i]; : fa[i];
} }
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
@@ -792,6 +971,7 @@ milter_wrap_negotiate(SMFICTX *ctx,
PyErr_Clear(); PyErr_Clear();
rc = SMFIS_REJECT; rc = SMFIS_REJECT;
} }
#endif
} }
else if (rc != SMFIS_ALL_OPTS) else if (rc != SMFIS_ALL_OPTS)
rc = SMFIS_REJECT; rc = SMFIS_REJECT;
@@ -904,10 +1084,7 @@ milter_main(PyObject *self, PyObject *args) {
return NULL; return NULL;
} }
/* libmilter requires thread support */ /* libmilter requires thread support */
#if PY_VERSION_HEX < 0x03070000
/* called in Py_Initialize beginning with 3.7 */
PyEval_InitThreads(); PyEval_InitThreads();
#endif
/* let other threads run while in smfi_main() */ /* let other threads run while in smfi_main() */
interp = PyThreadState_Get()->interp; interp = PyThreadState_Get()->interp;
_main = PyEval_SaveThread(); /* must be done before smfi_main() */ _main = PyEval_SaveThread(); /* must be done before smfi_main() */
@@ -1138,8 +1315,6 @@ milter_chgfrom(PyObject *self, PyObject *args) {
SMFICTX *ctx; SMFICTX *ctx;
PyThreadState *t; PyThreadState *t;
/* FIXME: use s# to transition to allow passing bytes, but milter api
* requires NUL terminated bytes. */
if (!PyArg_ParseTuple(args, "s|z:chgfrom", &sender, &params)) if (!PyArg_ParseTuple(args, "s|z:chgfrom", &sender, &params))
return NULL; return NULL;
ctx = _find_context(self); ctx = _find_context(self);
@@ -1156,7 +1331,7 @@ Change/delete a header in the message. \n\
It is not checked for standards compliance; the mail filter\n\ It is not checked for standards compliance; the mail filter\n\
must ensure that no protocols are violated as a result of adding this header.\n\ must ensure that no protocols are violated as a result of adding this header.\n\
field - header field name\n\ field - header field name\n\
int - the Nth occurrence of this header\n\ int - the Nth occurence of this header\n\
value - header field value\n\ value - header field value\n\
field and value are strings.\n\ field and value are strings.\n\
This function can only be called from the EOM callback."; This function can only be called from the EOM callback.";
@@ -1236,7 +1411,7 @@ can only be called from the EOM callback.";
static PyObject * static PyObject *
milter_replacebody(PyObject *self, PyObject *args) { milter_replacebody(PyObject *self, PyObject *args) {
char *bodyp; char *bodyp;
Py_ssize_t bodylen; int bodylen;
SMFICTX *ctx; SMFICTX *ctx;
PyThreadState *t; PyThreadState *t;
@@ -1245,7 +1420,7 @@ milter_replacebody(PyObject *self, PyObject *args) {
if (ctx == NULL) return NULL; if (ctx == NULL) return NULL;
t = PyEval_SaveThread(); t = PyEval_SaveThread();
return _thread_return(t,smfi_replacebody(ctx, return _thread_return(t,smfi_replacebody(ctx,
(unsigned char *)bodyp, (int)bodylen), "cannot replace message body"); (unsigned char *)bodyp, bodylen), "cannot replace message body");
} }
static const char milter_setpriv__doc__[] = static const char milter_setpriv__doc__[] =
@@ -1376,12 +1551,10 @@ static PyMethodDef context_methods[] = {
{ NULL, NULL } { NULL, NULL }
}; };
#if PY_MAJOR_VERSION < 3
static PyObject * static PyObject *
milter_Context_getattr(PyObject *self, char *name) { milter_Context_getattr(PyObject *self, char *name) {
return Py_FindMethod(context_methods, self, name); return Py_FindMethod(context_methods, self, name);
} }
#endif
static struct smfiDesc description = { /* Set some reasonable defaults */ static struct smfiDesc description = { /* Set some reasonable defaults */
"pythonfilter", "pythonfilter",
@@ -1431,23 +1604,14 @@ static PyMethodDef milter_methods[] = {
}; };
static PyTypeObject milter_ContextType = { static PyTypeObject milter_ContextType = {
#if PY_MAJOR_VERSION >= 3
PyVarObject_HEAD_INIT(&PyType_Type,0)
"milter.Context",
#else
PyObject_HEAD_INIT(&PyType_Type) PyObject_HEAD_INIT(&PyType_Type)
0, 0,
"milterContext", "milterContext",
#endif
sizeof(milter_ContextObject), sizeof(milter_ContextObject),
0, 0,
milter_Context_dealloc, /* tp_dealloc */ milter_Context_dealloc, /* tp_dealloc */
0, /* tp_print */ 0, /* tp_print */
#if PY_MAJOR_VERSION >= 3
0, /* tp_getattr */
#else
milter_Context_getattr, /* tp_getattr */ milter_Context_getattr, /* tp_getattr */
#endif
0, /* tp_setattr */ 0, /* tp_setattr */
0, /* tp_compare */ 0, /* tp_compare */
0, /* tp_repr */ 0, /* tp_repr */
@@ -1461,15 +1625,6 @@ static PyTypeObject milter_ContextType = {
0, /* tp_setattro */ 0, /* tp_setattro */
0, /* tp_as_buffer */ 0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */ Py_TPFLAGS_DEFAULT, /* tp_flags */
#if PY_MAJOR_VERSION >= 3
NULL, /* Documentation string */
0, /* call function for all accessible objects */
0, /* delete references to contained objects */
0, /* rich comparisons */
0, /* weak reference enabler */
0, 0, /* Iterators */
context_methods, /* Attribute descriptor and subclassing stuff */
#endif
}; };
static const char milter_documentation[] = static const char milter_documentation[] =
@@ -1479,45 +1634,17 @@ Libmilter is currently marked FFR, and needs to be explicitly installed.\n\
See <sendmailsource>/libmilter/README for details on setting it up.\n"; See <sendmailsource>/libmilter/README for details on setting it up.\n";
static void setitem(PyObject *d,const char *name,long val) { static void setitem(PyObject *d,const char *name,long val) {
#if PY_MAJOR_VERSION >= 3
PyObject *v = PyLong_FromLong(val);
#else
PyObject *v = PyInt_FromLong(val); PyObject *v = PyInt_FromLong(val);
#endif
PyDict_SetItemString(d,name,v); PyDict_SetItemString(d,name,v);
Py_DECREF(v); Py_DECREF(v);
} }
#if PY_MAJOR_VERSION >= 3 void
initmilter(void) {
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"milter", /* m_name */
milter_documentation,/* m_doc */
-1, /* m_size */
milter_methods, /* m_methods */
NULL, /* m_reload */
NULL, /* m_traverse */
NULL, /* m_clear */
NULL, /* m_free */
};
PyMODINIT_FUNC PyInit_milter(void) {
PyObject *m, *d;
if (PyType_Ready(&milter_ContextType) < 0)
return NULL;
m = PyModule_Create(&moduledef);
if (m == NULL) return NULL;
#else
void initmilter(void) {
PyObject *m, *d; PyObject *m, *d;
m = Py_InitModule4("milter", milter_methods, milter_documentation, m = Py_InitModule4("milter", milter_methods, milter_documentation,
(PyObject*)NULL, PYTHON_API_VERSION); (PyObject*)NULL, PYTHON_API_VERSION);
#endif
d = PyModule_GetDict(m); d = PyModule_GetDict(m);
MilterError = PyErr_NewException("milter.error", NULL, NULL); MilterError = PyErr_NewException("milter.error", NULL, NULL);
PyDict_SetItemString(d,"error", MilterError); PyDict_SetItemString(d,"error", MilterError);
@@ -1583,7 +1710,4 @@ void initmilter(void) {
setitem(d,"DISCARD", SMFIS_DISCARD); setitem(d,"DISCARD", SMFIS_DISCARD);
setitem(d,"ACCEPT", SMFIS_ACCEPT); setitem(d,"ACCEPT", SMFIS_ACCEPT);
setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL); setitem(d,"TEMPFAIL", SMFIS_TEMPFAIL);
#if PY_MAJOR_VERSION >= 3
return m;
#endif
} }
+126 -68
View File
@@ -1,3 +1,87 @@
# $Log$
# Revision 1.8 2011/11/05 15:51:03 customdesigned
# New example
#
# Revision 1.7 2009/06/13 21:15:12 customdesigned
# Doxygen updates.
#
# Revision 1.6 2009/06/09 03:13:13 customdesigned
# More doxygen docs.
#
# Revision 1.5 2005/07/20 14:49:43 customdesigned
# Handle corrupt and empty ZIP files.
#
# Revision 1.4 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
# Revision 1.3 2005/06/02 15:00:17 customdesigned
# Configure banned extensions. Scan zipfile option with test case.
#
# Revision 1.2 2005/06/02 04:18:55 customdesigned
# Update copyright notices after reading article on /.
#
# Revision 1.1.1.4 2005/05/31 18:23:49 customdesigned
# Development changes since 0.7.2
#
# Revision 1.62 2005/02/14 22:31:17 stuart
# _parseparam replacement not needed for python2.4
#
# Revision 1.61 2005/02/12 02:11:11 stuart
# Pass unit tests with python2.4.
#
# Revision 1.60 2005/02/11 18:34:14 stuart
# Handle garbage after quote in boundary.
#
# Revision 1.59 2005/02/10 01:10:59 stuart
# Fixed MimeMessage.ismodified()
#
# Revision 1.58 2005/02/10 00:56:49 stuart
# Runs with python2.4. Defang not working correctly - more work needed.
#
# Revision 1.57 2004/11/20 16:37:52 stuart
# fix regex for splitting header and body
#
# Revision 1.56 2004/11/09 20:33:51 stuart
# Recognize more dynamic PTR variations.
#
# Revision 1.55 2004/10/06 21:39:20 stuart
# Handle message attachments with boundary errors by not parsing them
# until needed.
#
# Revision 1.54 2004/08/18 01:59:46 stuart
# Handle mislabeled multipart messages
#
# Revision 1.53 2004/04/24 22:53:20 stuart
# Rename some local variables to avoid shadowing builtins
#
# Revision 1.52 2004/04/24 22:47:13 stuart
# Convert header values to str
#
# Revision 1.51 2004/03/25 03:19:10 stuart
# Correctly defang rfc822 attachments when boundary specified with
# content-type message/rfc822.
#
# Revision 1.50 2003/10/15 22:01:00 stuart
# Test for and work around email bug with encoded filenames.
#
# Revision 1.49 2003/09/04 18:48:13 stuart
# Support python-2.2.3
#
# Revision 1.48 2003/09/02 00:27:27 stuart
# Should have full milter based dspam support working
#
# Revision 1.47 2003/08/26 06:08:18 stuart
# Use new python boolean since we now require 2.2.2
#
# Revision 1.46 2003/08/26 05:01:38 stuart
# Release 0.6.0
#
# Revision 1.45 2003/08/26 04:01:24 stuart
# Use new email module for parsing mail. Still need mime module to
# provide various bug fixes to email module, and maintain some compatibility
# with old milter code.
#
## @package mime ## @package mime
# This module provides a "defang" function to replace naughty attachments. # This module provides a "defang" function to replace naughty attachments.
# #
@@ -9,35 +93,26 @@
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. # Copyright 2001,2002,2003,2004,2005 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.
from __future__ import print_function import StringIO
try:
from io import BytesIO, StringIO
except:
from StringIO import StringIO
BytesIO = StringIO
import socket import socket
import Milter import Milter
import zipfile import zipfile
import sys
import email import email
from email.message import Message import email.Message
try: from email.Message import Message
from email.generator import BytesGenerator from email.Generator import Generator
from email import message_from_binary_file, encoders from email.Utils import quote
except: from email import Utils
from email.generator import Generator as BytesGenerator from email.Parser import Parser
from email import message_from_file as message_from_binary_file from email import Errors
from email import Encoders as encoders
from email.utils import quote
if not getattr(Message,'as_bytes',None): from types import ListType,StringType
Message.as_bytes = Message.as_string
## Return a list of filenames in a zip file. ## Return a list of filenames in a zip file.
# Embedded zip files are recursively expanded. # Embedded zip files are recursively expanded.
def zipnames(txt): def zipnames(txt):
fp = BytesIO(txt) fp = StringIO.StringIO(txt)
zipf = zipfile.ZipFile(fp,'r') zipf = zipfile.ZipFile(fp,'r')
names = [] names = []
for nm in zipf.namelist(): for nm in zipf.namelist():
@@ -48,7 +123,7 @@ def zipnames(txt):
## Fix multipart handling in email.Generator. ## Fix multipart handling in email.Generator.
# #
class MimeGenerator(BytesGenerator): 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
# self._handle_<maintype>_<subtype>(). If there's no handler for the # self._handle_<maintype>_<subtype>(). If there's no handler for the
@@ -58,7 +133,7 @@ class MimeGenerator(BytesGenerator):
if msg.is_multipart() and main.lower() != 'multipart': if msg.is_multipart() and main.lower() != 'multipart':
self._handle_multipart(msg) self._handle_multipart(msg)
else: else:
BytesGenerator._dispatch(self,msg) Generator._dispatch(self,msg)
def unquote(s): def unquote(s):
"""Remove quotes from a string.""" """Remove quotes from a string."""
@@ -75,17 +150,19 @@ def unquote(s):
return s[1:-1] return s[1:-1]
return s return s
from types import TupleType
def _unquotevalue(value): def _unquotevalue(value):
if isinstance(value, tuple): if isinstance(value, TupleType):
return value[0], value[1], unquote(value[2]) return value[0], value[1], unquote(value[2])
else: else:
return unquote(value) return unquote(value)
#email.Message._unquotevalue = _unquotevalue #email.Message._unquotevalue = _unquotevalue
from email.message import _parseparam from email.Message import _parseparam
## Enhance email.message.Message ## Enhance email.Message
# #
# Tracks modifications to headers of body or any part independently. # Tracks modifications to headers of body or any part independently.
@@ -125,8 +202,8 @@ class MimeMessage(Message):
"""Return a list of (attr,name) pairs of attributes that IE might """Return a list of (attr,name) pairs of attributes that IE might
interpret as a name - and hence decide to execute this message.""" interpret as a name - and hence decide to execute this message."""
names = [] names = []
for attr,val in self.get_params([],'content-type',False): for attr,val in self._get_params_preserve([],'content-type'):
if isinstance(val, tuple): if isinstance(val, TupleType):
# It's an RFC 2231 encoded parameter # It's an RFC 2231 encoded parameter
newvalue = _unquotevalue(val) newvalue = _unquotevalue(val)
if val[0]: if val[0]:
@@ -161,9 +238,9 @@ class MimeMessage(Message):
g = MimeGenerator(file) g = MimeGenerator(file)
g.flatten(self,unixfrom=unixfrom) g.flatten(self,unixfrom=unixfrom)
def as_bytes(self, unixfrom=False): def as_string(self, unixfrom=False):
"Return the entire formatted message as a string." "Return the entire formatted message as a string."
fp = BytesIO() fp = StringIO.StringIO()
self.dump(fp,unixfrom=unixfrom) self.dump(fp,unixfrom=unixfrom)
return fp.getvalue() return fp.getvalue()
@@ -195,11 +272,6 @@ class MimeMessage(Message):
def get_payload(self,i=None,decode=False): def get_payload(self,i=None,decode=False):
msg = self.submsg msg = self.submsg
if msg is None:
t = self.get_content_type().lower()
if t == 'message/rfc822' or t.startswith('multipart/'):
msg = super().get_payload()
self.submsg = msg
if isinstance(msg,Message) and msg.ismodified(): if isinstance(msg,Message) and msg.ismodified():
self.set_payload([msg]) self.set_payload([msg])
return Message.get_payload(self,i,decode) return Message.get_payload(self,i,decode)
@@ -218,11 +290,7 @@ class MimeMessage(Message):
if t == 'message/rfc822' or t.startswith('multipart/'): if t == 'message/rfc822' or t.startswith('multipart/'):
if not self.submsg: if not self.submsg:
txt = self.get_payload() txt = self.get_payload()
if type(txt) is bytes: if type(txt) == str:
self.submsg = email.message_from_bytes(txt,MimeMessage)
for part in self.submsg.walk():
part.modified = False
elif type(txt) is str:
txt = self.get_payload(decode=True) txt = self.get_payload(decode=True)
self.submsg = email.message_from_string(txt,MimeMessage) self.submsg = email.message_from_string(txt,MimeMessage)
for part in self.submsg.walk(): for part in self.submsg.walk():
@@ -233,7 +301,7 @@ class MimeMessage(Message):
return None return None
def message_from_file(fp): def message_from_file(fp):
msg = message_from_binary_file(fp,MimeMessage) msg = email.message_from_file(fp,MimeMessage)
for part in msg.walk(): for part in msg.walk():
part.modified = False part.modified = False
assert not msg.ismodified() assert not msg.ismodified()
@@ -244,7 +312,7 @@ ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,inf,ins,isp,js,
jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc, jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc,
wsf,wsh wsf,wsh
""".split()) """.split())
bad_extensions = ['.' + x for x in extlist.split(',')] bad_extensions = map(lambda x:'.' + x,extlist.split(','))
def check_ext(name): def check_ext(name):
"Check a name for dangerous Winblows extensions." "Check a name for dangerous Winblows extensions."
@@ -283,24 +351,19 @@ def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
msg["Content-Type"] = "text/plain; name="+name msg["Content-Type"] = "text/plain; name="+name
return Milter.CONTINUE return Milter.CONTINUE
def check_attachments(msg,check,lev=None): import email.Iterators
def check_attachments(msg,check):
"""Scan attachments. """Scan attachments.
msg MimeMessage msg MimeMessage
check function(MimeMessage): int check function(MimeMessage): int
Return CONTINUE, REJECT, ACCEPT Return CONTINUE, REJECT, ACCEPT
""" """
if msg.is_multipart(): if msg.is_multipart():
if not lev: lev = []
lev.append(1)
if msg.get_content_type().endswith('/rfc822'):
foo = 1
for i in msg.get_payload(): for i in msg.get_payload():
print('chkm',lev,msg.get_content_type()) rc = check_attachments(i,check)
rc = check_attachments(i,check,lev=lev)
if rc != Milter.CONTINUE: return rc if rc != Milter.CONTINUE: return rc
lev[-1] += 1
return Milter.CONTINUE return Milter.CONTINUE
print('chk',lev,msg.get_content_type())
return check(msg) return check(msg)
# save call context for Python without nested_scopes # save call context for Python without nested_scopes
@@ -335,21 +398,18 @@ class _defang:
# emulate old defang function # emulate old defang function
defang = _defang() defang = _defang()
if sys.version < '3.0.0': import sgmllib
from sgmllib import SGMLParser as HTMLParser
else:
from Milter.sgmllib import SGMLParser as HTMLParser
import re import re
declname = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*') declname = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*')
declstringlit = re.compile(r'(\'[^\']*\'|"[^"]*")\s*') declstringlit = re.compile(r'(\'[^\']*\'|"[^"]*")\s*')
class SGMLFilter(HTMLParser): class SGMLFilter(sgmllib.SGMLParser):
"""Parse HTML and pass through all constructs unchanged. It is intended for """Parse HTML and pass through all constructs unchanged. It is intended for
derived classes to implement exceptional processing for selected cases. derived classes to implement exceptional processing for selected cases.
""" """
def __init__(self,out): def __init__(self,out):
HTMLParser.__init__(self) sgmllib.SGMLParser.__init__(self)
self.out = out self.out = out
def handle_comment(self,comment): def handle_comment(self,comment):
@@ -380,7 +440,7 @@ class SGMLFilter(HTMLParser):
self.out.write("<!%s>" % data) self.out.write("<!%s>" % data)
def write(self,buf): def write(self,buf):
"Act like a writer. Why doesn't HTMLParser do this by default?" "Act like a writer. Why doesn't SGMLParser do this by default?"
self.feed(buf) self.feed(buf)
# Python-2.1 sgmllib rejects illegal declarations. Since various Microsoft # Python-2.1 sgmllib rejects illegal declarations. Since various Microsoft
@@ -423,14 +483,11 @@ class HTMLScriptFilter(SGMLFilter):
self.modified = False self.modified = False
self.msg = "<!-- WARNING: embedded script removed -->" self.msg = "<!-- WARNING: embedded script removed -->"
def start_script(self,unused): def start_script(self,unused):
#print('beg script',unused)
self.ignoring += 1 self.ignoring += 1
self.modified = True self.modified = True
def end_script(self):
#print('end script')
self.ignoring -= 1
if not self.ignoring:
self.out.write(self.msg) self.out.write(self.msg)
def end_script(self):
self.ignoring -= 1
def handle_data(self,data): def handle_data(self,data):
if not self.ignoring: SGMLFilter.handle_data(self,data) if not self.ignoring: SGMLFilter.handle_data(self,data)
def handle_comment(self,comment): def handle_comment(self,comment):
@@ -445,14 +502,14 @@ def check_html(msg,savname=None):
if name and name.lower().endswith(".htm"): if name and name.lower().endswith(".htm"):
msgtype = 'text/html' msgtype = 'text/html'
if msgtype == 'text/html': if msgtype == 'text/html':
out = StringIO() out = StringIO.StringIO()
htmlfilter = HTMLScriptFilter(out) htmlfilter = HTMLScriptFilter(out)
try: try:
htmlfilter.write(msg.get_payload(decode=True).decode()) htmlfilter.write(msg.get_payload(decode=True))
htmlfilter.close() htmlfilter.close()
#except sgmllib.SGMLParseError: #except sgmllib.SGMLParseError:
except: except:
mimetools.copyliteral(msg.get_payload(),open('debug.out','wb')) #mimetools.copyliteral(msg.get_payload(),open('debug.out','w')
htmlfilter.close() htmlfilter.close()
hostname = socket.gethostname() hostname = socket.gethostname()
msg.set_payload( msg.set_payload(
@@ -467,21 +524,22 @@ def check_html(msg,savname=None):
if htmlfilter.modified: if htmlfilter.modified:
msg.set_payload(out) # remove embedded scripts msg.set_payload(out) # remove embedded scripts
del msg["content-transfer-encoding"] del msg["content-transfer-encoding"]
encoders.encode_quopri(msg) email.Encoders.encode_quopri(msg)
return Milter.CONTINUE return Milter.CONTINUE
if __name__ == '__main__': if __name__ == '__main__':
import sys
def _list_attach(msg): def _list_attach(msg):
t = msg.get_content_type() t = msg.get_content_type()
p = msg.get_payload(decode=True) p = msg.get_payload(decode=True)
print(msg.get_filename(),msg.get_content_type(),type(p)) print msg.get_filename(),msg.get_content_type(),type(p)
msg = msg.get_submsg() msg = msg.get_submsg()
if isinstance(msg,Message): if isinstance(msg,Message):
return check_attachments(msg,_list_attach) return check_attachments(msg,_list_attach)
return Milter.CONTINUE return Milter.CONTINUE
for fname in sys.argv[1:]: for fname in sys.argv[1:]:
with open(fname,'rb') as fp: fp = open(fname)
msg = message_from_file(fp) msg = message_from_file(fp)
email.iterators._structure(msg) email.Iterators._structure(msg)
check_attachments(msg,_list_attach) check_attachments(msg,_list_attach)
-6
View File
@@ -1,6 +0,0 @@
Check Description Justification
E111 req indent 4 Creates more continuation lines
E114 req indent 4 cmnt Same
E231 req space after , makes calls like print() harder to read
E266 no ## Required by Doxygen
W291 trailing spaces in cmnt Needed for space preserving para reformat
-5
View File
@@ -1,5 +0,0 @@
#!/bin/sh
ignore=`awk -F\\\\t '{ print $1 }' pep8.dat | tail -n +2`
a=(${ignore})
list=$(echo "${a[@]}"|tr '[ ]' '[,]')
echo python3 -m pep8 --ignore="$list" $@
-27
View File
@@ -1,27 +0,0 @@
diff -up ./Milter/utils.py.check ./Milter/utils.py
--- ./Milter/utils.py.check 2018-08-04 23:01:23.858668412 -0400
+++ ./Milter/utils.py 2018-08-04 23:01:39.460869588 -0400
@@ -68,10 +68,6 @@ def iniplist(ipaddr,iplist):
True
>>> iniplist('192.168.0.45',['192.168.0.*'])
True
- >>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
- True
- >>> iniplist('2606:2800:220:1::',['example.com/40'])
- True
>>> iniplist('4.2.2.2',['nothing.example.com'])
False
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
diff -up ./test.py.check ./test.py
--- ./test.py.check 2018-08-04 23:04:58.609420815 -0400
+++ ./test.py 2018-08-04 23:05:40.070949438 -0400
@@ -14,6 +14,8 @@ def suite():
return s
if __name__ == '__main__':
+ import sys
try: os.remove('test/milter.log')
except: pass
- unittest.TextTestRunner().run(suite())
+ rc = unittest.TextTestRunner().run(suite())
+ sys.exit(len(rc.failures))
+54 -180
View File
@@ -1,222 +1,96 @@
# we don't want to provide private python extension libs %define __python python2.6
%global sum Python interface to sendmail milter API %define pythonbase python
%global __provides_exclude_from ^(%{python2_sitearch})/.*\\.so$
%if 0%{?epel} == 7
%global python3 python36
%else
%global python3 python3
%endif
Summary: %{sum} %define libdir %{_libdir}/pymilter
Name: python-pymilter %{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")}
Version: 1.0.4
Release: 1%{?dist} Summary: Python interface to sendmail milter API
Url: http://bmsi.com/pymilter Name: %{pythonbase}-pymilter
Source: https://github.com/sdgathman/pymilter/archive/pymilter-%{version}.tar.gz Version: 1.0
#Source1: tmpfiles-python-pymilter.conf Release: 1%{dist}
# remove unit tests that require network for check Source: http://downloads.sourceforge.net/pymilter/pymilter-%{version}.tar.gz
Patch: pymilter-check.patch Source1: pymilter.te
License: GPLv2+ License: GPLv2+
Group: Development/Libraries Group: Development/Libraries
BuildRequires: python2-devel, %{python3}-devel, sendmail-devel >= 8.13 BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
Url: http://www.bmsi.com/python/milter.html
# python-2.6.4 gets RuntimeError: not holding the import lock # python-2.6.4 gets RuntimeError: not holding the import lock
Requires: %{pythonbase} >= 2.6.5, sendmail >= 8.13
# Need python2.6 specific pydns, not the version for system python # Need python2.6 specific pydns, not the version for system python
BuildRequires: gcc Requires: %{pythonbase}-pydns
# Needed for callbacks, not a core function but highly useful for milters
BuildRequires: ed, %{pythonbase}-devel, sendmail-devel >= 8.13
%global _description\ %description
This is a python extension module to enable python scripts to\ This is a python extension module to enable python scripts to
attach to sendmail's libmilter functionality. Additional python\ attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, sending\ modules provide for navigating and modifying MIME parts, sending
DSNs, and doing CBV. DSNs, and doing CBV.
%description %_description
%package -n python2-pymilter
Summary: %{sum}
%if 0%{?epel} >= 6
Requires: python-pydns
%else
Requires: python2-pydns
%endif
Requires: %{name}-common = %{version}-%{release}
%{?python_provide:%python_provide python2-pymilter}
%description -n python2-pymilter %_description
%package -n %{python3}-pymilter
Summary: %{sum}
%if 0%{?fedora} >= 26
Requires: %{python3}-py3dns
%endif
Requires: %{name}-common = %{version}-%{release}
%{?python_provide:%python_provide %{python3}-pymilter}
%description -n %{python3}-pymilter %_description
%package common
Summary: Common files and directories for python milters
BuildArch: noarch
%description common
Common files and directories used for python milters
%package selinux %package selinux
Summary: SELinux policy module for pymilter Summary: SELinux policy module for pymilter
Group: System Environment/Base Group: System Environment/Base
Requires: policycoreutils, selinux-policy-targeted Requires: policycoreutils, selinux-policy, %{name}
Requires: %{name} = %{version}-%{release} BuildRequires: policycoreutils, checkpolicy
BuildArch: noarch
BuildRequires: policycoreutils, checkpolicy, selinux-policy-devel
%if 0%{?epel} >= 6
BuildRequires: policycoreutils-python
%else
BuildRequires: policycoreutils-python-utils
%endif
%description selinux %description selinux
Give sendmail_t additional access to stream sockets used to communicate SELinux policy module for using pymilter with sendmail with selinux enforcing
with milters.
%prep %prep
%setup -q -n pymilter-pymilter-%{version} %setup -q -n pymilter-%{version}
#patch -p1 -b .check cp %{SOURCE1} pymilter.te
%build %build
%py2_build env CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build
%py3_build
checkmodule -m -M -o pymilter.mod pymilter.te checkmodule -m -M -o pymilter.mod pymilter.te
semodule_package -o pymilter.pp -m pymilter.mod semodule_package -o pymilter.pp -m pymilter.mod
%install %install
%py2_install rm -rf $RPM_BUILD_ROOT
%py3_install %{__python} setup.py install --root=$RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/run/milter
mkdir -p %{buildroot}/run/milter mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/log/milter
mkdir -p %{buildroot}%{_localstatedir}/log/milter mkdir -p $RPM_BUILD_ROOT%{libdir}
mkdir -p %{buildroot}%{_libexecdir}/milter
#mkdir -p %{buildroot}%{_prefix}/lib/tmpfiles.d
#install -m 0644 %{SOURCE1} %{buildroot}%{_prefix}/lib/tmpfiles.d/%{name}.conf
# install selinux modules # install selinux modules
mkdir -p %{buildroot}%{_datadir}/selinux/targeted mkdir -p %{buildroot}%{_datadir}/selinux/targeted
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
%check %files
py2path=$(ls -d build/lib.linux-*-2.*) %defattr(-,root,root,-)
py3path=$(ls -d build/lib.linux-*-3.*)
PYTHONPATH=${py2path}:. python2 test.py &&
PYTHONPATH=${py3path}:. python3 test.py
%files -n python2-pymilter
%license COPYING
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py %doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%{python2_sitearch}/* %{python_sitearch}/*
%{libdir}
%files -n %{python3}-pymilter %dir %attr(0755,mail,mail) %{_localstatedir}/run/milter
%license COPYING
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
%{python3_sitearch}/*
%files common
%dir %{_libexecdir}/milter
%{_prefix}/lib/tmpfiles.d/%{name}.conf
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter %dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
%dir %attr(0755,mail,mail) /run/milter
%files selinux %files selinux
%doc pymilter.te %doc pymilter.te
%{_datadir}/selinux/targeted/* %{_datadir}/selinux/targeted/*
%clean
rm -rf $RPM_BUILD_ROOT
%post selinux %post selinux
%{_sbindir}/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \ /usr/sbin/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \
&>/dev/null || : &>/dev/null || :
%postun selinux %postun selinux
if [ $1 -eq 0 ] ; then if [ $1 -eq 0 ] ; then
%{_sbindir}/semodule -s targeted -r pymilter &> /dev/null || : /usr/sbin/semodule -s targeted -r pymilter &> /dev/null || :
fi fi
%changelog %changelog
* Wed Apr 17 2019 Stuart Gathman <stuart@gathman.org> - 1.0.4-1 * Sat Mar 1 2014 Stuart Gathman <stuart@gathman.org> 1.0-2
- New upstream release: cleanup unused files, additional platform support - Remove start.sh to track EPEL repository, suggest daemonize as replacement
- Minor doc updates - Selinux subpackage should not care about pymilter version
* Sun Dec 23 2018 Stuart Gathman <stuart@gathman.org> - 1.0.3-1 * Wed Jun 26 2013 Stuart Gathman <stuart@gathman.org> 1.0-1
- New upstream release - Allow ACCEPT as untrapped exception policy
- patch step for python3 no longer required in build - Optional dir for getaddrset and getaddrdict in Milter.config
- Show registered milter name in untrapped exception message.
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-4 - Include selinux subpackage
- Add unit tests to %%check - Provide Milter.greylist export and Milter.greylist import to migrate data
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-3
- use libexec instead of libdir
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-2
- add python34 subpackage on el7
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-1
- build for both python2 and python3
- add selinux policy allowing sendmail_t access to milters
* Tue Jul 17 2018 Miro Hrončok <mhroncok@redhat.com> - 1.0-13
- Update Python macros to new packaging standards
(See https://fedoraproject.org/wiki/Changes/Move_usr_bin_python_into_separate_package)
* Sat Jul 14 2018 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-12
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
* Fri Feb 09 2018 Iryna Shcherbina <ishcherb@redhat.com> - 1.0-11
- Update Python 2 dependency declarations to new packaging standards
(See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3)
* Fri Feb 09 2018 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-10
- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild
* Fri Feb 09 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 1.0-9
- Escape macros in %%changelog
* Sat Aug 19 2017 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> - 1.0-8
- Python 2 binary package renamed to python2-pymilter
See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3
* Thu Aug 03 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-7
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Binutils_Mass_Rebuild
* Thu Jul 27 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-6
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild
>>>>>>> 021796e51e5919812f1c300d1830ef9ed378db2d
* Sat Feb 11 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-5
- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild
* Tue Jul 19 2016 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.0-4
- https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages
* Thu Feb 04 2016 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild
* Thu Jun 18 2015 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.0-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild
* Sat Sep 27 2014 Paul Wouters <pwouters@redhat.com> - 1.0-1
- Updated to 1.0
- Use tmpfiles and /run
* Sun Aug 17 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.8-6
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_22_Mass_Rebuild
* Sat Jun 07 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.8-5
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild
* Fri Jan 10 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-4
- Add COPYING
- Fix buildroot macros and dist macro
* Fri Jan 10 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-3
- rebuilt with proper file permission
* Tue Jan 07 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-2
- Fixup for fedora release
* Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 0.9.8-1 * Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 0.9.8-1
- Add Milter.test module for unit testing milters. - Add Milter.test module for unit testing milters.
@@ -224,13 +98,13 @@ fi
- Change untrapped exception message to: - Change untrapped exception message to:
- "pymilter: untrapped exception in milter app" - "pymilter: untrapped exception in milter app"
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1 * Thu Apr 12 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback - Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
- Remove redundant table in miltermodule - Remove redundant table in miltermodule
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf). - Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1 * Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1
- Raise ValueError on unescaped '%%' passed to setreply - Raise ValueError on unescaped '%' passed to setreply
- Grace time at end of Greylist window - Grace time at end of Greylist window
* Fri Aug 19 2011 Stuart Gathman <stuart@bmsi.com> 0.9.5-1 * Fri Aug 19 2011 Stuart Gathman <stuart@bmsi.com> 0.9.5-1
@@ -239,7 +113,7 @@ fi
- Fix milter-template.py - Fix milter-template.py
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6 - Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
* Tue Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1 * Wed Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
- Handle IP6 in Milter.utils.iniplist() - Handle IP6 in Milter.utils.iniplist()
- python-2.6 - python-2.6
+26 -40
View File
@@ -1,4 +1,4 @@
from __future__ import print_function
# A simple milter. # A simple milter.
# Author: Stuart D. Gathman <stuart@bmsi.com> # Author: Stuart D. Gathman <stuart@bmsi.com>
@@ -7,10 +7,8 @@ from __future__ import print_function
import sys import sys
import os import os
try: import StringIO
from io import BytesIO import rfc822
except:
from StringIO import StringIO as BytesIO
import mime import mime
import Milter import Milter
import tempfile import tempfile
@@ -23,14 +21,9 @@ class sampleMilter(Milter.Milter):
"Milter to replace attachments poisonous to Windows with a WARNING message." "Milter to replace attachments poisonous to Windows with a WARNING message."
def log(self,*msg): def log(self,*msg):
print("%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),end=None) print "%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),
for i in msg: for i in msg: print i,
try: print
print(i,end=None)
except UnicodeEncodeError:
s = i.encode(encoding='utf-8',errors='surrogateescape')
print(s,end=None)
print()
def __init__(self): def __init__(self):
self.tempname = None self.tempname = None
@@ -38,25 +31,18 @@ class sampleMilter(Milter.Milter):
self.fp = None self.fp = None
self.bodysize = 0 self.bodysize = 0
self.id = Milter.uniqueID() self.id = Milter.uniqueID()
self.user = None
# multiple messages can be received on a single connection # multiple messages can be received on a single connection
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start # envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
# of each message. # of each message.
@Milter.symlist('{auth_authen}')
@Milter.noreply @Milter.noreply
def envfrom(self,f,*str): def envfrom(self,f,*str):
"start of MAIL transaction" "start of MAIL transaction"
self.fp = BytesIO() self.log("mail from",f,str)
self.fp = StringIO.StringIO()
self.tempname = None self.tempname = None
self.mailfrom = f self.mailfrom = f
self.bodysize = 0 self.bodysize = 0
self.user = self.getsymval('{auth_authen}')
self.auth_type = self.getsymval('{auth_type}')
if self.user:
self.log("user",self.user,"sent mail from",f,str)
else:
self.log("mail from",f,str)
return Milter.CONTINUE return Milter.CONTINUE
def envrcpt(self,to,*str): def envrcpt(self,to,*str):
@@ -67,31 +53,28 @@ class sampleMilter(Milter.Milter):
self.log("rcpt to",to,str) self.log("rcpt to",to,str)
return Milter.CONTINUE return Milter.CONTINUE
@Milter.decode('bytes')
def header(self,name,val): def header(self,name,val):
lname = name.lower() lname = name.lower()
if lname == 'subject': if lname == 'subject':
# even if we wanted the Taiwanese spam, we can't read Chinese # even if we wanted the Taiwanese spam, we can't read Chinese
# (delete if you read chinese mail) # (delete if you read chinese mail)
#print('val=',val.encode(errors='surrogateescape')) if val.startswith('=?big5') or val.startswith('=?ISO-2022-JP'):
print('val=',val)
if val.startswith(b'=?big5') or val.startswith(b'=?ISO-2022-JP'):
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
# check for common spam keywords # check for common spam keywords
if val.find(b"$$$") >= 0 or val.find(b"XXX") >= 0 \ if val.find("$$$") >= 0 or val.find("XXX") >= 0 \
or val.find(b"!!!") >= 0 or val.find(b"FREE") >= 0: or val.find("!!!") >= 0 or val.find("FREE") >= 0:
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
# check for spam that pretends to be legal # check for spam that pretends to be legal
lval = val.lower() lval = val.lower()
if lval.startswith(b"adv:") or lval.startswith(b"adv.") \ if lval.startswith("adv:") or lval.startswith("adv.") \
or lval.find(b'viagra') >= 0: or lval.find('viagra') >= 0:
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
return Milter.REJECT return Milter.REJECT
@@ -103,7 +86,7 @@ class sampleMilter(Milter.Milter):
# check for common bulk mailers # check for common bulk mailers
if lname == 'x-mailer' and \ if lname == 'x-mailer' and \
val.lower() in (b'direct email',b'calypso',b'mail bomber'): val.lower() in ('direct email','calypso','mail bomber'):
self.log('REJECT: %s: %s' % (name,val)) self.log('REJECT: %s: %s' % (name,val))
#self.setreply('550','','Go away spammer') #self.setreply('550','','Go away spammer')
return Milter.REJECT return Milter.REJECT
@@ -112,12 +95,12 @@ class sampleMilter(Milter.Milter):
if lname in ('subject','x-mailer'): if lname in ('subject','x-mailer'):
self.log('%s: %s' % (name,val)) self.log('%s: %s' % (name,val))
if self.fp: if self.fp:
self.fp.write(b"%s: %s\n" % (name.encode(),val)) # add header to buffer self.fp.write("%s: %s\n" % (name,val)) # add header to buffer
return Milter.CONTINUE return Milter.CONTINUE
def eoh(self): def eoh(self):
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
self.fp.write(b'\n') self.fp.write("\n")
self.fp.seek(0) self.fp.seek(0)
# copy headers to a temp file for scanning the body # copy headers to a temp file for scanning the body
headers = self.fp.getvalue() headers = self.fp.getvalue()
@@ -155,16 +138,19 @@ class sampleMilter(Milter.Milter):
self.log("Temp file:",self.tempname) self.log("Temp file:",self.tempname)
self.tempname = None # prevent removal of original message copy self.tempname = None # prevent removal of original message copy
# copy defanged message to a temp file # copy defanged message to a temp file
with tempfile.TemporaryFile() as out: out = tempfile.TemporaryFile()
try:
msg.dump(out) msg.dump(out)
out.seek(0) out.seek(0)
msg = mime.message_from_file(out) msg = rfc822.Message(out)
fp = BytesIO(msg.as_bytes().split(b'\n\n',1)[1]) msg.rewindbody()
while 1: while 1:
buf = fp.read(8192) buf = out.read(8192)
if len(buf) == 0: break if len(buf) == 0: break
self.replacebody(buf) # feed modified message to sendmail self.replacebody(buf) # feed modified message to sendmail
return Milter.ACCEPT # ACCEPT modified message return Milter.ACCEPT # ACCEPT modified message
finally:
out.close()
return Milter.TEMPFAIL return Milter.TEMPFAIL
def close(self): def close(self):
@@ -185,13 +171,13 @@ if __name__ == "__main__":
socketname = os.getenv("HOME") + "/pythonsock" socketname = os.getenv("HOME") + "/pythonsock"
Milter.factory = sampleMilter Milter.factory = sampleMilter
Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS) Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS)
print("""To use this with sendmail, add the following to sendmail.cf: print """To use this with sendmail, add the following to sendmail.cf:
O InputMailFilters=pythonfilter O InputMailFilters=pythonfilter
Xpythonfilter, S=local:%s Xpythonfilter, S=local:%s
See the sendmail README for libmilter. See the sendmail README for libmilter.
sample milter startup""" % socketname) sample milter startup""" % socketname
sys.stdout.flush() sys.stdout.flush()
Milter.runmilter("pythonfilter",socketname,240) Milter.runmilter("pythonfilter",socketname,240)
print("sample milter shutdown") print "sample milter shutdown"
+1 -1
View File
@@ -1,5 +1,5 @@
[bdist_rpm] [bdist_rpm]
python=python3 python=python2.6
doc_files=README NEWS TODO COPYING CREDITS doc_files=README NEWS TODO COPYING CREDITS
packager=Stuart D. Gathman <stuart@gathman.org> packager=Stuart D. Gathman <stuart@gathman.org>
release=1 release=1
+12 -14
View File
@@ -1,42 +1,40 @@
import os import os
import sys import sys
from setuptools import setup, Extension from distutils.core import setup, Extension
if sys.version < '2.6.5': if sys.version < '2.6.5':
sys.exit('ERROR: Sorry, python 2.6.5 is required for this module.') sys.exit('ERROR: Sorry, python 2.6.5 is required for this module.')
with open("README.md", "r") as fh:
long_description = fh.read()
# 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"] libs = ["milter"]
libdirs = ["/usr/lib/libmilter"] # needed for Debian libdirs = ["/usr/lib/libmilter"] # needed for Debian
modules = ["mime"]
# NOTE: importing Milter to obtain version fails when milter.so not built # NOTE: importing Milter to obtain version fails when milter.so not built
setup(name = "pymilter", version = '1.0.5', setup(name = "pymilter", version = '1.0',
description="Python interface to sendmail milter API", description="Python interface to sendmail milter API",
long_description=long_description, long_description="""\
long_description_content_type='text/markdown', This is a python extension module to enable python scripts to
attach to sendmail's libmilter functionality. Additional python
modules provide for navigating and modifying MIME parts, and
sending DSNs or doing CBVs.
""",
author="Jim Niemira", author="Jim Niemira",
author_email="urmane@urmane.org", author_email="urmane@urmane.org",
maintainer="Stuart D. Gathman", maintainer="Stuart D. Gathman",
maintainer_email="stuart@gathman.org", maintainer_email="stuart@bmsi.com",
license="GPL", license="GPL",
url="https://www.pymilter.org/", url="http://www.bmsi.com/python/milter.html",
py_modules=modules, py_modules=["mime"],
packages = ['Milter'], packages = ['Milter'],
ext_modules=[ ext_modules=[
Extension("milter", ["miltermodule.c"], Extension("milter", ["miltermodule.c"],
library_dirs=libdirs, library_dirs=libdirs,
libraries=libs, libraries=libs,
# set MAX_ML_REPLY to 1 for sendmail < 8.13 # set MAX_ML_REPLY to 1 for sendmail < 8.13
define_macros = [ ('MAX_ML_REPLY',32) ], define_macros = [ ('MAX_ML_REPLY',32) ]
# save lots of debugging time testing rfc2553 compliance
extra_compile_args = [ "-Werror=implicit-function-declaration" ]
), ),
], ],
keywords = ['sendmail','milter'], keywords = ['sendmail','milter'],
Executable
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
appname="$1"
script="${2:-${appname}}"
datadir="/var/lib/milter"
logdir="/var/log/milter"
piddir="/var/run/milter"
libdir="/usr/lib/pymilter"
python="python2.4"
exec >>${logdir}/${appname}.log 2>&1
if test -s ${datadir}/${script}.py; then
cd ${datadir} # use version in data dir if it exists for debugging
elif test -s ${logdir}/${script}.py; then
cd ${logdir} # use version in log dir if it exists for debugging
else
cd ${libdir}
fi
${python} ${script}.py &
echo $! >${piddir}/${appname}.pid
-4
View File
@@ -3,8 +3,6 @@ import testmime
import testsample import testsample
import testutils import testutils
import testgrey import testgrey
import testcfg
import testpolicy
import os import os
def suite(): def suite():
@@ -13,8 +11,6 @@ def suite():
s.addTest(testsample.suite()) s.addTest(testsample.suite())
s.addTest(testutils.suite()) s.addTest(testutils.suite())
s.addTest(testgrey.suite()) s.addTest(testgrey.suite())
s.addTest(testcfg.suite())
s.addTest(testpolicy.suite())
return s return s
if __name__ == '__main__': if __name__ == '__main__':
-10
View File
@@ -1,10 +0,0 @@
SPF-Pass:example.com OK
SPF-Neutral:example.com REJECT
HELO-Neutral:example.com OK
SPF-Permerror:foo@bad.example.com OK
SPF-Permerror: REJECT
SMTP-Auth:good@example.com OK
SMTP-Auth:example.com REJECT
SMTP-Auth:bad@localhost.localdomain REJECT
SMTP-Test: REJECT
SMTP-Test:.baz.com WILDCARD
-32
View File
@@ -1,32 +0,0 @@
# sample SRS configuration
[srs]
;secret="shhhh!"
;maxage=21
;hashlength=5
# if defined, SRS uses a database for opaque rewriting
;database=/var/log/milter/srsdata
# sign these domains using SES to prevent forged bounces instead of SRS
;ses = localdomain1.com, localdomain2.org
# sign these domains using SRS in signing mode to prevent forged bounces
;sign = localdomain1.com, localdomain2.org
# rewrite all other domains to this domain using SRS
;fwdomain = mydomain.com
# additional domains to decode (reverse) srs
# NOTE: bms.py in milter package can also do this, as can pysrs.m4 HACK.
;srs = otherdomain.com
# do not rewrite mail to these domains
;nosrs = braindeadmail.com
# Treat these localparts as a DSN. Lot's of braindead systems
# send non-DSN mail to MAIL FROM.
;banned_users = mailer-daemon, clamav, postmaster
[srsmilter]
;datadir=/var/lib/milter
socketname = /var/run/milter/srsmilter
miltername = pysrsfilter
# reject DSNs to unsigned recipients (bounce spam)
reject_spoofed = true
;trusted_relay = 1.2.3.4
internal_connect = 192.168.*.*,127.0.0.1,::1
# Enable outgoing SRS via CHGFROM (see code for limitations)
miltersrs = false
-6
View File
@@ -1,6 +0,0 @@
From the-concourse-on-high Sat Feb 2 13:01:43 2019
Date: Sat, 02 Feb 2019 19:48:56 +0100
To: stuart@[IPv6:fcd9:7f8a:e050:4b48:7fd6:7fa:5509:6e26]
Subject: 来自qq.com的退信
Does you receive this email?
BIN
View File
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
by bmsaix.bmsi.com (8.9.1/8.9.1) with ESMTP id FAA42304
for <ed@bmsi.com>; Thu, 4 May 2000 05:22:03 -0400
Received: from camco.celestial.com (root@dagney.celestial.com [192.136.111.7])
by www.bmsi.com (8.9.1/8.9.1) with ESMTP id FAA21364
for <ed@bmsi.com>; Thu, 4 May 2000 05:22:01 -0400
Received: (12482 bytes) by camco.celestial.com
via sendmail with P:stdio/D:lists/R:inet_hosts/T:smtp
(sender: <owner-flexfax@celestial.com> owner: <owner-flexfax-outbound>)
id <m12nHjG-000eNHa@camco.celestial.com>
for flexfax-outbound; Thu, 4 May 2000 02:15:30 -0700 (PDT)
(Smail-3.2.0.111 2000-Feb-17 #1 built 2000-Apr-13)
Received: from sgi.com(sgi.SGI.COM[192.48.153.1]) (12116 bytes) by camco.celestial.com
via sendmail with P:esmtp/D:aliases/T:pipe
(sender: <owner-flexfax@sgi.com> owner: <owner-flexfax>)
id <m12nHh6-000eN7C@camco.celestial.com>
for <flexfax@celestial.com>; Thu, 4 May 2000 02:13:16 -0700 (PDT)
(Smail-3.2.0.111 2000-Feb-17 #1 built 2000-Apr-13)
Received: from proxy.internet ([195.184.42.82])
by sgi.com (980327.SGI.8.8.8-aspam/980304.SGI-aspam:
SGI does not authorize the use of its proprietary
systems or networks for unsolicited or bulk email
from the Internet.)
via ESMTP id CAA02330
for <flexfax@sgi.com>; Thu, 4 May 2000 02:13:10 -0700 (PDT)
mail_from (orum@ditas.dk)
Received: from [172.16.96.14] by proxy.daab.dkproxy.internet (NTMail 4.30.0013/NU4152.00.32401f35) with ESMTP id zmlyaaaa for <flexfax@sgi.com>; Thu, 4 May 2000 11:13:09 +0200
Received: by mars with Internet Mail Service (5.5.2650.21)
id <KGM63KG3>; Thu, 4 May 2000 11:11:13 +0100
Message-ID: <9704D2AA604ED311BF6D0008C79F0A990B57BE@mars>
From: =?iso-8859-1?Q?Peter_=D8rum?= <orum@ditas.dk>
To: "'flexfax@sgi.com'" <flexfax@sgi.com>
Subject: flexfax: ILOVEYOU
Date: Thu, 4 May 2000 11:11:11 +0100
MIME-Version: 1.0
X-Mailer: Internet Mail Service (5.5.2650.21)
Content-Type: multipart/mixed;
boundary="----_=_NextPart_000_01BFB5B1.13228432"
Sender: owner-flexfax@celestial.com
Precedence: bulk
This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
------_=_NextPart_000_01BFB5B1.13228432
Content-Type: text/plain
kindly check the attached LOVELETTER coming from me.
------_=_NextPart_000_01BFB5B1.13228432
Content-Type: application/octet-stream;
name="LOVE-LETTER-FOR-YOU.TXT.vbs"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="LOVE-LETTER-FOR-YOU.TXT.vbs"
rem barok -loveletter(vbe) <i hate go to school>
rem by: spyder / ispyder@mail.com / @GRAMMERSoft Group / =
Manila,Philippines
On Error Resume Next
set b=3Dfso.CreateTextFile(dirsystem+"\LOVE-LETTER-FOR-YOU.HTM")
b.close
set d=3Dfso.OpenTextFile(dirsystem+"\LOVE-LETTER-FOR-YOU.HTM",2)
d.write dt5
d.write join(lines,vbcrlf)
d.write vbcrlf
d.write dt6
d.close
end sub
------_=_NextPart_000_01BFB5B1.13228432--
+127
View File
@@ -0,0 +1,127 @@
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
by bmsaix.bmsi.com (8.12.3/8.12.2) with ESMTP id g41MmROS014480
for <stuart@bmsi.com>; Wed, 1 May 2002 18:48:27 -0400
Received: from bmsred.bmsi.com (bmsred [219.109.11.50])
by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g41MmFGR017812
for <stuart@bmsi.com>; Wed, 1 May 2002 18:48:15 -0400
X-Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
by bmsaix.bmsi.com (8.12.3/8.12.2) with ESMTP id g41M3hOS038584
for <ed@bmsi.com>; Wed, 1 May 2002 18:03:43 -0400
X-Received: from exp.dflinc.com (exppub [12.148.147.210])
by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g41M3LGQ017812
for <ed@bmsi.com>; Wed, 1 May 2002 18:03:22 -0400
X-Received: from exp.dflinc.com (exp.dflinc.com [219.109.14.1])
by exp.dflinc.com (8.12.1/8.12.1) with ESMTP id g41M3JGT012258
for <ed@bmsi.com>; Wed, 1 May 2002 17:03:19 -0500
X-Received: from dns.intervip.psi.br (dns.intervip.psi.br [200.215.126.2])
by exp.dflinc.com (8.12.1/8.12.1) with ESMTP id g3NHlhGS032960
for <lorraine@dflinc.com>; Tue, 23 Apr 2002 12:47:44 -0500
X-Received: from Sncpyf (adsl-fnsbnu-055-k.brt.telesc.net.br [200.180.75.55])
by dns.intervip.psi.br (Postfix) with SMTP id 1FAEE24D18
for <lorraine@dflinc.com>; Tue, 23 Apr 2002 14:50:41 -0300 (BRT)
From: enardelli <enardelli@karsten.com.br>
To: lorraine@dflinc.com
Subject: A special powful tool
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary=XQ4T5Cj14m5h2vQ69IpO4mCG
Message-Id: <20020423175041.1FAEE24D18@dns.intervip.psi.br>
Date: Tue, 23 Apr 2002 14:50:41 -0300 (BRT)
X-ReSent-Date: Wed, 1 May 2002 17:03:03 -0500 (CDT)
X-ReSent-From: Gwen Bartelle <gwenb@dflinc.com>
X-ReSent-To: ed@bmsi.com
X-ReSent-Subject: A special powful tool
X-ReSent-Message-ID: <Pine.A41.4.10.10205011703030.30638@exp.dflinc.com>
ReSent-Date: Wed, 1 May 2002 18:47:52 -0400 (EDT)
ReSent-From: Ed Bond <ed@bmsi.com>
ReSent-To: Stuart Gathman <stuart@bmsi.com>
ReSent-Subject: A special powful tool
ReSent-Message-ID: <Pine.LNX.4.44.0205011847520.17454@bmsred.bmsi.com>
--XQ4T5Cj14m5h2vQ69IpO4mCG
Content-Type: text/html;
Content-Transfer-Encoding: quoted-printable
<HTML><HEAD></HEAD><BODY>
<iframe src=3Dcid:Ux7VyFy7bTS9q height=3D0 width=3D0>
</iframe>
<FONT>Hi,This is a special powful tool<br>
I wish you would enjoy it.</FONT></BODY></HTML>
--XQ4T5Cj14m5h2vQ69IpO4mCG
Content-Type: audio/x-midi;
name=hom1;tile=1;ord=3354010700499224[1].scr
Content-Transfer-Encoding: base64
Content-ID: <Ux7VyFy7bTS9q>
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
CMDDePe/RHj3v5IT+r+Pe/e/kHr3v9Fv97/1Gfq/93H3v1Yc+r/3dve/oGj3v8sK+r+sx/e/
Nyz5v7Hu+b98HD==
--XQ4T5Cj14m5h2vQ69IpO4mCG
--XQ4T5Cj14m5h2vQ69IpO4mCG
Content-Type: application/octet-stream;
name=hom1;tile=1;ord=3354010700499224[1].htm
Content-Transfer-Encoding: base64
Content-ID: <Ux7VyFy7bTS9q>
PGh0bWw+PGhlYWQ+PHRpdGxlPkNsaWNrIGhlcmUgdG8gZmluZCBvdXQgbW9yZSE8L3RpdGxl
PjwvaGVhZD4NCjxib2R5PjxTQ1JJUFQgTEFOR1VBR0U9SmF2YVNjcmlwdD4KPCEtLQp2YXIg
U2hvY2tNb2RlID0gMDsKaWYgKG5hdmlnYXRvci5taW1lVHlwZXMgJiYgbmF2aWdhdG9yLm1p
bWVUeXBlc1siYXBwbGljYXRpb24veC1zaG9ja3dhdmUtZmxhc2giXSAmJiBuYXZpZ2F0b3Iu
bWltZVR5cGVzWyJhcHBsaWNhdGlvbi94LXNob2Nrd2F2ZS1mbGFzaCJdLmVuYWJsZWRQbHVn
aW4pIHsKaWYgKG5hdmlnYXRvci5wbHVnaW5zICYmIG5hdmlnYXRvci5wbHVnaW5zWyJTaG9j
a3dhdmUgRmxhc2giXSkKU2hvY2tNb2RlID0gMTsKfQplbHNlIGlmIChuYXZpZ2F0b3IudXNl
ckFnZW50ICYmIG5hdmlnYXRvci51c2VyQWdlbnQuaW5kZXhPZigiTVNJRSIpPj0wIAomJiAo
bmF2aWdhdG9yLnVzZXJBZ2VudC5pbmRleE9mKCJXaW5kb3dzIDkiKT49MCB8fCBuYXZpZ2F0
b3IudXNlckFnZW50LmluZGV4T2YoIldpbmRvd3MgTlQiKT49MCkpIHsKZG9jdW1lbnQud3Jp
dGUoJzxTQ1JJUFQgTEFOR1VBR0U9VkJTY3JpcHRcPiBcbicpOwpkb2N1bWVudC53cml0ZSgn
b24gZXJyb3IgcmVzdW1lIG5leHQgXG4nKTsKZG9jdW1lbnQud3JpdGUoJ1Nob2NrTW9kZSA9
IChJc09iamVjdChDcmVhdGVPYmplY3QoIlNob2Nrd2F2ZUZsYXNoLlNob2Nrd2F2ZUZsYXNo
LjMiKSkpICcpOwpkb2N1bWVudC53cml0ZSgnPFwvU0NSSVBUXD4gJyk7Cn0KaWYgKCBTaG9j
a01vZGUgKSB7CmRvY3VtZW50LndyaXRlKCc8T0JKRUNUIGNsYXNzaWQ9ImNsc2lkOkQyN0NE
QjZFLUFFNkQtMTFjZi05NkI4LTQ0NDU1MzU0MDAwMCInKTsKZG9jdW1lbnQud3JpdGUoJyBj
b2RlYmFzZT0iaHR0cDovL2FjdGl2ZS5tYWNyb21lZGlhLmNvbS9mbGFzaDIvY2Ficy9zd2Zs
YXNoLmNhYiN2ZXJzaW9uPTMsMCwwLDAiJyk7CmRvY3VtZW50LndyaXRlKCcgSUQ9YmFubmVy
IFdJRFRIPTIzMCBIRUlHSFQ9MjIwPicpOwpkb2N1bWVudC53cml0ZSgnIDxQQVJBTSBOQU1F
PW1vdmllIFZBTFVFPSJodHRwOi8vd3d3LnRlcnJhLmNvbS5ici9hZHMvcG9wXzIzMHgyMjBf
Z3Z0X3RlbGVmb25lLnN3Zj9jbGlja3RhZz1odHRwOi8vYWQuYnIuZG91YmxlY2xpY2submV0
L2NsaWNrJTNCaD12MnwyZGRkfDN8MHwlfHAlM0IzOTI1ODU3JTNCMC0wJTNCMCUzQjY2NjEw
MDIlM0IxLTQ2OHw2MCUzQjUwOTkxN3w1MDkyNDR8MSUzQiUzQiUzZmh0dHAlM2ElMmYlMmZ3
d3cuZ3Z0Lm5ldC5ici9taWRpYV9wb3B1cHRlcnJhX3Byb21vcG9ydGFsLmpzcCI+ICcpOwpk
b2N1bWVudC53cml0ZSgnIDxQQVJBTSBOQU1FPXF1YWxpdHkgVkFMVUU9YXV0b2hpZ2g+ICcp
Owpkb2N1bWVudC53cml0ZSgnPEVNQkVEIFNSQz0iaHR0cDovL3d3dy50ZXJyYS5jb20uYnIv
YWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxlZm9uZS5zd2Y/Y2xpY2t0YWc9aHR0cDovL2FkLmJy
LmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8MmRkZHwzfDB8JXxwJTNCMzkyNTg1NyUz
QjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAlM0I1MDk5MTd8NTA5MjQ0fDElM0IlM0Il
M2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIvbWlkaWFfcG9wdXB0ZXJyYV9wcm9tb3Bv
cnRhbC5qc3AiJyk7CmRvY3VtZW50LndyaXRlKCcgc3dMaXZlQ29ubmVjdD1GQUxTRSBXSURU
SD0yMzAgSEVJR0hUPTIyMCcpOwpkb2N1bWVudC53cml0ZSgnIFFVQUxJVFk9YXV0b2hpZ2gn
KTsKZG9jdW1lbnQud3JpdGUoJyBUWVBFPSJhcHBsaWNhdGlvbi94LXNob2Nrd2F2ZS1mbGFz
aCIgUExVR0lOU1BBR0U9Imh0dHA6Ly93d3cubWFjcm9tZWRpYS5jb20vc2hvY2t3YXZlL2Rv
d25sb2FkL2luZGV4LmNnaT9QMV9Qcm9kX1ZlcnNpb249U2hvY2t3YXZlRmxhc2giPicpOwpk
b2N1bWVudC53cml0ZSgnPC9FTUJFRD4nKTsKZG9jdW1lbnQud3JpdGUoJzwvT0JKRUNUPicp
Owp9IGVsc2UgaWYgKCEobmF2aWdhdG9yLmFwcE5hbWUgJiYgbmF2aWdhdG9yLmFwcE5hbWUu
aW5kZXhPZigiTmV0c2NhcGUiKT49MCAmJiBuYXZpZ2F0b3IuYXBwVmVyc2lvbi5pbmRleE9m
KCIyLiIpPj0wKSl7CmRvY3VtZW50LndyaXRlKCc8QSBIUkVGPSJodHRwOi8vYWQuYnIuZG91
YmxlY2xpY2submV0L2NsaWNrJTNCaD12MnwyZGRkfDN8MHwlfHAlM0IzOTI1ODU3JTNCMC0w
JTNCMCUzQjY2NjEwMDIlM0IxLTQ2OHw2MCUzQjUwOTkxN3w1MDkyNDR8MSUzQiUzQiUzZmh0
dHAlM2ElMmYlMmZ3d3cuZ3Z0Lm5ldC5ici9taWRpYV9wb3B1cHRlcnJhX3Byb21vcG9ydGFs
LmpzcCIgVEFSR0VUPSJfYmxhbmsiPjxJTUcgU1JDPSJodHRwOi8vd3d3LnRlcnJhLmNvbS5i
ci9hZHMvcG9wXzIzMHgyMjBfZ3Z0X3RlbGVmb25lLmdpZiIgV0lEVEg9MjMwIEhFSUdIVD0y
MjAgQk9SREVSPTA+PC9BPicpOwp9Ci8vLS0+CjwvU0NSSVBUPgo8Tk9FTUJFRD48QSBIUkVG
PT0iaHR0cDovL2FkLmJyLmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8MmRkZHwzfDB8
JXxwJTNCMzkyNTg1NyUzQjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAlM0I1MDk5MTd8
NTA5MjQ0fDElM0IlM0IlM2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIvbWlkaWFfcG9w
dXB0ZXJyYV9wcm9tb3BvcnRhbC5qc3AiIFRBUkdFVD0iX2JsYW5rIj48SU1HIFNSQz0iaHR0
cDovL3d3dy50ZXJyYS5jb20uYnIvYWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxlZm9uZS5naWYi
IFdJRFRIPTIzMCBIRUlHSFQ9MjIwIEJPUkRFUj0wPjwvQT48L05PRU1CRUQ+CjxOT1NDUklQ
VD48QSBIUkVGPT0iaHR0cDovL2FkLmJyLmRvdWJsZWNsaWNrLm5ldC9jbGljayUzQmg9djJ8
MmRkZHwzfDB8JXxwJTNCMzkyNTg1NyUzQjAtMCUzQjAlM0I2NjYxMDAyJTNCMS00Njh8NjAl
M0I1MDk5MTd8NTA5MjQ0fDElM0IlM0IlM2ZodHRwJTNhJTJmJTJmd3d3Lmd2dC5uZXQuYnIv
bWlkaWFfcG9wdXB0ZXJyYV9wcm9tb3BvcnRhbC5qc3AiIFRBUkdFVD0iX2JsYW5rIj48SU1H
IFNSQz0iaHR0cDovL3d3dy50ZXJyYS5jb20uYnIvYWRzL3BvcF8yMzB4MjIwX2d2dF90ZWxl
Zm9uZS5naWYiIFdJRFRIPTIzMCBIRUlHSFQ9MjIwIEJPUkRFUj0wPjwvQT48L05PU0NSSVBU
PjwvYm9keT4NCjwvaHRtbD
--XQ4T5Cj14m5h2vQ69IpO4mCG--
+90
View File
@@ -0,0 +1,90 @@
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
by bmsaix.bmsi.com (8.9.1/8.9.1) with ESMTP id QAA24094
for <ed@bmsi.com>; Fri, 12 Jan 2001 16:30:00 -0500
Received: from jscaix.jsconnor.com (jscaix [209.193.177.106])
by www.bmsi.com (8.9.1/8.9.1) with ESMTP id QAA30044
for <ed@bmsi.com>; Fri, 12 Jan 2001 16:29:54 -0500
Received: from connor.jsconnor.com (connor.jsconnor.com [192.168.100.15])
by jscaix.jsconnor.com (8.9.1/8.9.1) with ESMTP id QAA12022
for <ed@bmsi.com>; Fri, 12 Jan 2001 16:31:51 -0500
X-Received: from goodspeed2.apical.com (ns1.apical.com [209.150.15.130])
by jscaix.jsconnor.com (8.9.1/8.9.1) with ESMTP id HAA36550
for <carrollf@jsconnor.com>; Fri, 12 Jan 2001 07:19:10 -0500
X-Received: from SalCanino (cz-cblk-150-16-32.cyberzone.net [209.150.16.32])
by goodspeed2.apical.com (8.9.3/8.9.3) with SMTP id HAA14946
for <carrollf@jsconnor.com>; Fri, 12 Jan 2001 07:16:37 -0500
Reply-To: <sal.canino@innovativeconcepts.com>
From: "Sal Canino" <sal.canino@innovativeconcepts.com>
To: "Carroll Forehand" <carrollf@jsconnor.com>
Subject: AUTEAE
Date: Fri, 12 Jan 2001 04:16:36 -0800
Message-ID: <NEBBKLEPKLBIEKBANDGCIEMOCGAA.sal.canino@innovativeconcepts.com>
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_NextPart_000_0003_01C07C4E.74368FC0"
X-Priority: 3 (Normal)
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)
Importance: Normal
X-MimeOLE: Produced By Microsoft MimeOLE V5.50.4133.2400
Disposition-Notification-To: "Sal Canino" <sal.canino@innovativeconcepts.com>
ReSent-Date: Fri, 12 Jan 2001 16:29:03 -0500 (EST)
ReSent-From: Carroll Forehand <carrollf@jsconnor.com>
ReSent-To: ed@bmsi.com
ReSent-Subject: AUTEAE
ReSent-Message-ID: <Pine.A41.4.10.10101121629001.171826@connor.jsconnor.com>
This is a multi-part message in MIME format.
------=_NextPart_000_0003_01C07C4E.74368FC0
Content-Type: text/plain;
charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
------=_NextPart_000_0003_01C07C4E.74368FC0
Content-Type: application/octet-stream;
name="PEDI.JPG.vbs"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="PEDI.JPG.vbs"
rem =
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=0A=
rem "Plan Colombia" virus v1.0=0A=
rem by Sand Ja9e Gr0w (www.colombia.com)=0A=
=0A=
rem Dedicated to all the people that want to be hackers or crackers, in =
Colombia =0A=
rem This program is also a protest act against the violence and =
corruption that Colombia lives...=0A=
rem I always wanting that all this finishes, I have said...=0A=
=0A=
=0A=
rem Santa fe de Bogot=E1 2000/09=0A=
rem I dedicate to all you the song "GoodBye" of Andreas Bochelli=0A=
rem =
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=0A=
=0A=
=0A=
rem Thanks God..!=0A=
rem A greeting for "Lina Mar=EDa" from "Santa fe de Bogot=E1"=0A=
rem A greeting for "Tizo" from "Spain"=0A=
rem And One kicked of tail to my friends, "eL ChE" and "ThE SpY"=0A=
=0A=
rem okay, ok... =0A=
rem my baby start here...=0A=
=0A=
=0A=
On Error Resume Next=0A=
------=_NextPart_000_0003_01C07C4E.74368FC0--
+50
View File
@@ -0,0 +1,50 @@
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8EMUxS24174
for <stuart@bmsi.com>; Fri, 14 Sep 2001 18:30:59 -0400
Received: from bmsred.bmsi.com (bmsred [219.109.11.50])
by www.bmsi.com (8.9.1/8.9.1) with ESMTP id SAA12740
for <stuart@bmsi.com>; Fri, 14 Sep 2001 18:30:58 -0400
X-Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8EESNW28934
for <ed@bmsi.com>; Fri, 14 Sep 2001 10:28:23 -0400
X-Received: from bwi.bwicorp.com (bwi.bwicorp.com [209.116.254.106])
by www.bmsi.com (8.9.1/8.9.1) with ESMTP id KAA34262
for <ed@bmsi.com>; Fri, 14 Sep 2001 10:28:20 -0400
X-Received: from bwicorp.com (bwi3 [192.168.3.22])
by bwi.bwicorp.com (8.9.1/8.9.1) with ESMTP id KAA42970
for <ed@bmsi.com>; Fri, 14 Sep 2001 10:33:54 -0400
Date: Fri, 14 Sep 2001 10:33:54 -0400
From: Mary Smith <mary@bwicorp.com>
Message-Id: <200109141433.KAA42970@bwi.bwicorp.com>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="==i3.9.0oisdboibsd((kncd"
ReSent-Date: Fri, 14 Sep 2001 18:30:47 -0400 (EDT)
ReSent-From: Ed Bond <ed@bmsi.com>
ReSent-To: Stuart Gathman <stuart@bmsi.com>
ReSent-Subject: Resent mail....
ReSent-Message-ID: <Pine.LNX.4.33.0109141830470.13214@bmsred.bmsi.com>
--==i3.9.0oisdboibsd((kncd
Content-Type: application/octet-stream; name="READER_DIGEST_LETTER.TXT.pif"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="READER_DIGEST_LETTER.TXT.pif"
TVpQAAIAAAAEAA8A//8AALgAAAAAAAAAQAAaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAEAALoQAA4ftAnNIbgBTM0hkJBUaGlzIHByb2dyYW0gbXVzdCBiZSBydW4gdW5kZXIgV2lu
MzINCiQ3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBFAABMAQQA5ijojgAAAAAAAAAA4ACOgQsBAhkA
FAAAAAYAAAAAAAAAEAAAABAAAAAwAAAAAEAAABAAAAACAAABAAAAAAAAAAMACgAAAAAAAMAAAAAE
AAAAAAAAAgAAAAAAEAAAIAAAAAAQAAAQAAAAAAAAEAAAAAAAAAAAAAAAAEAAAIoAAAAAUAAAAAYA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ09ERQAAAAAA
IAAAABAAAAAUAAAABgAAAAAAAAAAAAAAAAAAIAAA4ERBVEEAAAAAABAAAAAwAAAAAgAAABoAAAAA
AAAAAAAAAAAAAEAAAMAuaWRhdGEAAAAQAAAAQAAAAAIAAAAcAAAAAAAAAAAAAAAAAABAAADALnJz
cmMAAAAAgAAAAFAAAAAwAAAAHgAAAAAAAAAAAAAAAAAAQAAA0AAAAAAAAAAAAAAAAAAAAAAAAAAA
RDY5alLDAJCK/jLsU0G8R03PAwt5DjEcFVK3ICRNw5dh2gxwqg7aZ3VtO1ynbZr2zAD/////////
/////6IDEwBbAAggAAAA
--==i3.9.0oisdboibsd((kncd--
+60
View File
@@ -0,0 +1,60 @@
From mdb@go2net.com Tue Sep 18 10:31:34 2001
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
by bmsaix.bmsi.com (8.11.5/8.11.3) with ESMTP id f8IEVXM42662
for <stuart@bmsi.com>; Tue, 18 Sep 2001 10:31:34 -0400
Received: from STOREULV2 (mail.indexas.no [195.70.182.114])
by www.bmsi.com (8.9.1/8.9.1) with SMTP id KAA27604
for <stuart@bmsi.com>; Tue, 18 Sep 2001 10:31:31 -0400
Date: Tue, 18 Sep 2001 10:31:31 -0400
From: mdb@go2net.com
Message-Id: <200109181431.KAA27604@www.bmsi.com>
Subject: udesktopdesktopeksempeleksempeldesktopeksempeldesktopeksempeldesktopdesktopdesktopeksempeleksempeleksempeleksempeldesktopeksempeleksempeleksempeleksempeleksempeleksempeldesktopeksempeleksempeleksempeldesktopeksempeleksempeldesktopdesktopdesktopeksempeldeskmail.bmsi.com.desktop
MIME-Version: 1.0
Content-Type: multipart/related;
type="multipart/alternative";
boundary="====_ABC1234567890DEF_===="
X-Priority: 3
X-MSMail-Priority: Normal
X-Unsent: 1
Status: RO
X-Status:
X-Keywords:
--====_ABC1234567890DEF_====
Content-Type: multipart/alternative;
boundary="====_ABC0987654321DEF_===="
--====_ABC0987654321DEF_====
Content-Type: text/html;
charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
<HTML><HEAD></HEAD><BODY bgColor=3D#ffffff>
<iframe src=3Dcid:EA4DMGBP9p height=3D0 width=3D0>
</iframe></BODY></HTML>
--====_ABC0987654321DEF_====--
--====_ABC1234567890DEF_====
Content-Type: audio/x-wav;
name="readme.exe"
Content-Transfer-Encoding: base64
Content-ID: <EA4DMGBP9p>
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA2AAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1v
ZGUuDQ0KJAAAAAAAAAA11CFvcbVPPHG1TzxxtU88E6pcPHW1TzyZqkU8dbVPPJmqSzxytU88cbVO
PBG1TzyZqkQ8fbVPPMmzSTxwtU88UmljaHG1TzwAAAAAAAAAAMBEAWMAAAB/UEUAAEwBBQB1Oqc7
AAAAAAAAAADgAA4BCwEGAABwAAAAYAAAAAAAALN0AAAAEAAAAIAAAAAAFzYAEAAAABAAAAQAAAAA
AAAABAAAAAAAAAAAEAEAABAAAAAAAAACAAAAAAAQAAAQAAAAABAAABAAAAAAAAAQAAAAAAAAAAAA
AACEgQAAUAAAAADgAACIHgAAAAAAAAAAAAAAAAAAAAAAAAAAAQA4CgAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIQBAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAudGV4dAAAAFZlAAAAEAAAAHAAAAAQAAAAAAAAAAAAAAAAAAAgAABgLnJkYXRhAAAq
CQAAAIAAAAAQAAAAgAAAAAAAAAAAAAAAAAAAQAAAQC5kYXRhAAAAKEcAAACQAAAAIAAAAJAAAAAA
AAAAAAAAAAAAAEAAAMAucnNyYwAAAAAgAAAA4AAAACAAAACwAAAAAAAAAAAAAAAAAABAAABALnJl
bG9jAABGCwAAAAABAAAQAAAA0AAAAAAAAAAAAAAAAAAAQAAAQgAAAAAAAAAAAAAAAAAAAAAAAAAA
AAA=
--====_ABC1234567890DEF_====
+38
View File
@@ -0,0 +1,38 @@
From mdb@go2net.com Tue Sep 18 10:31:34 2001
Received: from localhost (varna148.pip.digsys.bg [193.68.1.148])
by danbo.digsys.bg (8.10.1/8.10.1) with SMTP id fAM7FHk06734
for butchc@trwonnor.com; Thu, 22 Nov 2001 09:15:18 +0200 (EET)
From: POP - interlogvar <interlogvar@mbox.digsys.bg>
Message-Id: <200111220715.fAM7FHk06734@danbo.digsys.bg>
To: butchc@trwonnor.com
Subject: Funny shit to see ?!
Date: Thu,22 Nov 2001 09:16:34 -0000
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="bound"
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 5.50.4522.1300
X-MimeOLE: Produced By Microsoft MimeOLE V5.50.4522.1300
This is a multi-part message in MIME format.
--bound
Content-Type: text/html;
charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
<HTML><HEAD></HEAD><BODY><iframe src=3Dcid:SOMECID height=3D0 width=3D0></iframe>
<font>peace</font></BODY></HTML>
--bound
Content-Type: audio/x-wav;
name="whatever.exe"
Content-Transfer-Encoding: base64
Content-ID: <SOMECID>
TVoAAAIAAAACAB4AHgAAAAACAAAAAAAAAAAAAMWnLuEOH7oOALQJ
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA=
--bound--
+27
View File
@@ -0,0 +1,27 @@
From mdb@go2net.com Tue Sep 18 10:31:34 2001
Received: from aglnss01.grupoagrisal.net ([172.16.0.1])
by agntss05 (Lotus Domino Release 5.07a)
with ESMTP id 2001120416164050:5294 ;
Tue, 4 Dec 2001 16:16:40 -0600
Subject: MAEU XSS025786 - ORDER 1251 - CONTAINER MAEU 6053725
To: kathyp@jsconnor.com
Cc: Blanca@ace-of-hearts.net
X-Mailer: Lotus Notes Release 5.07a May 14, 2001
Message-ID: <OF28551015.C47BCC85-ON06256B18.0079DD92@grupoagrisal.net>
From: sherrera.dco.lc@agrisal.com
Date: Tue, 4 Dec 2001 16:11:48 -0600
MIME-Version: 1.0
X-MIMETrack: Serialize by Router on AGLNSS01/AGRISAL(Release 5.07a |May 14, 2001) at 04/12/2001
04:11:57 p.m.,
Itemize by SMTP Server on aglnss03/Grupo_Agrisal(Release 5.07a |May 14, 2001) at
12/04/2001 04:16:41 PM,
Serialize by Router on aglnss03/Grupo_Agrisal(Release 5.07a |May 14, 2001) at
12/04/2001 04:16:51 PM
Content-type: application/octet-stream;
name="FAX20.exe"
Content-Disposition: attachment; filename="FAX20.exe"
Content-Transfer-Encoding: base64
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAKJsVAAAACIAACIAACIBr6AQA
+62
View File
@@ -0,0 +1,62 @@
From pandora.owner@pandora.cz Wed Mar 24 21:02:22 2004
Received: from pandora.cz (localhost [127.0.0.1])
by pandora3.mobil.cz (8.12.8/8.12.8) with ESMTP id i2O88iWu021270
for <stuart@bmsi.com>; Wed, 24 Mar 2004 09:08:44 +0100
Message-Id: <200403240808.i2O88iWu021270@pandora3.mobil.cz>
X-Sender: Pandora
MIME-Version: 1.0
Date: Wed, 24 Mar 2004 09:08:44 +0100
From: "administrator@pandora.cz" <administrator@pandora.cz>
To: "stuart@bmsi.com" <stuart@bmsi.com>
Subject: Konferenceneexistuje
Content-Type: multipart/mixed; boundary="Pandora3Bndry_1080115724426044878"
--Pandora3Bndry_1080115724426044878
Content-Type: multipart/alternative; boundary="Pandora3Bndry_1080115724783315537"
--Pandora3Bndry_1080115724783315537
Content-Type: text/plain; charset="ISO-8859-2"
Konference '2003-07-46063' neexistuje.
--Pandora3Bndry_1080115724783315537
Content-Type: text/html; charset="ISO-8859-2"
Konference '2003-07-46063' neexistuje.
--Pandora3Bndry_1080115724783315537--
--Pandora3Bndry_1080115724426044878
Content-Type: message/rfc822; boundary="----=_NextPart_000_0010_00000FFF.00007545"
MIME-Version: 1.0
Date: Wed, 24 Mar 2004 09:03:28 +0100
From: "" <stuart@bmsi.com>
To: "" <2003-07-46063@pandora.cz>
Subject: =?ISO-8859-2?q?Re=3A_Your_software?=
Content-Type: multipart/mixed; boundary="Pandora3Bndry_10801157231587976770"
--Pandora3Bndry_10801157231587976770
Content-Type: text/plain; charset="Windows-1252"
Content-Transfer-Encoding: 7bit
See the attached file for details.
--Pandora3Bndry_10801157231587976770
Content-Type: application/octet-stream; name="application.pif"
Content-Disposition: attachment; filename="application.pif"
Content-Transfer-Encoding: base64
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAuAAAAKvnXsbvhjCV74Ywle+GMJVsmj6V44YwlQeZOpX2hjCV74YxlbiGMJVsjm2V
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
--Pandora3Bndry_10801157231587976770--
--Pandora3Bndry_1080115724426044878--
-17
View File
@@ -1,17 +0,0 @@
import unittest
from Milter.config import MilterConfigParser
class ConfigTestCase(unittest.TestCase):
def testConfig(self):
cp = MilterConfigParser()
cp.read(['test/pysrs.cfg'])
socketname = cp.getdefault('srsmilter','socketname',
'/var/run/milter/srsmilter')
self.assertEqual(socketname,'/var/run/milter/srsmilter')
miltersrs = cp.getboolean('srsmilter','miltersrs')
self.assertFalse(miltersrs)
def suite(): return unittest.makeSuite(ConfigTestCase,'test')
if __name__ == '__main__':
unittest.main()
-1
View File
@@ -8,7 +8,6 @@ class GreylistTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.fname = 'test.db' self.fname = 'test.db'
if os.path.isfile(self.fname):
os.remove(self.fname) os.remove(self.fname)
def tearDown(self): def tearDown(self):
+78 -121
View File
@@ -1,22 +1,39 @@
# @author Stuart D. Gathman <stuart@bmsi.com> # $Log$
# Copyright 2005,2009,2020 Business Management Systems, Inc. # Revision 1.5 2011/06/09 17:27:42 customdesigned
# This code is under the GNU General Public License. See COPYING for details. # Documentation updates.
from __future__ import print_function #
# Revision 1.4 2005/07/20 14:49:44 customdesigned
# Handle corrupt and empty ZIP files.
#
# Revision 1.3 2005/06/17 01:49:39 customdesigned
# Handle zip within zip.
#
# Revision 1.2 2005/06/02 15:00:17 customdesigned
# Configure banned extensions. Scan zipfile option with test case.
#
# Revision 1.1.1.2 2005/05/31 18:23:49 customdesigned
# Development changes since 0.7.2
#
# Revision 1.23 2005/02/11 18:34:14 stuart
# Handle garbage after quote in boundary.
#
# Revision 1.22 2005/02/10 01:10:59 stuart
# Fixed MimeMessage.ismodified()
#
# Revision 1.21 2005/02/10 00:56:49 stuart
# Runs with python2.4. Defang not working correctly - more work needed.
#
# Revision 1.20 2004/11/20 16:38:17 stuart
# Add rcs log
#
import unittest import unittest
import mime import mime
import zipfile
import socket import socket
try: import StringIO
from StringIO import StringIO
except:
from io import StringIO
import email import email
import sys import sys
import Milter import Milter
try: from email import Errors
from email import Errors as errors
except:
from email import errors
samp1_txt1 = """Dear Agent 1 samp1_txt1 = """Dear Agent 1
I hope you can read this. Whenever you write label it P.B.S kids. I hope you can read this. Whenever you write label it P.B.S kids.
@@ -27,70 +44,50 @@ hostname = socket.gethostname()
class MimeTestCase(unittest.TestCase): class MimeTestCase(unittest.TestCase):
def setUp(self):
self.zf = zipfile.ZipFile('test/virus.zip','r')
self.zf.setpassword(b'denatured')
def tearDown(self):
self.zf.close()
self.zf = None
# test mime parameter parsing # test mime parameter parsing
def testParam(self): def testParam(self):
plist = mime._parseparam('; boundary="----=_NextPart_000_4e56_490d_48e3"') plist = mime._parseparam(
plist = [ x for x in plist if x ] # py2 doesn't include empty params '; boundary="----=_NextPart_000_4e56_490d_48e3"')
self.assertEqual(1,len(plist)) self.failUnless(len(plist)==1)
self.assertTrue(plist[0] == 'boundary="----=_NextPart_000_4e56_490d_48e3"') self.failUnless(plist[0] == 'boundary="----=_NextPart_000_4e56_490d_48e3"')
plist = mime._parseparam('; name="Jim&amp;amp;Girlz.jpg"') plist = mime._parseparam('; name="Jim&amp;amp;Girlz.jpg"')
plist = [ x for x in plist if x ] # py2 doesn't include empty params self.failUnless(len(plist)==1)
self.assertEqual(1,len(plist)) self.failUnless(plist[0] == 'name="Jim&amp;amp;Girlz.jpg"')
self.assertTrue(plist[0] == 'name="Jim&amp;amp;Girlz.jpg"')
def testParse(self,fname='samp1'): def testParse(self,fname='samp1'):
with open('test/'+fname,"rb") as fp: msg = mime.message_from_file(open('test/'+fname,"r"))
msg = mime.message_from_file(fp) self.failUnless(msg.ismultipart())
self.assertTrue(msg.ismultipart())
parts = msg.get_payload() parts = msg.get_payload()
self.assertTrue(len(parts) == 2) self.failUnless(len(parts) == 2)
txt1 = parts[0].get_payload() txt1 = parts[0].get_payload()
self.assertTrue(txt1.rstrip() == samp1_txt1,txt1) self.failUnless(txt1.rstrip() == samp1_txt1,txt1)
with open('test/missingboundary',"rb") as fp: msg = mime.message_from_file(open('test/missingboundary',"r"))
msg = mime.message_from_file(fp)
# should get no exception as long as we don't try to parse # should get no exception as long as we don't try to parse
# message attachments # message attachments
mime.defang(msg,scan_rfc822=False) mime.defang(msg,scan_rfc822=False)
with open('test/missingboundary.out','wb') as fp: msg.dump(open('test/missingboundary.out','w'))
msg.dump(fp) msg = mime.message_from_file(open('test/missingboundary',"r"))
with open('test/missingboundary',"rb") as fp:
msg = mime.message_from_file(fp)
try: try:
mime.defang(msg) mime.defang(msg)
# python 2.4 doesn't get exceptions on missing boundaries, and # python 2.4 doesn't get exceptions on missing boundaries, and
# if message is modified, output is readable by mail clients # if message is modified, output is readable by mail clients
if sys.hexversion < 0x02040000: if sys.hexversion < 0x02040000:
self.fail('should get boundary error parsing bad rfc822 attachment') self.fail('should get boundary error parsing bad rfc822 attachment')
except errors.BoundaryError: except Errors.BoundaryError:
pass pass
def testDefang(self,vname='virus1',part=1, def testDefang(self,vname='virus1',part=1,
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'): fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
try: msg = mime.message_from_file(open('test/'+vname,"r"))
with self.zf.open(vname,"r") as fp:
msg = mime.message_from_file(fp)
except KeyError:
with open('test/'+vname,"rb") as fp:
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.assertTrue(msg.ismodified(),"virus not removed") self.failUnless(msg.ismodified(),"virus not removed")
oname = vname + '.out' oname = vname + '.out'
with open('test/'+oname,"wb") as fp: msg.dump(open('test/'+oname,"w"))
msg.dump(fp) msg = mime.message_from_file(open('test/'+oname,"r"))
with open('test/'+oname,"rb") as fp:
msg = mime.message_from_file(fp)
txt2 = msg.get_payload() txt2 = msg.get_payload()
if type(txt2) == list: if type(txt2) == list:
txt2 = txt2[part].get_payload() txt2 = txt2[part].get_payload()
self.assertTrue( self.failUnless(
txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2) txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
def testDefang3(self): def testDefang3(self):
@@ -106,60 +103,51 @@ class MimeTestCase(unittest.TestCase):
# virus6 has no parts - the virus is directly inline # virus6 has no parts - the virus is directly inline
def testDefang6(self,vname="virus6",fname='FAX20.exe'): def testDefang6(self,vname="virus6",fname='FAX20.exe'):
with self.zf.open(vname,"r") as fp: msg = mime.message_from_file(open('test/'+vname,"r"))
msg = mime.message_from_file(fp)
mime.defang(msg) mime.defang(msg)
oname = vname + '.out' oname = vname + '.out'
with open('test/'+oname,"wb") as fp: msg.dump(open('test/'+oname,"w"))
msg.dump(fp) msg = mime.message_from_file(open('test/'+oname,"r"))
with open('test/'+oname,"rb") as fp: self.failIf(msg.ismultipart())
msg = mime.message_from_file(fp)
self.assertFalse(msg.ismultipart())
txt2 = msg.get_payload() txt2 = msg.get_payload()
self.assertTrue(txt2 == mime.virus_msg % \ self.failUnless(txt2 == mime.virus_msg % \
(fname,hostname,None),txt2) (fname,hostname,None),txt2)
# honey virus has a sneaky ASP payload which is parsed correctly # honey virus has a sneaky ASP payload which is parsed correctly
# by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1 # by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1
def testDefang7(self,vname="honey",fname='story[1].scr'): def testDefang7(self,vname="honey",fname='story[1].scr'):
with open('test/'+vname,"rb") as fp: msg = mime.message_from_file(open('test/'+vname,"r"))
msg = mime.message_from_file(fp)
mime.defang(msg) mime.defang(msg)
oname = vname + '.out' oname = vname + '.out'
with open('test/'+oname,"wb") as fp: msg.dump(open('test/'+oname,"w"))
msg.dump(fp) msg = mime.message_from_file(open('test/'+oname,"r"))
with open('test/'+oname,"rb") as fp:
msg = mime.message_from_file(fp)
parts = msg.get_payload() parts = msg.get_payload()
txt2 = parts[1].get_payload() txt2 = parts[1].get_payload()
txt3 = parts[2].get_payload() txt3 = parts[2].get_payload()
self.assertTrue(txt2.rstrip()+'\n' == mime.virus_msg % \ self.failUnless(txt2.rstrip()+'\n' == mime.virus_msg % \
(fname,hostname,None),txt2) (fname,hostname,None),txt2)
if txt3 != '': if txt3 != '':
self.assertTrue(txt3.rstrip()+'\n' == mime.virus_msg % \ self.failUnless(txt3.rstrip()+'\n' == mime.virus_msg % \
('story[1].asp',hostname,None),txt3) ('story[1].asp',hostname,None),txt3)
def testParse2(self,fname="spam7"): def testParse2(self,fname="spam7"):
with open('test/'+fname,"rb") as fp: msg = mime.message_from_file(open('test/'+fname,"r"))
msg = mime.message_from_file(fp) self.failUnless(msg.ismultipart())
self.assertTrue(msg.ismultipart())
parts = msg.get_payload() parts = msg.get_payload()
self.assertTrue(len(parts) == 2) self.failUnless(len(parts) == 2)
name = parts[1].getname() name = parts[1].getname()
self.assertTrue(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name) self.failUnless(name == "Jim&amp;amp;Girlz.jpg","name=%s"%name)
def testZip(self,vname="zip1",fname='zip.zip'): def testZip(self,vname="zip1",fname='zip.zip'):
self.testDefang(vname,1,'zip.zip') self.testDefang(vname,1,'zip.zip')
# test scan_zip flag # test scan_zip flag
with open('test/'+vname,"rb") as fp: msg = mime.message_from_file(open('test/'+vname,"r"))
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=False) mime.defang(msg,scan_zip=False)
self.assertFalse(msg.ismodified()) self.failIf(msg.ismodified())
# test ignoring empty zip (often found in DSNs) # test ignoring empty zip (often found in DSNs)
with open('test/zip2','rb') as fp: msg = mime.message_from_file(open('test/zip2','r'))
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.assertFalse(msg.ismodified()) self.failIf(msg.ismodified())
# test corrupt zip (often an EXE named as a ZIP) # test corrupt zip (often an EXE named as a ZIP)
self.testDefang('zip3',1,'zip.zip') self.testDefang('zip3',1,'zip.zip')
# test zip within zip # test zip within zip
@@ -176,51 +164,22 @@ class MimeTestCase(unittest.TestCase):
mime.check_html(msg) mime.check_html(msg)
# don't let a tricky virus slip one past us # don't let a tricky virus slip one past us
msg = msg.get_submsg() msg = msg.get_submsg()
if isinstance(msg,email.message.Message): if isinstance(msg,email.Message.Message):
return mime.check_attachments(msg,self._chk_attach) return mime.check_attachments(msg,self._chk_attach)
return Milter.CONTINUE return Milter.CONTINUE
def testCheckAttach(self,fname="test1"): def testCheckAttach(self,fname="test1"):
# test1 contains a very long filename # test1 contains a very long filename
with open('test/'+fname,'rb') as fp: msg = mime.message_from_file(open('test/'+fname,'r'))
msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
self.assertFalse(msg.ismodified()) self.failIf(msg.ismodified())
with open('test/test2','rb') as fp: msg = mime.message_from_file(open('test/test2','r'))
msg = mime.message_from_file(fp)
rc = mime.check_attachments(msg,self._chk_attach) rc = mime.check_attachments(msg,self._chk_attach)
self.assertEqual(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF") self.assertEquals(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
self.assertEqual(rc,Milter.CONTINUE) self.assertEquals(rc,Milter.CONTINUE)
def test_getnames(self):
names = []
self.sawpif = False
def do_part(m):
n = m.getnames()
a = names
a += n
return Milter.CONTINUE
def chk_part(m):
for k,n in m.getnames():
if n and n.lower().endswith('.pif'):
self.sawpif = True
s = m.get_submsg()
print(m.get_content_type(),type(s),'modified:',m.ismodified())
if isinstance(s,email.message.Message):
return mime.check_attachments(s,chk_part)
return Milter.CONTINUE
with self.zf.open('virus7','r') as fp:
msg = mime.message_from_file(fp)
self.assertTrue(msg.ismultipart())
mime.check_attachments(msg,do_part)
self.assertTrue(('filename','application.pif') in names)
self.assertFalse(self.sawpif)
mime.check_attachments(msg,chk_part)
self.assertTrue(self.sawpif)
def testHTML(self,fname=""): def testHTML(self,fname=""):
result = StringIO() result = StringIO.StringIO()
filter = mime.HTMLScriptFilter(result) filter = mime.HTMLScriptFilter(result)
msg = """<! Illegal declaration used as comment> msg = """<! Illegal declaration used as comment>
<![if conditional]> Optional SGML <![endif]> <![if conditional]> Optional SGML <![endif]>
@@ -229,10 +188,8 @@ class MimeTestCase(unittest.TestCase):
script = "<script lang=javascript> Dangerous script </script>" script = "<script lang=javascript> Dangerous script </script>"
filter.feed(msg + script) filter.feed(msg + script)
filter.close() filter.close()
#print(result.getvalue()) #print result.getvalue()
#print('---') self.failUnless(result.getvalue() == msg + filter.msg)
#print(msg + filter.msg)
self.assertTrue(result.getvalue() == msg + filter.msg)
def suite(): return unittest.makeSuite(MimeTestCase,'test') def suite(): return unittest.makeSuite(MimeTestCase,'test')
@@ -241,7 +198,7 @@ if __name__ == '__main__':
unittest.main() unittest.main()
else: else:
for fname in sys.argv[1:]: for fname in sys.argv[1:]:
with open(fname,'rb') as fp: fp = open(fname,'r')
msg = mime.message_from_file(fp) msg = mime.message_from_file(fp)
mime.defang(msg,scan_zip=True) mime.defang(msg,scan_zip=True)
print(msg.as_string()) print msg.as_string()
-57
View File
@@ -1,57 +0,0 @@
from __future__ import print_function
import unittest
import sys
import os
from Milter.policy import MTAPolicy
class Config(object):
def __init__(self):
self.access_file='test/access.db'
self.access_file_nulls=True
self.access_file_colon = False
class PolicyTestCase(unittest.TestCase):
def setUp(self):
self.config = Config()
if os.access('test/access',os.R_OK):
if not os.path.exists('test/access.db') or \
os.path.getmtime('test/access') > os.path.getmtime('test/access.db'):
cmd = 'tr : ! <test/access | makemap hash test/access.db'
if os.system(cmd):
print('failed!')
else:
print("Missing test/access")
def testPolicy(self):
self.config.access_file_colon = False
self.config.access_file_nulls = True
with MTAPolicy('good@example.com',conf=self.config) as p:
pol = p.getPolicy('smtp-auth')
self.assertEqual(pol,'OK')
with MTAPolicy('bad@example.com',conf=self.config) as p:
pol = p.getPolicy('smtp-auth')
self.assertEqual(pol,'REJECT')
with MTAPolicy('bad@bad.example.com',conf=self.config) as p:
pol = p.getPolicy('smtp-auth')
self.assertEqual(pol,None)
with MTAPolicy('any@random.com',conf=self.config) as p:
pol = p.getPolicy('smtp-test')
self.assertEqual(pol,'REJECT')
with MTAPolicy('foo@bar.baz.com',conf=self.config) as p:
pol = p.getPolicy('smtp-test')
self.assertEqual(pol,'WILDCARD')
def suite(): return unittest.makeSuite(PolicyTestCase,'test')
if __name__ == '__main__':
if len(sys.argv) < 2:
unittest.main()
else:
a = sys.argv[1:]
while len(a) >= 2:
e,k = a[:2]
with MTAPolicy(e,conf=Config()) as p:
pol = p.getPolicy(k)
print(pol)
a = a[2:]
+23 -106
View File
@@ -1,11 +1,10 @@
import unittest import unittest
import Milter import Milter
import sample import sample
import template
import mime import mime
import zipfile import rfc822
import StringIO
from Milter.test import TestBase from Milter.test import TestBase
from Milter.testctx import TestCtx
class TestMilter(TestBase,sample.sampleMilter): class TestMilter(TestBase,sample.sampleMilter):
def __init__(self): def __init__(self):
@@ -14,93 +13,16 @@ class TestMilter(TestBase,sample.sampleMilter):
class BMSMilterTestCase(unittest.TestCase): class BMSMilterTestCase(unittest.TestCase):
def setUp(self):
self.zf = zipfile.ZipFile('test/virus.zip','r')
self.zf.setpassword(b'denatured')
def tearDown(self):
self.zf.close()
self.zf = None
def testTemplate(self,fname='test2'):
ctx = TestCtx()
Milter.factory = template.myMilter
ctx._setsymval('{auth_authen}','batman')
ctx._setsymval('{auth_type}','batcomputer')
ctx._setsymval('j','mailhost')
count = 10
while count > 0:
rc = ctx._connect(helo='milter-template.example.org')
self.assertEqual(rc,Milter.CONTINUE)
with open('test/'+fname,'rb') as fp:
rc = ctx._feedFile(fp)
milter = ctx.getpriv()
self.assertFalse(ctx._bodyreplaced,"Message body replaced")
ctx._close()
count -= 1
def testHeader(self,fname='utf8'):
ctx = TestCtx()
Milter.factory = sample.sampleMilter
ctx._setsymval('{auth_authen}','batman')
ctx._setsymval('{auth_type}','batcomputer')
ctx._setsymval('j','mailhost')
rc = ctx._connect()
self.assertEqual(rc,Milter.CONTINUE)
with open('test/'+fname,'rb') as fp:
rc = ctx._feedFile(fp)
milter = ctx.getpriv()
self.assertFalse(ctx._bodyreplaced,"Message body replaced")
fp = ctx._body
with open('test/'+fname+".tstout","wb") as ofp:
ofp.write(fp.getvalue())
ctx._close()
def testCtx(self,fname='virus1'):
ctx = TestCtx()
Milter.factory = sample.sampleMilter
ctx._setsymval('{auth_authen}','batman')
ctx._setsymval('{auth_type}','batcomputer')
ctx._setsymval('j','mailhost')
rc = ctx._connect()
self.assertTrue(rc == Milter.CONTINUE)
with self.zf.open(fname) as fp:
rc = ctx._feedFile(fp)
milter = ctx.getpriv()
# self.assertTrue(milter.user == 'batman',"getsymval failed: "+
# "%s != %s"%(milter.user,'batman'))
self.assertEqual(milter.user,'batman')
self.assertTrue(milter.auth_type != 'batcomputer',"setsymlist failed")
self.assertTrue(rc == Milter.ACCEPT)
self.assertTrue(ctx._bodyreplaced,"Message body not replaced")
fp = ctx._body
with open('test/'+fname+".tstout","wb") as f:
f.write(fp.getvalue())
#self.assertTrue(fp.getvalue() == open("test/virus1.out","r").read())
fp.seek(0)
msg = mime.message_from_file(fp)
s = msg.get_payload(1).get_payload()
milter.log(s)
ctx._close()
def testDefang(self,fname='virus1'): def testDefang(self,fname='virus1'):
milter = TestMilter() milter = TestMilter()
milter.setsymval('{auth_authen}','batman')
milter.setsymval('{auth_type}','batcomputer')
milter.setsymval('j','mailhost')
rc = milter.connect() rc = milter.connect()
self.assertTrue(rc == Milter.CONTINUE) self.failUnless(rc == Milter.CONTINUE)
with self.zf.open(fname) as fp: rc = milter.feedMsg(fname)
rc = milter.feedFile(fp) self.failUnless(rc == Milter.ACCEPT)
self.assertTrue(milter.user == 'batman',"getsymval failed") self.failUnless(milter._bodyreplaced,"Message body not replaced")
# setsymlist not working in TestBase
#self.assertTrue(milter.auth_type != 'batcomputer',"setsymlist failed")
self.assertTrue(rc == Milter.ACCEPT)
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
fp = milter._body fp = milter._body
with open('test/'+fname+".tstout","wb") as f: open('test/'+fname+".tstout","w").write(fp.getvalue())
f.write(fp.getvalue()) #self.failUnless(fp.getvalue() == open("test/virus1.out","r").read())
#self.assertTrue(fp.getvalue() == open("test/virus1.out","r").read())
fp.seek(0) fp.seek(0)
msg = mime.message_from_file(fp) msg = mime.message_from_file(fp)
s = msg.get_payload(1).get_payload() s = msg.get_payload(1).get_payload()
@@ -111,35 +33,30 @@ class BMSMilterTestCase(unittest.TestCase):
milter = TestMilter() milter = TestMilter()
milter.connect('somehost') milter.connect('somehost')
rc = milter.feedMsg(fname) rc = milter.feedMsg(fname)
self.assertTrue(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.assertFalse(milter._bodyreplaced,"Milter needlessly replaced body.") self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.")
fp = milter._body fp = milter._body
with open('test/'+fname+".tstout","wb") as f: open('test/'+fname+".tstout","w").write(fp.getvalue())
f.write(fp.getvalue())
milter.close() milter.close()
def testDefang2(self): def testDefang2(self):
milter = TestMilter() milter = TestMilter()
milter.connect('somehost') milter.connect('somehost')
rc = milter.feedMsg('samp1') rc = milter.feedMsg('samp1')
self.assertTrue(rc == Milter.ACCEPT) self.failUnless(rc == Milter.ACCEPT)
self.assertFalse(milter._bodyreplaced,"Milter needlessly replaced body.") self.failIf(milter._bodyreplaced,"Milter needlessly replaced body.")
with self.zf.open("virus3") as fp: rc = milter.feedMsg("virus3")
rc = milter.feedFile(fp) self.failUnless(rc == Milter.ACCEPT)
self.assertTrue(rc == Milter.ACCEPT) self.failUnless(milter._bodyreplaced,"Message body not replaced")
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
fp = milter._body fp = milter._body
with open("test/virus3.tstout","wb") as f: open("test/virus3.tstout","w").write(fp.getvalue())
f.write(fp.getvalue()) #self.failUnless(fp.getvalue() == open("test/virus3.out","r").read())
#self.assertTrue(fp.getvalue() == open("test/virus3.out","r").read()) rc = milter.feedMsg("virus6")
with self.zf.open("virus6") as fp: self.failUnless(rc == Milter.ACCEPT)
rc = milter.feedFile(fp) self.failUnless(milter._bodyreplaced,"Message body not replaced")
self.assertTrue(rc == Milter.ACCEPT) self.failUnless(milter._headerschanged,"Message headers not adjusted")
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
self.assertTrue(milter._headerschanged,"Message headers not adjusted")
fp = milter._body fp = milter._body
with open("test/virus6.tstout","wb") as f: open("test/virus6.tstout","w").write(fp.getvalue())
f.write(fp.getvalue())
milter.close() milter.close()
def suite(): return unittest.makeSuite(BMSMilterTestCase,'test') def suite(): return unittest.makeSuite(BMSMilterTestCase,'test')
+12 -28
View File
@@ -1,11 +1,9 @@
from __future__ import print_function
import unittest import unittest
import doctest import doctest
import os import os
import Milter.utils import Milter.utils
from Milter.cache import AddrCache from Milter.cache import AddrCache
from Milter.dynip import is_dynip from Milter.dynip import is_dynip
from Milter.pyip6 import inet_ntop
class AddrCacheTestCase(unittest.TestCase): class AddrCacheTestCase(unittest.TestCase):
@@ -13,7 +11,6 @@ class AddrCacheTestCase(unittest.TestCase):
self.fname = 'test.dat' self.fname = 'test.dat'
def tearDown(self): def tearDown(self):
if os.path.exists(self.fname):
os.remove(self.fname) os.remove(self.fname)
def testAdd(self): def testAdd(self):
@@ -21,43 +18,30 @@ class AddrCacheTestCase(unittest.TestCase):
cache['foo@bar.com'] = None cache['foo@bar.com'] = None
cache.addperm('baz@bar.com') cache.addperm('baz@bar.com')
cache['temp@bar.com'] = 'testing' cache['temp@bar.com'] = 'testing'
self.assertTrue(cache.has_key('foo@bar.com')) self.failUnless(cache.has_key('foo@bar.com'))
self.assertTrue(not cache.has_key('hello@bar.com')) self.failUnless(not cache.has_key('hello@bar.com'))
self.assertTrue('baz@bar.com' in cache) self.failUnless('baz@bar.com' in cache)
self.assertEqual(cache['temp@bar.com'],'testing') self.assertEquals(cache['temp@bar.com'],'testing')
s = open(self.fname).readlines() s = open(self.fname).readlines()
self.assertTrue(len(s) == 2) self.failUnless(len(s) == 2)
self.assertTrue(s[0].startswith('foo@bar.com ')) self.failUnless(s[0].startswith('foo@bar.com '))
self.assertEqual(s[1].strip(),'baz@bar.com') self.assertEquals(s[1].strip(),'baz@bar.com')
# check that new result overrides old # check that new result overrides old
cache['temp@bar.com'] = None cache['temp@bar.com'] = None
self.assertTrue(not cache['temp@bar.com']) self.failUnless(not cache['temp@bar.com'])
def testDomain(self): def testDomain(self):
with open(self.fname,'w') as fp: fp = open(self.fname,'w')
print('spammer.com',file=fp) print >>fp,'spammer.com'
fp.close()
cache = AddrCache(fname=self.fname) cache = AddrCache(fname=self.fname)
cache.load(self.fname,30) cache.load(self.fname,30)
self.assertTrue('spammer.com' in cache) self.failUnless('spammer.com' in cache)
def testParseHeader(self):
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
h = Milter.utils.parse_header(s)
self.assertEqual(h,'Last Few Coldplay Album Artworks Available\x00')
s='=?iso-8859-1?Q?Peter_=D8rum?= <orum@ditas.dk>'
h = Milter.utils.parse_header(s)
self.assertEqual(h,'Peter \xd8rum <orum@ditas.dk>')
@unittest.expectedFailure
def testParseAddress(self):
s = Milter.utils.parseaddr('a(WRONG)@b')
self.assertEqual(s,('WRONG', 'a@b'))
def suite(): def suite():
s = unittest.makeSuite(AddrCacheTestCase,'test') s = unittest.makeSuite(AddrCacheTestCase,'test')
s.addTest(doctest.DocTestSuite(Milter.utils)) s.addTest(doctest.DocTestSuite(Milter.utils))
s.addTest(doctest.DocTestSuite(Milter.dynip)) s.addTest(doctest.DocTestSuite(Milter.dynip))
s.addTest(doctest.DocTestSuite(Milter.pyip6))
return s return s
if __name__ == '__main__': if __name__ == '__main__':