Compare commits
356 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a776a58240 | |||
| 5330047902 | |||
| a8f373ea65 | |||
| f357be1e99 | |||
| 84eeecf9a6 | |||
| a180b212c6 | |||
| bd0df5d77a | |||
| 34746823f7 | |||
| baeddd9fa5 | |||
| 4854f95b59 | |||
| 242f2fa78f | |||
| 1e0324399b | |||
| 078d9f2078 | |||
| ff06b5f1b4 | |||
| dd581f5d9a | |||
| 3fb9beb5c0 | |||
| b12c4c9746 | |||
| f3fbb1c99d | |||
| 27887daf3f | |||
| 23defb880b | |||
| 7502c29e47 | |||
| 594d3ad365 | |||
| b2e0b2ebc6 | |||
| 04a241f1e9 | |||
| 16bfe5d4da | |||
| 70d19001c0 | |||
| 0d001dd8e9 | |||
| 8f4a82794c | |||
| de0ec3430d | |||
| c9e32e4b06 | |||
| 83a1762515 | |||
| feb6526cb8 | |||
| 3a3add814e | |||
| 1ba522e501 | |||
| a43649f2ce | |||
| de679b1514 | |||
| b946759857 | |||
| f6702e39dd | |||
| 5a8aaf85d7 | |||
| 720db3d7bd | |||
| a46627959c | |||
| 4e0d3da07d | |||
| 53c7519922 | |||
| b3d6328167 | |||
| 2133942c19 | |||
| eef3cde27e | |||
| 5290bc0668 | |||
| 92ad624c3b | |||
| 7c5899b0cd | |||
| c6ccea9099 | |||
| eea110d120 | |||
| 4b2c08c0cf | |||
| 953e8a61fa | |||
| fa4408540e | |||
| 65986632de | |||
| e44321561b | |||
| 344ee43f22 | |||
| 99bf3209c6 | |||
| 2848a090e3 | |||
| c29a21d2dd | |||
| 25a02d9de2 | |||
| c20e82e3d4 | |||
| a3889189f0 | |||
| f86bda2ba4 | |||
| 3ed14cc6ab | |||
| aeff1f8ab5 | |||
| a7bd7b71d8 | |||
| 939fc61df7 | |||
| f6a3b57fb9 | |||
| 3428477eca | |||
| 144fe264c4 | |||
| a3530d4c49 | |||
| 307c54e1b1 | |||
| 66f8a1d437 | |||
| 73e1f469ce | |||
| 2e45d6e187 | |||
| 6a1996117c | |||
| 77c0ce6b2e | |||
| 7311f65150 | |||
| 84bd61aac1 | |||
| 372fad6ac9 | |||
| 60963b3c37 | |||
| 6221f8b753 | |||
| 344ecc7c07 | |||
| ee14614c3e | |||
| 4bb2403223 | |||
| d58546930a | |||
| f8efbb23df | |||
| 26b006455e | |||
| 9b7ca633f3 | |||
| 5928e99520 | |||
| 6d3833da72 | |||
| 2937935fea | |||
| 31aa39034b | |||
| cb31963492 | |||
| ed17f9cecf | |||
| 0e1a2de41f | |||
| 9f419e3fc8 | |||
| 6913fd3e66 | |||
| 780ac63ebe | |||
| b51c08ba3a | |||
| 2e7805e531 | |||
| b1eae98453 | |||
| 9118364164 | |||
| 577c0bd134 | |||
| a97dbb8fd9 | |||
| df036eb55f | |||
| 7eede7ae31 | |||
| 37d4f99aaf | |||
| f55ddbce83 | |||
| 30f4c27c45 | |||
| 67cb78ded5 | |||
| a1bbc31b11 | |||
| 14b95998c9 | |||
| 368ffd5374 | |||
| f12bcf9af9 | |||
| 394e7c6b8e | |||
| 66314dc675 | |||
| dad2f4f087 | |||
| bc88a64d9b | |||
| a5078a6eb1 | |||
| 96f5b6e9dc | |||
| 1c4878963b | |||
| f8e1c15ccd | |||
| c86ad6f68c | |||
| 0d1f2b7f4d | |||
| d4cafcd435 | |||
| d64aad95c1 | |||
| f9ed6f7194 | |||
| 93e9644574 | |||
| d86b9f7312 | |||
| cbf69f596b | |||
| 5b84d454da | |||
| e5bf1aee09 | |||
| 5df3a80f7b | |||
| df67ee9147 | |||
| 593384d610 | |||
| 1280f1360e | |||
| 3e1e528abe | |||
| 04ce8f81b9 | |||
| bc390e69b9 | |||
| c07ed917ab | |||
| a14d676fb6 | |||
| 600e3dfbfb | |||
| 8cfa03bbc4 | |||
| 28a0e551bd | |||
| be3f463450 | |||
| a420148b1e | |||
| f4465ea816 | |||
| 1845876665 | |||
| cee6bc3bea | |||
| 71403de50e | |||
| 017784b5a7 | |||
| 632e7b4248 | |||
| 10f4f2613e | |||
| 69369c3b2a | |||
| 5386e08ca5 | |||
| d0fe3b0b84 | |||
| 670e97cb79 | |||
| 6397b7027f | |||
| 94ce032559 | |||
| 91230381cb | |||
| 46ed3ddbcb | |||
| 6048fe6e8c | |||
| d225384829 | |||
| a84f6aa574 | |||
| 344e8f0a0a | |||
| 1fa4b72c84 | |||
| 021ea96748 | |||
| a490e79564 | |||
| 33e8f7c4cc | |||
| 6bbb6b3f02 | |||
| 6577e40bfb | |||
| 04eeeab2e1 | |||
| cdfeb2d792 | |||
| 46545cab94 | |||
| 9a8fdcb120 | |||
| 218f5168bc | |||
| ddbb8ac3ea | |||
| a2215124bb | |||
| e505d2bb28 | |||
| 9f40f265cd | |||
| 20a875b84d | |||
| 1da5ca54b5 | |||
| bac593f05d | |||
| dbba488d58 | |||
| 6936b599fe | |||
| cee38f8149 | |||
| 188e8256f3 | |||
| 4013365a3d | |||
| e571ccc5a5 | |||
| f65294b470 | |||
| b2d8e838a2 | |||
| f136e973dc | |||
| d289822f42 | |||
| 806aa5a6de | |||
| e84a803cc1 | |||
| 20612240f3 | |||
| c9e6bb68d9 | |||
| 4d69b8fbfe | |||
| 21e3c6f489 | |||
| 83529320ae | |||
| e5685c6035 | |||
| 4c72135b0e | |||
| 393aa6140a | |||
| 2a6a68230b | |||
| 279c831a8e | |||
| c0aa632e16 | |||
| a875ac7834 | |||
| 9f8cef5ee2 | |||
| 4b0e7b22da | |||
| 40fb05b0e3 | |||
| 8ae7bd4217 | |||
| 139e141e1e | |||
| 8932dc36db | |||
| bda654b7a0 | |||
| 09b671f47b | |||
| 732e7317f1 | |||
| 702ec2d4ca | |||
| 7bbff66000 | |||
| 5ad6d321bd | |||
| d01dc65f39 | |||
| b703031c7e | |||
| 1bc0a4faef | |||
| 2bea6ad76f | |||
| c9f0c94b92 | |||
| 59bf86e747 | |||
| 8f5513a502 | |||
| 87482d5740 | |||
| b227ca6bb0 | |||
| dd0125b641 | |||
| a7e98f411e | |||
| ea76acdd3d | |||
| b92154934b | |||
| 33aeefa19f | |||
| 2fe8fa8813 | |||
| e0f58cce1f | |||
| 157f33edb8 | |||
| 64bf954a17 | |||
| 357cd1b740 | |||
| 3a90a35cbc | |||
| 30923ab3a1 | |||
| d38cf5885e | |||
| 8c4cca8f55 | |||
| a20eeda04d | |||
| d50215d0ba | |||
| c5b2169509 | |||
| 2e42eea306 | |||
| 1c78384da9 | |||
| 053c32e450 | |||
| b57e365349 | |||
| 99396a1eee | |||
| 528810c31a | |||
| a9ffc3ae28 | |||
| eda8680b70 | |||
| afd3e0f042 | |||
| f42ddbfb53 | |||
| 44d76a63d8 | |||
| ec4f9fdd99 | |||
| 6102d641c5 | |||
| d69b805690 | |||
| 994bcce7dc | |||
| 7f5d8b6b11 | |||
| 8d02ab1771 | |||
| 18759c3698 | |||
| 2f533c4591 | |||
| 04c8b2e1fc | |||
| 56c1cbd0fd | |||
| ce51034f69 | |||
| 285d4663c9 | |||
| 5830e13d00 | |||
| 1b685fca76 | |||
| 71e769ef0c | |||
| 63e45eb884 | |||
| 28bc84eda0 | |||
| 7f7f2500dc | |||
| 4f220b48cf | |||
| a9ca154a92 | |||
| 65672fb26f | |||
| 155eb4e675 | |||
| 14d5869019 | |||
| 28ca3b2837 | |||
| 52b0ac9377 | |||
| 8bc182cb37 | |||
| fb3c140d4c | |||
| 52d23604f7 | |||
| 15f8b797bf | |||
| 3b544a4076 | |||
| 36a7dce2e5 | |||
| a418f34491 | |||
| ba5854fc91 | |||
| a0878320fa | |||
| d1583d88c9 | |||
| 3ad67bd33b | |||
| eb2e730b5d | |||
| daa1eacff3 | |||
| aaf23f35f8 | |||
| 25b6378631 | |||
| c6ac3ddad8 | |||
| b3dce26928 | |||
| fcd85dbfb5 | |||
| 3a1c964f0d | |||
| 36ae390f01 | |||
| 4c0cf4fb95 | |||
| 8f8de8fa97 | |||
| bc516456c1 | |||
| a50194d07f | |||
| 1cf272ceb0 | |||
| d2dc09f979 | |||
| ea82d6d608 | |||
| ace3e13685 | |||
| 78ea2e2263 | |||
| d34efa39bb | |||
| 36b5b4e6d4 | |||
| 04874d6e35 | |||
| 073f87dcc7 | |||
| 7ab5ddf053 | |||
| d6ef1a4007 | |||
| 2a4ab4e87c | |||
| 241717b0e2 | |||
| bd8fabae0f | |||
| d119af1a3e | |||
| f1f082fe8a | |||
| b0286bff22 | |||
| a9663a23d7 | |||
| 8df5cd026e | |||
| 0cbfc0d249 | |||
| 46ad2794f1 | |||
| 8fef702522 | |||
| 62b33bd964 | |||
| ffcadf6c01 | |||
| 9f7d52118a | |||
| 95b24f7663 | |||
| db0f1095e5 | |||
| f749b6f2cd | |||
| 23485978fc | |||
| e1f4744a22 | |||
| ef413913d0 | |||
| 8ad4b16156 | |||
| b28a56ea37 | |||
| e3b18d61c9 | |||
| 5335e18925 | |||
| e2f1587832 | |||
| febf56b031 | |||
| e9f6773096 | |||
| 2276762c52 | |||
| a142fefb19 | |||
| 900b7ef3fb | |||
| d07e536f44 | |||
| 2d291d35f6 | |||
| a94f82d8f3 | |||
| 124747c309 | |||
| 4c659c7f87 | |||
| 3e47952438 | |||
| a01c5d31f1 | |||
| 493741c81e |
Executable
+794
@@ -0,0 +1,794 @@
|
|||||||
|
## @package Milter
|
||||||
|
# A thin OO wrapper for the milter module.
|
||||||
|
#
|
||||||
|
# Clients generally subclass Milter.Base and define callback
|
||||||
|
# methods.
|
||||||
|
#
|
||||||
|
# @author Stuart D. Gathman <stuart@bmsi.com>
|
||||||
|
# Copyright 2001,2009 Business Management Systems, Inc.
|
||||||
|
# This code is under the GNU General Public License. See COPYING for details.
|
||||||
|
|
||||||
|
__version__ = '0.9.8'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import milter
|
||||||
|
import thread
|
||||||
|
|
||||||
|
from milter import *
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
_seq_lock = thread.allocate_lock()
|
||||||
|
_seq = 0
|
||||||
|
|
||||||
|
## @fn set_flags(flags)
|
||||||
|
# @brief Enable optional %milter actions.
|
||||||
|
# Certain %milter actions need to be enabled before calling milter.runmilter()
|
||||||
|
# or they throw an exception.
|
||||||
|
# @param flags Bit ored mask of optional actions to enable
|
||||||
|
|
||||||
|
def uniqueID():
|
||||||
|
"""Return a unique sequence number (incremented on each call).
|
||||||
|
"""
|
||||||
|
global _seq
|
||||||
|
_seq_lock.acquire()
|
||||||
|
seqno = _seq = _seq + 1
|
||||||
|
_seq_lock.release()
|
||||||
|
return seqno
|
||||||
|
|
||||||
|
## @private
|
||||||
|
OPTIONAL_CALLBACKS = {
|
||||||
|
'connect':(P_NR_CONN,P_NOCONNECT),
|
||||||
|
'hello':(P_NR_HELO,P_NOHELO),
|
||||||
|
'envfrom':(P_NR_MAIL,P_NOMAIL),
|
||||||
|
'envrcpt':(P_NR_RCPT,P_NORCPT),
|
||||||
|
'data':(P_NR_DATA,P_NODATA),
|
||||||
|
'unknown':(P_NR_UNKN,P_NOUNKNOWN),
|
||||||
|
'eoh':(P_NR_EOH,P_NOEOH),
|
||||||
|
'body':(P_NR_BODY,P_NOBODY),
|
||||||
|
'header':(P_NR_HDR,P_NOHDRS)
|
||||||
|
}
|
||||||
|
|
||||||
|
## @private
|
||||||
|
R = re.compile(r'%+')
|
||||||
|
|
||||||
|
## @private
|
||||||
|
def decode_mask(bits,names):
|
||||||
|
t = [ (s,getattr(milter,s)) for s in names]
|
||||||
|
nms = [s for s,m in t if bits & m]
|
||||||
|
for s,m in t: bits &= ~m
|
||||||
|
if bits: nms += hex(bits)
|
||||||
|
return nms
|
||||||
|
|
||||||
|
## Class decorator to enable optional protocol steps.
|
||||||
|
# P_SKIP is enabled by default when supported, but
|
||||||
|
# applications may wish to enable P_HDR_LEADSPC
|
||||||
|
# to send and receive the leading space of header continuation
|
||||||
|
# lines unchanged, and/or P_RCPT_REJ to have recipients
|
||||||
|
# detected as invalid by the MTA passed to the envcrpt callback.
|
||||||
|
#
|
||||||
|
# Applications may want to check whether the protocol is actually
|
||||||
|
# supported by the MTA in use. Base._protocol
|
||||||
|
# is a bitmask of protocol options negotiated. So,
|
||||||
|
# for instance, if <code>self._protocol & Milter.P_RCPT_REJ</code>
|
||||||
|
# is true, then that feature was successfully negotiated with the MTA
|
||||||
|
# and the application will see recipients the MTA has flagged as invalid.
|
||||||
|
#
|
||||||
|
# Sample use:
|
||||||
|
# <pre>
|
||||||
|
# class myMilter(Milter.Base):
|
||||||
|
# def envrcpt(self,to,*params):
|
||||||
|
# return Milter.CONTINUE
|
||||||
|
# myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ)
|
||||||
|
# </pre>
|
||||||
|
# @since 0.9.3
|
||||||
|
# @param klass the %milter application class to modify
|
||||||
|
# @param mask a bitmask of protocol steps to enable
|
||||||
|
# @return the modified %milter class
|
||||||
|
def enable_protocols(klass,mask):
|
||||||
|
klass._protocol_mask = klass.protocol_mask() & ~mask
|
||||||
|
return klass
|
||||||
|
|
||||||
|
## Milter rejected recipients. A class decorator that calls
|
||||||
|
# enable_protocols() with the P_RCPT_REJ flag. By default, the MTA
|
||||||
|
# does not pass recipients that it knows are invalid on to the milter.
|
||||||
|
# This decorator enables a %milter app to see all recipients if supported
|
||||||
|
# by the MTA. Use like this with python-2.6 and later:
|
||||||
|
# <pre>
|
||||||
|
# @@Milter.rejected_recipients
|
||||||
|
# class myMilter(Milter.Base):
|
||||||
|
# def envrcpt(self,to,*params):
|
||||||
|
# return Milter.CONTINUE
|
||||||
|
# </pre>
|
||||||
|
# @since 0.9.5
|
||||||
|
# @param klass the %milter application class to modify
|
||||||
|
# @return the modified %milter class
|
||||||
|
def rejected_recipients(klass):
|
||||||
|
return enable_protocols(klass,P_RCPT_REJ)
|
||||||
|
|
||||||
|
## Milter leading space on headers. A class decorator that calls
|
||||||
|
# enable_protocols() with the P_HEAD_LEADSPC flag. By default,
|
||||||
|
# header continuation lines are collected and joined before getting
|
||||||
|
# sent to a milter. Headers modified or added by the milter are
|
||||||
|
# folded by the MTA as necessary according to its own standards.
|
||||||
|
# With this flag, header continuation lines are preserved
|
||||||
|
# with their newlines and leading space. In addition, header folding
|
||||||
|
# done by the milter is preserved as well.
|
||||||
|
# Use like this with python-2.6 and later:
|
||||||
|
# <pre>
|
||||||
|
# @@Milter.header_leading_space
|
||||||
|
# class myMilter(Milter.Base):
|
||||||
|
# def header(self,hname,value):
|
||||||
|
# return Milter.CONTINUE
|
||||||
|
# </pre>
|
||||||
|
# @since 0.9.5
|
||||||
|
# @param klass the %milter application class to modify
|
||||||
|
# @return the modified %milter class
|
||||||
|
def header_leading_space(klass):
|
||||||
|
return enable_protocols(klass,P_HEAD_LEADSPC)
|
||||||
|
|
||||||
|
## Function decorator to disable callback methods.
|
||||||
|
# If the MTA supports it, tells the MTA not to invoke this callback,
|
||||||
|
# increasing efficiency. All the callbacks (except negotiate)
|
||||||
|
# are disabled in Milter.Base, and overriding them reenables the
|
||||||
|
# callback. An application may need to use @@nocallback when it extends
|
||||||
|
# another %milter and wants to disable a callback again.
|
||||||
|
# The disabled method should still return Milter.CONTINUE, in case the MTA does
|
||||||
|
# not support protocol negotiation, and for when called from a test harness.
|
||||||
|
# @since 0.9.2
|
||||||
|
def nocallback(func):
|
||||||
|
try:
|
||||||
|
func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][1]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(
|
||||||
|
'@nocallback applied to non-optional method: '+func.__name__)
|
||||||
|
def wrapper(self,*args):
|
||||||
|
if func(self,*args) != CONTINUE:
|
||||||
|
raise RuntimeError('%s return code must be CONTINUE with @nocallback'
|
||||||
|
% func.__name__)
|
||||||
|
return CONTINUE
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
## Function decorator to disable callback reply.
|
||||||
|
# If the MTA supports it, tells the MTA not to wait for a reply from
|
||||||
|
# this callback, and assume CONTINUE. The method should still return
|
||||||
|
# CONTINUE in case the MTA does not support protocol negotiation.
|
||||||
|
# The decorator arranges to change the return code to NOREPLY
|
||||||
|
# when supported by the MTA.
|
||||||
|
# @since 0.9.2
|
||||||
|
def noreply(func):
|
||||||
|
try:
|
||||||
|
nr_mask = OPTIONAL_CALLBACKS[func.__name__][0]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(
|
||||||
|
'@noreply applied to non-optional method: '+func.__name__)
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self,*args):
|
||||||
|
rc = func(self,*args)
|
||||||
|
if self._protocol & nr_mask:
|
||||||
|
if rc != CONTINUE:
|
||||||
|
raise RuntimeError('%s return code must be CONTINUE with @noreply'
|
||||||
|
% func.__name__)
|
||||||
|
return NOREPLY
|
||||||
|
return rc
|
||||||
|
wrapper.milter_protocol = nr_mask
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
## Disabled action exception.
|
||||||
|
# set_flags() can tell the MTA that this application will not use certain
|
||||||
|
# features (such as CHGFROM). This can also be negotiated for each
|
||||||
|
# connection in the negotiate callback. If the application then calls
|
||||||
|
# the feature anyway via an instance method, this exception is
|
||||||
|
# thrown.
|
||||||
|
# @since 0.9.2
|
||||||
|
class DisabledAction(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
## A do "nothing" Milter base class representing an SMTP connection.
|
||||||
|
#
|
||||||
|
# Python milters should derive from this class
|
||||||
|
# unless they are using the low level milter module directly.
|
||||||
|
#
|
||||||
|
# Most of the methods are either "actions" or "callbacks". Callbacks
|
||||||
|
# are invoked by the MTA at certain points in the SMTP protocol. For
|
||||||
|
# instance when the HELO command is seen, the MTA calls the helo
|
||||||
|
# callback before returning a response code. All callbacks must
|
||||||
|
# return one of these constants: CONTINUE, TEMPFAIL, REJECT, ACCEPT,
|
||||||
|
# DISCARD, SKIP. The NOREPLY response is supplied automatically by
|
||||||
|
# the @@noreply decorator if negotiation with the MTA is successful.
|
||||||
|
# @@noreply and @@nocallback methods should return CONTINUE for two reasons:
|
||||||
|
# the MTA may not support negotiation, and the class may be running in a test
|
||||||
|
# harness.
|
||||||
|
#
|
||||||
|
# Optional callbacks are disabled with the @@nocallback decorator, and
|
||||||
|
# automatically reenabled when overridden. Disabled callbacks should
|
||||||
|
# still return CONTINUE for testing and MTAs that do not support
|
||||||
|
# negotiation.
|
||||||
|
|
||||||
|
# Each SMTP connection to the MTA calls the factory method you provide to
|
||||||
|
# create an instance derived from this class. This is typically the
|
||||||
|
# constructor for a class derived from Base. The _setctx() method attaches
|
||||||
|
# the instance to the low level milter.milterContext object. When the SMTP
|
||||||
|
# connection terminates, the close callback is called, the low level connection
|
||||||
|
# object is destroyed, and this normally causes instances of this class to be
|
||||||
|
# garbage collected as well. The close() method should release any global
|
||||||
|
# resources held by instances.
|
||||||
|
# @since 0.9.2
|
||||||
|
class Base(object):
|
||||||
|
"The core class interface to the %milter module."
|
||||||
|
|
||||||
|
## Attach this Milter to the low level milter.milterContext object.
|
||||||
|
def _setctx(self,ctx):
|
||||||
|
## The low level @ref milter.milterContext object.
|
||||||
|
self._ctx = ctx
|
||||||
|
## A bitmask of actions this connection has negotiated to use.
|
||||||
|
# By default, all actions are enabled. High throughput milters
|
||||||
|
# may want to disable unused actions to increase efficiency.
|
||||||
|
# Some optional actions may be disabled by calling milter.set_flags(), or
|
||||||
|
# by overriding the negotiate callback. The bits include:
|
||||||
|
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
|
||||||
|
# CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST</code>.
|
||||||
|
# The <code>Milter.CURR_ACTS</code> bitmask is all actions
|
||||||
|
# known when the milter module was compiled.
|
||||||
|
# Application code can also inspect this field to determine
|
||||||
|
# which actions are available. This is especially useful in
|
||||||
|
# generic library code designed to work in multiple milters.
|
||||||
|
# @since 0.9.2
|
||||||
|
#
|
||||||
|
self._actions = CURR_ACTS # all actions enabled by default
|
||||||
|
## A bitmask of protocol options this connection has negotiated.
|
||||||
|
# An application may inspect this
|
||||||
|
# variable to determine which protocol steps are supported. Options
|
||||||
|
# of interest to applications: the SKIP result code is allowed
|
||||||
|
# only if the P_SKIP bit is set, rejected recipients are passed to the
|
||||||
|
# %milter application only if the P_RCPT_REJ bit is set, and
|
||||||
|
# header values are sent and received with leading spaces (in the
|
||||||
|
# continuation lines) intact if the P_HDR_LEADSPC bit is set (so
|
||||||
|
# that the application can customize indenting).
|
||||||
|
#
|
||||||
|
# The P_N* bits should be negotiated via the @@noreply and @@nocallback
|
||||||
|
# method decorators, and P_RCPT_REJ, P_HDR_LEADSPC should
|
||||||
|
# be enabled using the enable_protocols class decorator.
|
||||||
|
#
|
||||||
|
# The bits include: <code>
|
||||||
|
# P_RCPT_REJ P_NR_CONN P_NR_HELO P_NR_MAIL P_NR_RCPT P_NR_DATA P_NR_UNKN
|
||||||
|
# P_NR_EOH P_NR_BODY P_NR_HDR P_NOCONNECT P_NOHELO P_NOMAIL P_NORCPT
|
||||||
|
# P_NODATA P_NOUNKNOWN P_NOEOH P_NOBODY P_NOHDRS P_HDR_LEADSPC P_SKIP
|
||||||
|
# </code> (all under the Milter namespace).
|
||||||
|
# @since 0.9.2
|
||||||
|
self._protocol = 0 # no protocol options by default
|
||||||
|
if ctx:
|
||||||
|
ctx.setpriv(self)
|
||||||
|
|
||||||
|
## Defined by subclasses to write log messages.
|
||||||
|
def log(self,*msg): pass
|
||||||
|
## Called for each connection to the MTA. Called by the
|
||||||
|
# <a href="https://www.milter.org/developers/api/xxfi_connect">
|
||||||
|
# xxfi_connect</a> callback.
|
||||||
|
# The <code>hostname</code> provided by the local MTA is either
|
||||||
|
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
|
||||||
|
# The format of hostaddr depends on the socket family:
|
||||||
|
# <dl>
|
||||||
|
# <dt><code>socket.AF_INET</code>
|
||||||
|
# <dd>A tuple of (IP as string in dotted quad form, integer port)
|
||||||
|
# <dt><code>socket.AF_INET6</code>
|
||||||
|
# <dd>A tuple of (IP as a string in standard representation,
|
||||||
|
# integer port, integer flow info, integer scope id)
|
||||||
|
# <dt><code>socket.AF_UNIX</code>
|
||||||
|
# <dd>A string with the socketname
|
||||||
|
# </dl>
|
||||||
|
# To vary behavior based on what port the client connected to,
|
||||||
|
# for example skipping blacklist checks for port 587 (which must
|
||||||
|
# be authenticated), use @link #getsymval getsymval('{daemon_port}') @endlink.
|
||||||
|
# The <code>{daemon_port}</code> macro must be enabled in sendmail.cf
|
||||||
|
# <pre>
|
||||||
|
# O Milter.macros.connect=j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}
|
||||||
|
# </pre>
|
||||||
|
# or sendmail.mc
|
||||||
|
# <pre>
|
||||||
|
# define(`confMILTER_MACROS_CONNECT', ``j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}'')dnl
|
||||||
|
# </pre>
|
||||||
|
# @param hostname the PTR name or bracketed IP of the SMTP client
|
||||||
|
# @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
|
||||||
|
# or <code>socket.AF_UNIX</code>
|
||||||
|
# @param hostaddr a tuple or string with peer IP or socketname
|
||||||
|
@nocallback
|
||||||
|
def connect(self,hostname,family,hostaddr): return CONTINUE
|
||||||
|
## Called when the SMTP client says HELO.
|
||||||
|
# Returning REJECT prevents progress until a valid HELO is provided;
|
||||||
|
# this almost always results in terminating the connection.
|
||||||
|
@nocallback
|
||||||
|
def hello(self,hostname): return CONTINUE
|
||||||
|
## Called when the SMTP client says MAIL FROM. Called by the
|
||||||
|
# <a href="https://www.milter.org/developers/api/xxfi_envfrom">
|
||||||
|
# xxfi_envfrom</a> callback.
|
||||||
|
# Returning REJECT rejects the message, but not the connection.
|
||||||
|
# The sender is the "envelope" from as defined by
|
||||||
|
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
|
||||||
|
# For the From: header (author) defined in
|
||||||
|
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
||||||
|
# see @link #header the header callback @endlink.
|
||||||
|
@nocallback
|
||||||
|
def envfrom(self,f,*str): return CONTINUE
|
||||||
|
## Called when the SMTP client says RCPT TO. Called by the
|
||||||
|
# <a href="https://www.milter.org/developers/api/xxfi_envrcpt">
|
||||||
|
# xxfi_envrcpt</a> callback.
|
||||||
|
# Returning REJECT rejects the current recipient, not the entire message.
|
||||||
|
# The recipient is the "envelope" recipient as defined by
|
||||||
|
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
|
||||||
|
# For recipients defined in
|
||||||
|
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
||||||
|
# for example To: or Cc:, see @link #header the header callback @endlink.
|
||||||
|
@nocallback
|
||||||
|
def envrcpt(self,to,*str): return CONTINUE
|
||||||
|
## Called when the SMTP client says DATA.
|
||||||
|
# Returning REJECT rejects the message without wasting bandwidth
|
||||||
|
# on the unwanted message.
|
||||||
|
# @since 0.9.2
|
||||||
|
@nocallback
|
||||||
|
def data(self): return CONTINUE
|
||||||
|
## Called for each header field in the message body.
|
||||||
|
@nocallback
|
||||||
|
def header(self,field,value): return CONTINUE
|
||||||
|
## Called at the blank line that terminates the header fields.
|
||||||
|
@nocallback
|
||||||
|
def eoh(self): return CONTINUE
|
||||||
|
## Called to supply the body of the message to the Milter by chunks.
|
||||||
|
# @param blk a block of message bytes
|
||||||
|
@nocallback
|
||||||
|
def body(self,blk): return CONTINUE
|
||||||
|
## Called when the SMTP client issues an unknown command.
|
||||||
|
# @param cmd the unknown command
|
||||||
|
# @since 0.9.2
|
||||||
|
@nocallback
|
||||||
|
def unknown(self,cmd): return CONTINUE
|
||||||
|
## Called at the end of the message body.
|
||||||
|
# Most of the message manipulation actions can only take place from
|
||||||
|
# the eom callback.
|
||||||
|
def eom(self): return CONTINUE
|
||||||
|
## Called when the connection is abnormally terminated.
|
||||||
|
# The close callback is still called also.
|
||||||
|
def abort(self): return CONTINUE
|
||||||
|
## Called when the connection is closed.
|
||||||
|
def close(self): return CONTINUE
|
||||||
|
|
||||||
|
## Return mask of SMFIP_N* protocol option bits to clear for this class
|
||||||
|
# The @@nocallback and @@noreply decorators set the
|
||||||
|
# <code>milter_protocol</code> function attribute to the protocol mask bit to
|
||||||
|
# pass to libmilter, causing that callback or its reply to be skipped.
|
||||||
|
# Overriding a method creates a new function object, so that
|
||||||
|
# <code>milter_protocol</code> defaults to 0.
|
||||||
|
# Libmilter passes the protocol bits that the current MTA knows
|
||||||
|
# how to skip. We clear the ones we don't want to skip.
|
||||||
|
# The negation is somewhat mind bending, but it is simple.
|
||||||
|
# @since 0.9.2
|
||||||
|
@classmethod
|
||||||
|
def protocol_mask(klass):
|
||||||
|
try:
|
||||||
|
return klass._protocol_mask
|
||||||
|
except AttributeError:
|
||||||
|
p = P_RCPT_REJ | P_HDR_LEADSPC # turn these new features off by default
|
||||||
|
for func,(nr,nc) in OPTIONAL_CALLBACKS.items():
|
||||||
|
func = getattr(klass,func)
|
||||||
|
ca = getattr(func,'milter_protocol',0)
|
||||||
|
#print func,hex(nr),hex(nc),hex(ca)
|
||||||
|
p |= (nr|nc) & ~ca
|
||||||
|
klass._protocol_mask = p
|
||||||
|
return p
|
||||||
|
|
||||||
|
## Negotiate milter protocol options. Called by the
|
||||||
|
# <a href="https://www.milter.org/developers/api/xxfi_negotiate">
|
||||||
|
# xffi_negotiate</a> callback. This is an advanced callback,
|
||||||
|
# do not override unless you know what you are doing. Most
|
||||||
|
# negotiation can be done simply by using the supplied
|
||||||
|
# class and function decorators.
|
||||||
|
# Options are passed as
|
||||||
|
# a list of 4 32-bit ints which can be modified and are passed
|
||||||
|
# back to libmilter on return.
|
||||||
|
# Default negotiation sets P_NO* and P_NR* for callbacks
|
||||||
|
# marked @@nocallback and @@noreply respectively, leaves all
|
||||||
|
# actions enabled, and enables Milter.SKIP. The @@enable_protocols
|
||||||
|
# class decorator can customize which protocol steps are implemented.
|
||||||
|
# @param opts a modifiable list of 4 ints with negotiated options
|
||||||
|
# @since 0.9.2
|
||||||
|
def negotiate(self,opts):
|
||||||
|
try:
|
||||||
|
self._actions,p,f1,f2 = opts
|
||||||
|
opts[1] = self._protocol = p & ~self.protocol_mask()
|
||||||
|
opts[2] = 0
|
||||||
|
opts[3] = 0
|
||||||
|
#self.log("Negotiated:",opts)
|
||||||
|
except:
|
||||||
|
# don't change anything if something went wrong
|
||||||
|
return ALL_OPTS
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
# Milter methods which can be invoked from most callbacks
|
||||||
|
|
||||||
|
## Return the value of an MTA macro. Sendmail macro names
|
||||||
|
# are either single chars (e.g. "j") or multiple chars enclosed
|
||||||
|
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
|
||||||
|
# See <a href="https://www.milter.org/developers/api/smfi_getsymval">
|
||||||
|
# smfi_getsymval</a> for default sendmail macros.
|
||||||
|
# @param sym the macro name
|
||||||
|
def getsymval(self,sym):
|
||||||
|
return self._ctx.getsymval(sym)
|
||||||
|
|
||||||
|
## Set the SMTP reply code and message.
|
||||||
|
# If the MTA does not support setmlreply, then only the
|
||||||
|
# first msg line is used. Any '%%' in a message line
|
||||||
|
# must be doubled, or libmilter will silently ignore the setreply.
|
||||||
|
# Beginning with 0.9.6, we test for that case and throw ValueError to avoid
|
||||||
|
# head scratching. What will <i>really</i> irritate you, however,
|
||||||
|
# is that if you carefully double any '%%', your message will be
|
||||||
|
# sent - but with the '%%' still doubled!
|
||||||
|
# See <a href="https://www.milter.org/developers/api/smfi_setreply">
|
||||||
|
# smfi_setreply</a> for more information.
|
||||||
|
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
|
||||||
|
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
|
||||||
|
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
|
||||||
|
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
|
||||||
|
# @param msg The text part of the SMTP reply. If msg is None,
|
||||||
|
# an empty message is used.
|
||||||
|
# @param ml Optional additional message lines.
|
||||||
|
def setreply(self,rcode,xcode=None,msg=None,*ml):
|
||||||
|
for m in (msg,)+ml:
|
||||||
|
if 1 in [len(s)&1 for s in R.findall(m)]:
|
||||||
|
raise ValueError("'%' must be doubled: "+m)
|
||||||
|
return self._ctx.setreply(rcode,xcode,msg,*ml)
|
||||||
|
|
||||||
|
## Tell the MTA which macro names will be used.
|
||||||
|
# This information can reduce the size of messages received from sendmail,
|
||||||
|
# and hence could reduce bandwidth between sendmail and your milter where
|
||||||
|
# that is a factor. The <code>Milter.SETSYMLIST</code> action flag must be
|
||||||
|
# set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT,
|
||||||
|
# M_DATA, M_EOM, M_EOH.
|
||||||
|
#
|
||||||
|
# May only be called from negotiate callback.
|
||||||
|
# @since 0.9.8, previous version was misspelled!
|
||||||
|
# @param stage the protocol stage to set to macro list for,
|
||||||
|
# one of the M_* constants defined in Milter
|
||||||
|
# @param macros space separated and/or lists of strings
|
||||||
|
def setsymlist(self,stage,*macros):
|
||||||
|
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
|
||||||
|
a = []
|
||||||
|
for m in macros:
|
||||||
|
try:
|
||||||
|
m = m.encode('utf8')
|
||||||
|
except: pass
|
||||||
|
try:
|
||||||
|
m = m.split(' ')
|
||||||
|
except: pass
|
||||||
|
a += m
|
||||||
|
return self._ctx.setsmlist(stage,' '.join(a))
|
||||||
|
|
||||||
|
# Milter methods which can only be called from eom callback.
|
||||||
|
|
||||||
|
## Add a mail header field.
|
||||||
|
# Calls <a href="https://www.milter.org/developers/api/smfi_addheader">
|
||||||
|
# smfi_addheader</a>.
|
||||||
|
# The <code>Milter.ADDHDRS</code> action flag must be set.
|
||||||
|
#
|
||||||
|
# May be called from eom callback only.
|
||||||
|
# @param field the header field name
|
||||||
|
# @param value the header field value
|
||||||
|
# @param idx header field index from the top of the message to insert at
|
||||||
|
# @throws DisabledAction if ADDHDRS is not enabled
|
||||||
|
def addheader(self,field,value,idx=-1):
|
||||||
|
if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS")
|
||||||
|
return self._ctx.addheader(field,value,idx)
|
||||||
|
|
||||||
|
## Change the value of a mail header field.
|
||||||
|
# Calls <a href="https://www.milter.org/developers/api/smfi_chgheader">
|
||||||
|
# smfi_chgheader</a>.
|
||||||
|
# The <code>Milter.CHGHDRS</code> action flag must be set.
|
||||||
|
#
|
||||||
|
# May be called from eom callback only.
|
||||||
|
# @param field the name of the field to change
|
||||||
|
# @param idx index of the field to change when there are multiple instances
|
||||||
|
# @param value the new value of the field
|
||||||
|
# @throws DisabledAction if CHGHDRS is not enabled
|
||||||
|
def chgheader(self,field,idx,value):
|
||||||
|
if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS")
|
||||||
|
return self._ctx.chgheader(field,idx,value)
|
||||||
|
|
||||||
|
## Add a recipient to the message.
|
||||||
|
# Calls <a href="https://www.milter.org/developers/api/smfi_addrcpt">
|
||||||
|
# smfi_addrcpt</a>.
|
||||||
|
# If no corresponding mail header is added, this is like a Bcc.
|
||||||
|
# The syntax of the recipient is the same as used in the SMTP
|
||||||
|
# RCPT TO command (and as delivered to the envrcpt callback), for example
|
||||||
|
# "self.addrcpt('<foo@example.com>')".
|
||||||
|
# The <code>Milter.ADDRCPT</code> action flag must be set.
|
||||||
|
# If the optional <code>params</code> argument is used, then
|
||||||
|
# the <code>Milter.ADDRCPT_PAR</code> action flag must be set.
|
||||||
|
#
|
||||||
|
# May be called from eom callback only.
|
||||||
|
# @param rcpt the message recipient
|
||||||
|
# @param params an optional list of ESMTP parameters
|
||||||
|
# @throws DisabledAction if ADDRCPT or ADDRCPT_PAR is not enabled
|
||||||
|
def addrcpt(self,rcpt,params=None):
|
||||||
|
if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT")
|
||||||
|
if params and not self._actions & ADDRCPT_PAR:
|
||||||
|
raise DisabledAction("ADDRCPT_PAR")
|
||||||
|
return self._ctx.addrcpt(rcpt,params)
|
||||||
|
## Delete a recipient from the message.
|
||||||
|
# Calls <a href="https://www.milter.org/developers/api/smfi_delrcpt">
|
||||||
|
# smfi_delrcpt</a>.
|
||||||
|
# The recipient should match one passed to the envrcpt callback.
|
||||||
|
# The <code>Milter.DELRCPT</code> action flag must be set.
|
||||||
|
#
|
||||||
|
# May be called from eom callback only.
|
||||||
|
# @param rcpt the message recipient to delete
|
||||||
|
# @throws DisabledAction if DELRCPT is not enabled
|
||||||
|
def delrcpt(self,rcpt):
|
||||||
|
if not self._actions & DELRCPT: raise DisabledAction("DELRCPT")
|
||||||
|
return self._ctx.delrcpt(rcpt)
|
||||||
|
|
||||||
|
## Replace the message body.
|
||||||
|
# Calls <a href="https://www.milter.org/developers/api/smfi_replacebody">
|
||||||
|
# smfi_replacebody</a>.
|
||||||
|
# The entire message body must be replaced.
|
||||||
|
# Call repeatedly with blocks of data until the entire body is transferred.
|
||||||
|
# The <code>Milter.MODBODY</code> action flag must be set.
|
||||||
|
#
|
||||||
|
# May be called from eom callback only.
|
||||||
|
# @param body a chunk of body data
|
||||||
|
# @throws DisabledAction if MODBODY is not enabled
|
||||||
|
def replacebody(self,body):
|
||||||
|
if not self._actions & MODBODY: raise DisabledAction("MODBODY")
|
||||||
|
return self._ctx.replacebody(body)
|
||||||
|
|
||||||
|
## Change the SMTP envelope sender address.
|
||||||
|
# Calls <a href="https://www.milter.org/developers/api/smfi_chgfrom">
|
||||||
|
# smfi_chgfrom</a>.
|
||||||
|
# The syntax of the sender is that same as used in the SMTP
|
||||||
|
# MAIL FROM command (and as delivered to the envfrom callback),
|
||||||
|
# for example <code>self.chgfrom('<bar@example.com>')</code>.
|
||||||
|
# The <code>Milter.CHGFROM</code> action flag must be set.
|
||||||
|
#
|
||||||
|
# May be called from eom callback only.
|
||||||
|
# @since 0.9.1
|
||||||
|
# @param sender the new sender address
|
||||||
|
# @param params an optional list of ESMTP parameters
|
||||||
|
# @throws DisabledAction if CHGFROM is not enabled
|
||||||
|
def chgfrom(self,sender,params=None):
|
||||||
|
if not self._actions & CHGFROM: raise DisabledAction("CHGFROM")
|
||||||
|
return self._ctx.chgfrom(sender,params)
|
||||||
|
|
||||||
|
## Quarantine the message.
|
||||||
|
# Calls <a href="https://www.milter.org/developers/api/smfi_quarantine">
|
||||||
|
# smfi_quarantine</a>.
|
||||||
|
# When quarantined, a message goes into the mailq as if to be delivered,
|
||||||
|
# but delivery is deferred until the message is unquarantined.
|
||||||
|
# The <code>Milter.QUARANTINE</code> action flag must be set.
|
||||||
|
#
|
||||||
|
# May be called from eom callback only.
|
||||||
|
# @param reason a string describing the reason for quarantine
|
||||||
|
# @throws DisabledAction if QUARANTINE is not enabled
|
||||||
|
def quarantine(self,reason):
|
||||||
|
if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE")
|
||||||
|
return self._ctx.quarantine(reason)
|
||||||
|
|
||||||
|
## Tell the MTA to wait a bit longer.
|
||||||
|
# Calls <a href="https://www.milter.org/developers/api/smfi_progress">
|
||||||
|
# smfi_progress</a>.
|
||||||
|
# Resets timeouts in the MTA that detect a "hung" milter.
|
||||||
|
def progress(self):
|
||||||
|
return self._ctx.progress()
|
||||||
|
|
||||||
|
## A logging but otherwise do nothing Milter base class.
|
||||||
|
# This is included for compatibility with previous versions of pymilter.
|
||||||
|
# The logging callbacks are marked @@noreply.
|
||||||
|
class Milter(Base):
|
||||||
|
"A simple class interface to the milter module."
|
||||||
|
|
||||||
|
## Provide simple logging to sys.stdout
|
||||||
|
def log(self,*msg):
|
||||||
|
print 'Milter:',
|
||||||
|
for i in msg: print i,
|
||||||
|
print
|
||||||
|
|
||||||
|
@noreply
|
||||||
|
def connect(self,hostname,family,hostaddr):
|
||||||
|
"Called for each connection to sendmail."
|
||||||
|
self.log("connect from %s at %s" % (hostname,hostaddr))
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
|
def hello(self,hostname):
|
||||||
|
"Called after the HELO command."
|
||||||
|
self.log("hello from %s" % hostname)
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
|
def envfrom(self,f,*str):
|
||||||
|
"""Called to begin each message.
|
||||||
|
f -> string message sender
|
||||||
|
str -> tuple additional ESMTP parameters
|
||||||
|
"""
|
||||||
|
self.log("mail from",f,str)
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
|
def envrcpt(self,to,*str):
|
||||||
|
"Called for each message recipient."
|
||||||
|
self.log("rcpt to",to,str)
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
|
def header(self,field,value):
|
||||||
|
"Called for each message header."
|
||||||
|
self.log("%s: %s" % (field,value))
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
@noreply
|
||||||
|
def eoh(self):
|
||||||
|
"Called after all headers are processed."
|
||||||
|
self.log("eoh")
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
def eom(self):
|
||||||
|
"Called at the end of message."
|
||||||
|
self.log("eom")
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
"Called if the connection is terminated abnormally."
|
||||||
|
self.log("abort")
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"Called at the end of connection, even if aborted."
|
||||||
|
self.log("close")
|
||||||
|
return CONTINUE
|
||||||
|
|
||||||
|
## The milter connection factory
|
||||||
|
# This factory method is called for each connection to create the
|
||||||
|
# python object that tracks the connection. It should return
|
||||||
|
# an object derived from Milter.Base.
|
||||||
|
#
|
||||||
|
# Note that since python is dynamic, this variable can be changed while
|
||||||
|
# the milter is running: for instance, to a new subclass based on a
|
||||||
|
# change in configuration.
|
||||||
|
factory = Milter
|
||||||
|
|
||||||
|
## @private
|
||||||
|
# @brief Connect context to connection instance and return enabled callbacks.
|
||||||
|
def negotiate_callback(ctx,opts):
|
||||||
|
m = factory()
|
||||||
|
m._setctx(ctx)
|
||||||
|
return m.negotiate(opts)
|
||||||
|
|
||||||
|
## @private
|
||||||
|
# @brief Connect context if needed and invoke connect method.
|
||||||
|
def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
|
||||||
|
m = ctx.getpriv()
|
||||||
|
if not m:
|
||||||
|
# If not already created (because the current MTA doesn't support
|
||||||
|
# xmfi_negotiate), create the connection object.
|
||||||
|
m = factory()
|
||||||
|
m._setctx(ctx)
|
||||||
|
return m.connect(hostname,family,hostaddr)
|
||||||
|
|
||||||
|
## @private
|
||||||
|
# @brief Disconnect milterContext and call close method.
|
||||||
|
def close_callback(ctx):
|
||||||
|
m = ctx.getpriv()
|
||||||
|
if not m: return CONTINUE
|
||||||
|
try:
|
||||||
|
rc = m.close()
|
||||||
|
finally:
|
||||||
|
m._setctx(None) # release milterContext
|
||||||
|
return rc
|
||||||
|
|
||||||
|
## Convert ESMTP parameters with values to a keyword dictionary.
|
||||||
|
# @deprecated You probably want Milter.param2dict instead.
|
||||||
|
def dictfromlist(args):
|
||||||
|
"Convert ESMTP parms with values to keyword dictionary."
|
||||||
|
kw = {}
|
||||||
|
for s in args:
|
||||||
|
pos = s.find('=')
|
||||||
|
if pos > 0:
|
||||||
|
kw[s[:pos].upper()] = s[pos+1:]
|
||||||
|
return kw
|
||||||
|
|
||||||
|
## Convert ESMTP parm list to keyword dictionary.
|
||||||
|
# Params with no value are set to None in the dictionary.
|
||||||
|
# @since 0.9.3
|
||||||
|
# @param str list of param strings of the form "NAME" or "NAME=VALUE"
|
||||||
|
# @return a dictionary of ESMTP param names and values
|
||||||
|
def param2dict(str):
|
||||||
|
"Convert ESMTP parm list to keyword dictionary."
|
||||||
|
pairs = [x.split('=',1) for x in str]
|
||||||
|
for e in pairs:
|
||||||
|
if len(e) < 2: e.append(None)
|
||||||
|
return dict([(k.upper(),v) for k,v in pairs])
|
||||||
|
|
||||||
|
def envcallback(c,args):
|
||||||
|
"""Call function c with ESMTP parms converted to keyword parameters.
|
||||||
|
Can be used in the envfrom and/or envrcpt callbacks to process
|
||||||
|
ESMTP parameters as python keyword parameters."""
|
||||||
|
kw = {}
|
||||||
|
pargs = [args[0]]
|
||||||
|
for s in args[1:]:
|
||||||
|
pos = s.find('=')
|
||||||
|
if pos > 0:
|
||||||
|
kw[s[:pos].upper()] = s[pos+1:]
|
||||||
|
else:
|
||||||
|
pargs.append(s)
|
||||||
|
return c(*pargs,**kw)
|
||||||
|
|
||||||
|
## Run the %milter.
|
||||||
|
# @param name the name of the %milter known to the MTA
|
||||||
|
# @param socketname the socket to be passed to milter.setconn()
|
||||||
|
# @param timeout the time in secs the MTA should wait for a response before
|
||||||
|
# considering this %milter dead
|
||||||
|
def runmilter(name,socketname,timeout = 0):
|
||||||
|
# This bit is here on the assumption that you will be starting this filter
|
||||||
|
# before sendmail. If sendmail is not running and the socket already exists,
|
||||||
|
# libmilter will throw a warning. If sendmail is running, this is still
|
||||||
|
# safe if there are no messages currently being processed. It's safer to
|
||||||
|
# shutdown sendmail, kill the filter process, restart the filter, and then
|
||||||
|
# restart sendmail.
|
||||||
|
pos = socketname.find(':')
|
||||||
|
if pos > 1:
|
||||||
|
s = socketname[:pos]
|
||||||
|
fname = socketname[pos+1:]
|
||||||
|
else:
|
||||||
|
s = "unix"
|
||||||
|
fname = socketname
|
||||||
|
if s == "unix" or s == "local":
|
||||||
|
print "Removing %s" % fname
|
||||||
|
try:
|
||||||
|
os.unlink(fname)
|
||||||
|
except os.error, x:
|
||||||
|
import errno
|
||||||
|
if x.errno != errno.ENOENT:
|
||||||
|
raise milter.error(x)
|
||||||
|
|
||||||
|
# The default flags set include everything
|
||||||
|
# milter.set_flags(milter.ADDHDRS)
|
||||||
|
milter.set_connect_callback(connect_callback)
|
||||||
|
milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host))
|
||||||
|
# For envfrom and envrcpt, we would like to convert ESMTP parms to keyword
|
||||||
|
# parms, but then all existing users would have to include **kw to accept
|
||||||
|
# arbitrary keywords without crashing. We do provide envcallback and
|
||||||
|
# dictfromlist to make parsing the ESMTP args convenient.
|
||||||
|
milter.set_envfrom_callback(lambda ctx,*str: ctx.getpriv().envfrom(*str))
|
||||||
|
milter.set_envrcpt_callback(lambda ctx,*str: ctx.getpriv().envrcpt(*str))
|
||||||
|
milter.set_header_callback(lambda ctx,fld,val: ctx.getpriv().header(fld,val))
|
||||||
|
milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh())
|
||||||
|
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
|
||||||
|
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
|
||||||
|
milter.set_abort_callback(lambda ctx: ctx.getpriv().abort())
|
||||||
|
milter.set_close_callback(close_callback)
|
||||||
|
|
||||||
|
milter.setconn(socketname)
|
||||||
|
if timeout > 0: milter.settimeout(timeout)
|
||||||
|
# disable negotiate callback if runtime version < (1,0,1)
|
||||||
|
ncb = negotiate_callback
|
||||||
|
if milter.getversion() < (1,0,1):
|
||||||
|
ncb = None
|
||||||
|
# The name *must* match the X line in sendmail.cf (supposedly)
|
||||||
|
milter.register(name,
|
||||||
|
data=lambda ctx: ctx.getpriv().data(),
|
||||||
|
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
|
||||||
|
negotiate=ncb
|
||||||
|
)
|
||||||
|
start_seq = _seq
|
||||||
|
try:
|
||||||
|
milter.main()
|
||||||
|
except milter.error:
|
||||||
|
if start_seq == _seq: raise # couldn't start
|
||||||
|
# milter has been running for a while, but now it can't start new threads
|
||||||
|
raise milter.error("out of thread resources")
|
||||||
|
|
||||||
|
__all__ = globals().copy()
|
||||||
|
for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'):
|
||||||
|
del __all__[priv]
|
||||||
|
__all__ = __all__.keys()
|
||||||
|
|
||||||
|
## @example milter-template.py
|
||||||
|
## @example milter-nomix.py
|
||||||
|
#
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
# Email address list with expiration
|
||||||
|
#
|
||||||
|
# This class acts like a map. Entries with a value of None are persistent,
|
||||||
|
# but disappear after a time limit. This is useful for automatic whitelists
|
||||||
|
# and blacklists with expiration. The persistent store is a simple ascii
|
||||||
|
# file with sender and timestamp on each line. Entries can be appended
|
||||||
|
# to the store, and will be picked up the next time it is loaded.
|
||||||
|
#
|
||||||
|
# Entries with other values are not persistent. This is used to hold failed
|
||||||
|
# CBV results.
|
||||||
|
#
|
||||||
|
# $Log$
|
||||||
|
# Revision 1.9 2008/05/08 21:35:57 customdesigned
|
||||||
|
# Allow explicitly whitelisted email from banned_users.
|
||||||
|
#
|
||||||
|
# Revision 1.8 2007/09/03 16:18:45 customdesigned
|
||||||
|
# Delete unparseable timestamps when loading address cache. These have
|
||||||
|
# arisen because of failure to parse MAIL FROM properly. Will have to
|
||||||
|
# tighten up MAIL FROM parsing to match RFC.
|
||||||
|
#
|
||||||
|
# Revision 1.7 2007/01/25 22:47:26 customdesigned
|
||||||
|
# Persist blacklisting from delayed DSNs.
|
||||||
|
#
|
||||||
|
# Revision 1.6 2007/01/19 23:31:38 customdesigned
|
||||||
|
# Move parse_header to Milter.utils.
|
||||||
|
# Test case for delayed DSN parsing.
|
||||||
|
# Fix plock when source missing or cannot set owner/group.
|
||||||
|
#
|
||||||
|
# Revision 1.5 2007/01/11 19:59:40 customdesigned
|
||||||
|
# Purge old entries in auto_whitelist and send_dsn logs.
|
||||||
|
#
|
||||||
|
# Revision 1.4 2007/01/11 04:31:26 customdesigned
|
||||||
|
# Negative feedback for bad headers. Purge cache logs on startup.
|
||||||
|
#
|
||||||
|
# Revision 1.3 2007/01/08 23:20:54 customdesigned
|
||||||
|
# Get user feedback.
|
||||||
|
#
|
||||||
|
# Revision 1.2 2007/01/05 23:33:55 customdesigned
|
||||||
|
# Make blacklist an AddrCache
|
||||||
|
#
|
||||||
|
# Revision 1.1 2007/01/05 21:25:40 customdesigned
|
||||||
|
# Move AddrCache to Milter package.
|
||||||
|
#
|
||||||
|
|
||||||
|
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
|
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
|
||||||
|
# This code is under the GNU General Public License. See COPYING for details.
|
||||||
|
|
||||||
|
import time
|
||||||
|
from plock import PLock
|
||||||
|
|
||||||
|
class AddrCache(object):
|
||||||
|
time_format = '%Y%b%d %H:%M:%S %Z'
|
||||||
|
|
||||||
|
def __init__(self,renew=7,fname=None):
|
||||||
|
self.age = renew
|
||||||
|
self.cache = {}
|
||||||
|
self.fname = fname
|
||||||
|
|
||||||
|
def load(self,fname,age=0):
|
||||||
|
"Load address cache from persistent store."
|
||||||
|
if not age:
|
||||||
|
age = self.age
|
||||||
|
self.fname = fname
|
||||||
|
cache = {}
|
||||||
|
self.cache = cache
|
||||||
|
now = time.time()
|
||||||
|
lock = PLock(self.fname)
|
||||||
|
wfp = lock.lock()
|
||||||
|
changed = False
|
||||||
|
try:
|
||||||
|
too_old = now - age*24*60*60 # max age in days
|
||||||
|
try:
|
||||||
|
fp = open(self.fname)
|
||||||
|
except OSError:
|
||||||
|
fp = ()
|
||||||
|
for ln in fp:
|
||||||
|
try:
|
||||||
|
rcpt,ts = ln.strip().split(None,1)
|
||||||
|
try:
|
||||||
|
l = time.strptime(ts,AddrCache.time_format)
|
||||||
|
t = time.mktime(l)
|
||||||
|
if t < too_old:
|
||||||
|
changed = True
|
||||||
|
continue
|
||||||
|
cache[rcpt.lower()] = (t,None)
|
||||||
|
except: # unparsable timestamp - likely garbage
|
||||||
|
changed = True
|
||||||
|
continue
|
||||||
|
except: # manual entry (no timestamp)
|
||||||
|
cache[ln.strip().lower()] = (now,None)
|
||||||
|
wfp.write(ln)
|
||||||
|
if changed:
|
||||||
|
lock.commit(self.fname+'.old')
|
||||||
|
else:
|
||||||
|
lock.unlock()
|
||||||
|
except IOError:
|
||||||
|
lock.unlock()
|
||||||
|
|
||||||
|
def has_precise_key(self,sender):
|
||||||
|
"""True if precise sender is cached and has not expired. Don't
|
||||||
|
try looking up wildcard entries.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
lsender = sender and sender.lower()
|
||||||
|
ts,res = self.cache[lsender]
|
||||||
|
too_old = time.time() - self.age*24*60*60 # max age in days
|
||||||
|
if not ts or ts > too_old:
|
||||||
|
return True
|
||||||
|
del self.cache[lsender]
|
||||||
|
except KeyError: pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_key(self,sender):
|
||||||
|
"True if sender is cached and has not expired."
|
||||||
|
if self.has_precise_key(sender):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
user,host = sender.split('@',1)
|
||||||
|
return self.has_precise_key(host)
|
||||||
|
except: pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
__contains__ = has_key
|
||||||
|
|
||||||
|
def __getitem__(self,sender):
|
||||||
|
try:
|
||||||
|
lsender = sender.lower()
|
||||||
|
ts,res = self.cache[lsender]
|
||||||
|
too_old = time.time() - self.age*24*60*60 # max age in days
|
||||||
|
if not ts or ts > too_old:
|
||||||
|
return res
|
||||||
|
del self.cache[lsender]
|
||||||
|
raise KeyError, sender
|
||||||
|
except KeyError,x:
|
||||||
|
try:
|
||||||
|
user,host = sender.split('@',1)
|
||||||
|
return self.__getitem__(host)
|
||||||
|
except ValueError:
|
||||||
|
raise x
|
||||||
|
|
||||||
|
def addperm(self,sender,res=None):
|
||||||
|
"Add a permanent sender."
|
||||||
|
lsender = sender.lower()
|
||||||
|
if self.has_key(lsender):
|
||||||
|
ts,res = self.cache[lsender]
|
||||||
|
if not ts: return # already permanent
|
||||||
|
self.cache[lsender] = (None,res)
|
||||||
|
if not res:
|
||||||
|
print >>open(self.fname,'a'),sender
|
||||||
|
|
||||||
|
def __setitem__(self,sender,res):
|
||||||
|
lsender = sender.lower()
|
||||||
|
now = time.time()
|
||||||
|
self.cache[lsender] = (now,res)
|
||||||
|
if not res and self.fname:
|
||||||
|
s = time.strftime(AddrCache.time_format,time.localtime(now))
|
||||||
|
print >>open(self.fname,'a'),sender,s # log refreshed senders
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.cache)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from ConfigParser import ConfigParser
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
class MilterConfigParser(ConfigParser):
|
||||||
|
|
||||||
|
def __init__(self,defaults={}):
|
||||||
|
ConfigParser.__init__(self)
|
||||||
|
self.defaults = defaults
|
||||||
|
|
||||||
|
# The defaults provided by ConfigParser show up in all sections,
|
||||||
|
# which screws up iterating over all options in a section.
|
||||||
|
# Worse, passing "defaults" with vars= overrides the config file!
|
||||||
|
# So we roll our own defaults.
|
||||||
|
def get(self,sect,opt):
|
||||||
|
if not self.has_option(sect,opt) and opt in self.defaults:
|
||||||
|
return self.defaults[opt]
|
||||||
|
return ConfigParser.get(self,sect,opt)
|
||||||
|
|
||||||
|
def getlist(self,sect,opt):
|
||||||
|
if self.has_option(sect,opt):
|
||||||
|
return [q.strip() for q in self.get(sect,opt).split(',')]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def getaddrset(self,sect,opt,dir=''):
|
||||||
|
if not self.has_option(sect,opt):
|
||||||
|
return {}
|
||||||
|
s = self.get(sect,opt)
|
||||||
|
d = {}
|
||||||
|
for q in s.split(','):
|
||||||
|
q = q.strip()
|
||||||
|
if q.startswith('file:'):
|
||||||
|
domain = q[5:].lower()
|
||||||
|
fname = os.path.join(dir,domain)
|
||||||
|
d[domain] = d.setdefault(domain,[]) + open(fname,'r').read().split()
|
||||||
|
else:
|
||||||
|
user,domain = q.split('@')
|
||||||
|
d.setdefault(domain.lower(),[]).append(user)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def getaddrdict(self,sect,opt,dir=''):
|
||||||
|
if not self.has_option(sect,opt):
|
||||||
|
return {}
|
||||||
|
d = {}
|
||||||
|
for q in self.get(sect,opt).split(','):
|
||||||
|
q = q.strip()
|
||||||
|
if self.has_option(sect,q):
|
||||||
|
l = self.get(sect,q)
|
||||||
|
for addr in l.split(','):
|
||||||
|
addr = addr.strip()
|
||||||
|
if addr.startswith('file:'):
|
||||||
|
fname = os.path.join(dir,addr[5:])
|
||||||
|
for a in open(fname,'r').read().split():
|
||||||
|
d[a] = q
|
||||||
|
else:
|
||||||
|
d[addr] = q
|
||||||
|
return d
|
||||||
|
|
||||||
|
def getdefault(self,sect,opt,default=None):
|
||||||
|
if self.has_option(sect,opt):
|
||||||
|
return self.get(sect,opt)
|
||||||
|
return default
|
||||||
|
|
||||||
|
def getintdefault(self,sect,opt,default=None):
|
||||||
|
if self.has_option(sect,opt):
|
||||||
|
return self.getint(sect,opt)
|
||||||
|
return default
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
## @package Milter.dns
|
||||||
|
# Provide a higher level interface to pydns.
|
||||||
|
|
||||||
|
import DNS
|
||||||
|
from DNS import DNSError
|
||||||
|
|
||||||
|
MAX_CNAME = 10
|
||||||
|
|
||||||
|
## Lookup DNS records by label and RR type.
|
||||||
|
# The response can include records of other types that the DNS
|
||||||
|
# server thinks we might need.
|
||||||
|
# @param name the DNS label to lookup
|
||||||
|
# @param qtype the name of the DNS RR type to lookup
|
||||||
|
# @return a list of ((name,type),data) tuples
|
||||||
|
def DNSLookup(name, qtype):
|
||||||
|
try:
|
||||||
|
# To be thread safe, we create a fresh DnsRequest with
|
||||||
|
# each call. It would be more efficient to reuse
|
||||||
|
# a req object stored in a Session.
|
||||||
|
req = DNS.DnsRequest(name, qtype=qtype)
|
||||||
|
resp = req.req()
|
||||||
|
#resp.show()
|
||||||
|
# key k: ('wayforward.net', 'A'), value v
|
||||||
|
# FIXME: pydns returns AAAA RR as 16 byte binary string, but
|
||||||
|
# A RR as dotted quad. For consistency, this driver should
|
||||||
|
# return both as binary string.
|
||||||
|
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
||||||
|
except IOError, x:
|
||||||
|
raise DNSError, str(x)
|
||||||
|
|
||||||
|
class Session(object):
|
||||||
|
"""A Session object has a simple cache with no TTL that is valid
|
||||||
|
for a single "session", for example an SMTP conversation."""
|
||||||
|
def __init__(self):
|
||||||
|
self.cache = {}
|
||||||
|
|
||||||
|
## Additional DNS RRs we can safely cache.
|
||||||
|
# We have to be careful which additional DNS RRs we cache. For
|
||||||
|
# instance, PTR records are controlled by the connecting IP, and they
|
||||||
|
# could poison our local cache with bogus A and MX records.
|
||||||
|
# Each entry is a tuple of (query_type,rr_type). So for instance,
|
||||||
|
# the entry ('MX','A') says it is safe (for milter purposes) to cache
|
||||||
|
# any 'A' RRs found in an 'MX' query.
|
||||||
|
SAFE2CACHE = frozenset((
|
||||||
|
('MX','MX'), ('MX','A'),
|
||||||
|
('CNAME','CNAME'), ('CNAME','A'),
|
||||||
|
('A','A'),
|
||||||
|
('AAAA','AAAA'),
|
||||||
|
('PTR','PTR'),
|
||||||
|
('NS','NS'), ('NS','A'),
|
||||||
|
('TXT','TXT'),
|
||||||
|
('SPF','SPF')
|
||||||
|
))
|
||||||
|
|
||||||
|
## Cached DNS lookup.
|
||||||
|
# @param name the DNS label to query
|
||||||
|
# @param qtype the query type, e.g. 'A'
|
||||||
|
# @param cnames tracks CNAMES already followed in recursive calls
|
||||||
|
def dns(self, name, qtype, cnames=None):
|
||||||
|
"""DNS query.
|
||||||
|
|
||||||
|
If the result is in cache, return that. Otherwise pull the
|
||||||
|
result from DNS, and cache ALL answers, so additional info
|
||||||
|
is available for further queries later.
|
||||||
|
|
||||||
|
CNAMEs are followed.
|
||||||
|
|
||||||
|
If there is no data, [] is returned.
|
||||||
|
|
||||||
|
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
|
||||||
|
post: isinstance(__return__, types.ListType)
|
||||||
|
"""
|
||||||
|
if name.endswith('.'): name = name[:-1]
|
||||||
|
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
|
||||||
|
return [] # invalid DNS name (too long or empty)
|
||||||
|
result = self.cache.get( (name, qtype) )
|
||||||
|
cname = None
|
||||||
|
if result: return result
|
||||||
|
cnamek = (name,'CNAME')
|
||||||
|
cname = self.cache.get( cnamek )
|
||||||
|
|
||||||
|
if cname:
|
||||||
|
cname = cname[0]
|
||||||
|
else:
|
||||||
|
safe2cache = Session.SAFE2CACHE
|
||||||
|
for k, v in DNSLookup(name, qtype):
|
||||||
|
if k == cnamek:
|
||||||
|
cname = v
|
||||||
|
if k[1] == 'CNAME' or (qtype,k[1]) in safe2cache:
|
||||||
|
self.cache.setdefault(k, []).append(v)
|
||||||
|
result = self.cache.get( (name, qtype), [])
|
||||||
|
if not result and cname:
|
||||||
|
if not cnames:
|
||||||
|
cnames = {}
|
||||||
|
elif len(cnames) >= MAX_CNAME:
|
||||||
|
#return result # if too many == NX_DOMAIN
|
||||||
|
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
||||||
|
cnames[name] = cname
|
||||||
|
if cname in cnames:
|
||||||
|
raise DNSError('CNAME loop')
|
||||||
|
result = self.dns(cname, qtype, cnames=cnames)
|
||||||
|
if result:
|
||||||
|
self.cache[(name,qtype)] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
def dns_txt(self, domainname, enc='ascii'):
|
||||||
|
"Get a list of TXT records for a domain name."
|
||||||
|
if domainname:
|
||||||
|
try:
|
||||||
|
return [''.join(s.decode(enc) for s in a)
|
||||||
|
for a in self.dns(domainname, 'TXT')]
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
raise DNSError('Non-ascii character in SPF TXT record.')
|
||||||
|
return []
|
||||||
|
|
||||||
|
DNS.DiscoverNameServers()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
s = Session()
|
||||||
|
for n,t in zip(*[iter(sys.argv[1:])]*2):
|
||||||
|
print n,t
|
||||||
|
print s.dns(n,t)
|
||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
|
# Copyright 2005 Business Management Systems, Inc.
|
||||||
|
# This code is under the GNU General Public License. See COPYING for details.
|
||||||
|
|
||||||
|
# Send DSNs, do call back verification,
|
||||||
|
# and generate DSN messages from a template
|
||||||
|
# $Log$
|
||||||
|
# Revision 1.22 2011/03/18 20:41:31 customdesigned
|
||||||
|
# Python2.6 SMTP.close() fails when instance never connected.
|
||||||
|
#
|
||||||
|
# Revision 1.21 2011/03/03 05:11:58 customdesigned
|
||||||
|
# Release 0.9.4
|
||||||
|
#
|
||||||
|
# Revision 1.20 2010/10/11 00:29:47 customdesigned
|
||||||
|
# Handle multiple recipients. For CBV or auto whitelist of multiple emails.
|
||||||
|
#
|
||||||
|
# Revision 1.19 2009/07/02 19:41:12 customdesigned
|
||||||
|
# Handle @ in localpart.
|
||||||
|
#
|
||||||
|
# Revision 1.18 2009/06/10 18:01:59 customdesigned
|
||||||
|
# Doxygen updates
|
||||||
|
#
|
||||||
|
# Revision 1.17 2009/05/20 20:08:44 customdesigned
|
||||||
|
# Support non-DSN CBV (non-empty MAIL FROM)
|
||||||
|
#
|
||||||
|
# Revision 1.16 2007/09/25 01:24:59 customdesigned
|
||||||
|
# Allow arbitrary object, not just spf.query like, to provide data for create_msg
|
||||||
|
#
|
||||||
|
# Revision 1.15 2007/09/24 20:13:26 customdesigned
|
||||||
|
# Remove explicit spf dependency.
|
||||||
|
#
|
||||||
|
# Revision 1.14 2007/03/03 18:19:40 customdesigned
|
||||||
|
# Handle DNS error sending DSN.
|
||||||
|
#
|
||||||
|
# Revision 1.13 2007/01/04 18:01:11 customdesigned
|
||||||
|
# Do plain CBV when template missing.
|
||||||
|
#
|
||||||
|
# Revision 1.12 2006/07/26 16:37:35 customdesigned
|
||||||
|
# Support timeout.
|
||||||
|
#
|
||||||
|
# Revision 1.11 2006/06/21 21:07:11 customdesigned
|
||||||
|
# Include header fields in DSN template.
|
||||||
|
#
|
||||||
|
# Revision 1.10 2006/05/24 20:56:35 customdesigned
|
||||||
|
# Remove default templates. Scrub test.
|
||||||
|
#
|
||||||
|
## @package Milter.dsn
|
||||||
|
# Support DSNs and CallBackValidations (CBV).
|
||||||
|
#
|
||||||
|
# A Delivery Status Notification (bounce) is sent to the envelope
|
||||||
|
# sender (original MAIL FROM) with a null MAIL FROM (<>) to notify the
|
||||||
|
# original sender # of delays or problems with delivery. A Callback Validation
|
||||||
|
# starts the DSN process, but stops before issuing the DATA command. The
|
||||||
|
# purpose is to check whether the envelope recipient is accepted (and is
|
||||||
|
# therefore a valid email). The null MAIL FROM tells the remote
|
||||||
|
# MTA to never reply according to RFC2821 (but some braindead MTAs
|
||||||
|
# reply anyway, of course).
|
||||||
|
#
|
||||||
|
# Milters should cache CBV results and should avoid sending DSNs
|
||||||
|
# unless the sender is authenticated somehow (e.g. SPF Pass). However,
|
||||||
|
# when email is quarantined, and is not known to be a forgery, sending a DSN
|
||||||
|
# is better than silently disappearing, and a DSN is better than sending
|
||||||
|
# a normal message as notification - because MAIL FROM signing schemes
|
||||||
|
# can reject bounces of forged emails. Whatever you do, don't copy those
|
||||||
|
# assinine commercial filters that send a normal message to notify you
|
||||||
|
# that some virus is forging your email.
|
||||||
|
#
|
||||||
|
# <b>DSNs should *only* be sent to MAIL FROM addresses.</b> Never send
|
||||||
|
# a DSN or use a null MAIL FROM with an email address obtained from
|
||||||
|
# anywhere else.
|
||||||
|
#
|
||||||
|
import smtplib
|
||||||
|
import socket
|
||||||
|
from email.Message import Message
|
||||||
|
import Milter
|
||||||
|
import time
|
||||||
|
import dns
|
||||||
|
|
||||||
|
## Send DSN.
|
||||||
|
# Try the published MX names in order, rejecting obviously bogus entries
|
||||||
|
# (like <code>localhost</code>).
|
||||||
|
# @param mailfrom the original sender we are notifying or validating
|
||||||
|
# @param receiver the HELO name of the MTA we are sending the DSN on behalf of.
|
||||||
|
# Be sure to send from an IP that matches the HELO.
|
||||||
|
# @param msg the DSN message in RFC2822 format, or None for CBV.
|
||||||
|
# @param timeout total seconds to wait for a response from an MX
|
||||||
|
# @param session Milter.dns.Session object from current incoming mail
|
||||||
|
# session to reuse its cache, or None to create a fresh one.
|
||||||
|
# @param ourfrom set to a valid email to send a normal notification from, or
|
||||||
|
# to validate emails not obtained from MAIL FROM.
|
||||||
|
# @return None on success or (status_code,msg) on failure.
|
||||||
|
def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
|
||||||
|
"""Send DSN. If msg is None, do callback verification.
|
||||||
|
Mailfrom is original sender we are sending DSN or CBV to.
|
||||||
|
Receiver is the MTA sending the DSN.
|
||||||
|
Return None for success or (code,msg) for failure."""
|
||||||
|
user,domain = mailfrom.rsplit('@',1)
|
||||||
|
if not session: session = dns.Session()
|
||||||
|
try:
|
||||||
|
mxlist = session.dns(domain,'MX')
|
||||||
|
except dns.DNSError:
|
||||||
|
return (450,'DNS Timeout: %s MX'%domain) # temp error
|
||||||
|
if not mxlist:
|
||||||
|
mxlist = (0,domain), # fallback to A record when no MX
|
||||||
|
else:
|
||||||
|
mxlist.sort()
|
||||||
|
smtp = smtplib.SMTP()
|
||||||
|
toolate = time.time() + timeout
|
||||||
|
for prior,host in mxlist:
|
||||||
|
try:
|
||||||
|
smtp.connect(host)
|
||||||
|
code,resp = smtp.helo(receiver)
|
||||||
|
# some wiley spammers have MX records that resolve to 127.0.0.1
|
||||||
|
a = resp.split()
|
||||||
|
if not a:
|
||||||
|
return (553,'MX for %s has no hostname in banner: %s' % (domain,host))
|
||||||
|
if a[0] == receiver:
|
||||||
|
return (553,'Fraudulent MX for %s: %s' % (domain,host))
|
||||||
|
if not (200 <= code <= 299):
|
||||||
|
raise smtplib.SMTPHeloError(code, resp)
|
||||||
|
if msg:
|
||||||
|
try:
|
||||||
|
smtp.sendmail('<%s>'%ourfrom,mailfrom,msg)
|
||||||
|
except smtplib.SMTPSenderRefused:
|
||||||
|
# does not accept DSN, try postmaster (at the risk of mail loops)
|
||||||
|
smtp.sendmail('<postmaster@%s>'%receiver,mailfrom,msg)
|
||||||
|
else: # CBV
|
||||||
|
code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom)
|
||||||
|
if code != 250:
|
||||||
|
raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom)
|
||||||
|
if isinstance(mailfrom,basestring):
|
||||||
|
mailfrom = [mailfrom]
|
||||||
|
badrcpts = {}
|
||||||
|
for rcpt in mailfrom:
|
||||||
|
code,resp = smtp.rcpt(rcpt)
|
||||||
|
if code not in (250,251):
|
||||||
|
badrcpts[rcpt] = (code,resp)# permanent error
|
||||||
|
smtp.quit()
|
||||||
|
if len(badrcpts) == 1:
|
||||||
|
return badrcpts.values()[0] # permanent error
|
||||||
|
if badrcpts:
|
||||||
|
return badrcpts
|
||||||
|
return None # success
|
||||||
|
except smtplib.SMTPRecipientsRefused,x:
|
||||||
|
if len(x.recipients) == 1:
|
||||||
|
return x.recipients.values()[0] # permanent error
|
||||||
|
return x.recipients
|
||||||
|
except smtplib.SMTPSenderRefused,x:
|
||||||
|
return x.args[:2] # does not accept DSN
|
||||||
|
except smtplib.SMTPDataError,x:
|
||||||
|
return x.args # permanent error
|
||||||
|
except smtplib.SMTPException:
|
||||||
|
pass # any other error, try next MX
|
||||||
|
except socket.error:
|
||||||
|
pass # MX didn't accept connections, try next one
|
||||||
|
except socket.timeout:
|
||||||
|
pass # MX too slow, try next one
|
||||||
|
if hasattr(smtp,'sock'): smtp.close()
|
||||||
|
if time.time() > toolate:
|
||||||
|
return (450,'No MX response within %f minutes'%(timeout/60.0))
|
||||||
|
return (450,'No MX servers available') # temp error
|
||||||
|
|
||||||
|
class Vars: pass
|
||||||
|
|
||||||
|
# NOTE: Caller can pass an object to create_msg that in a typical milter
|
||||||
|
# collects things like heloname or sender anyway.
|
||||||
|
def create_msg(v,rcptlist=None,origmsg=None,template=None):
|
||||||
|
"""Create a DSN message from a template. Template must be '\n' separated.
|
||||||
|
v - an object whose attributes are used for substitutions. Must
|
||||||
|
have sender and receiver attributes at a minimum.
|
||||||
|
rcptlist - used to set v.rcpt if given
|
||||||
|
origmsg - used to set v.subject and v.spf_result if given
|
||||||
|
template - a '\n' separated string with python '%(name)s' substitutions.
|
||||||
|
"""
|
||||||
|
if not template:
|
||||||
|
return None
|
||||||
|
if hasattr(v,'perm_error'):
|
||||||
|
# likely to be an spf.query, try translating for backward compatibility
|
||||||
|
q = v
|
||||||
|
v = Vars()
|
||||||
|
try:
|
||||||
|
v.heloname = q.h
|
||||||
|
v.sender = q.s
|
||||||
|
v.connectip = q.i
|
||||||
|
v.receiver = q.r
|
||||||
|
v.sender_domain = q.o
|
||||||
|
v.result = q.result
|
||||||
|
v.perm_error = q.perm_error
|
||||||
|
except: v = q
|
||||||
|
if rcptlist:
|
||||||
|
v.rcpt = '\n\t'.join(rcptlist)
|
||||||
|
if origmsg:
|
||||||
|
try: v.subject = origmsg['Subject']
|
||||||
|
except: v.subject = '(none)'
|
||||||
|
try:
|
||||||
|
v.spf_result = origmsg['Received-SPF']
|
||||||
|
except: v.spf_result = None
|
||||||
|
|
||||||
|
msg = Message()
|
||||||
|
|
||||||
|
msg.add_header('X-Mailer','PyMilter-'+Milter.__version__)
|
||||||
|
msg.set_type('text/plain')
|
||||||
|
|
||||||
|
hdrs,body = template.split('\n\n',1)
|
||||||
|
for ln in hdrs.splitlines():
|
||||||
|
name,val = ln.split(':',1)
|
||||||
|
msg.add_header(name,(val % v.__dict__).strip())
|
||||||
|
msg.set_payload(body % v.__dict__)
|
||||||
|
# add headers if missing from old template
|
||||||
|
if 'to' not in msg:
|
||||||
|
msg.add_header('To',v.sender)
|
||||||
|
if 'from' not in msg:
|
||||||
|
msg.add_header('From','postmaster@%s'%v.receiver)
|
||||||
|
if 'auto-submitted' not in msg:
|
||||||
|
msg.add_header('Auto-Submitted','auto-generated')
|
||||||
|
return msg
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import spf
|
||||||
|
q = spf.query('192.168.9.50',
|
||||||
|
'SRS0=pmeHL=RH==stuart@example.com',
|
||||||
|
'red.example.com',receiver='mail.example.com')
|
||||||
|
q.result = 'softfail'
|
||||||
|
q.perm_error = None
|
||||||
|
msg = create_msg(q,['charlie@example.com'],None,
|
||||||
|
"""From: postmaster@%(receiver)s
|
||||||
|
To: %(sender)s
|
||||||
|
Subject: Test
|
||||||
|
|
||||||
|
Test DSN template
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
print msg.as_string()
|
||||||
|
# print send_dsn(f,msg.as_string())
|
||||||
|
# print send_dsn(q.s,'mail.example.com',msg.as_string())
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
|
# Copyright 2005 Business Management Systems, Inc.
|
||||||
|
# This code is under the GNU General Public License. See COPYING for details.
|
||||||
|
|
||||||
|
# Heuristically determine whether a domain name is for a dynamic IP.
|
||||||
|
|
||||||
|
# examples we don't yet recognize:
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
ip3 = re.compile('[0-9]{1,3}')
|
||||||
|
hpats = (
|
||||||
|
'h[0-9a-f]{12}[.]',
|
||||||
|
'h\d*n\d*c\d*o\d*\.',
|
||||||
|
'pcp\d{6,10}pcs[.]',
|
||||||
|
'no-reverse',
|
||||||
|
'S[0-9a-f]{16}[.][a-z]{2}[.]',
|
||||||
|
'user<3>\.',
|
||||||
|
'[Cc]ust<3>\.',
|
||||||
|
'^<3>\.',
|
||||||
|
'ppp[^.]*<3>\.',
|
||||||
|
'-ppp\d*\.',
|
||||||
|
'\d*-<3>\.',
|
||||||
|
'[0-9a-f]{1,3}-<3>\.',
|
||||||
|
'p<3>\.pool',
|
||||||
|
'h<3>\.',
|
||||||
|
'xdsl-\d*\.',
|
||||||
|
'-\d*-\d*\.',
|
||||||
|
'\.adsl\.',
|
||||||
|
'\.cable\.'
|
||||||
|
)
|
||||||
|
rehmac = re.compile('|'.join(hpats))
|
||||||
|
|
||||||
|
def is_dynip(host,addr):
|
||||||
|
"""Return True if hostname is for a dynamic ip.
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
>>> is_dynip('post3.fabulousdealz.com','69.60.99.112')
|
||||||
|
False
|
||||||
|
>>> is_dynip('adsl-69-208-201-177.dsl.emhril.ameritech.net','69.208.201.177')
|
||||||
|
True
|
||||||
|
>>> is_dynip('[1.2.3.4]','1.2.3.4')
|
||||||
|
True
|
||||||
|
>>> is_dynip('c-71-63-151-151.hsd1.mn.comcast.net','71.63.151.151')
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
if host.startswith('[') and host.endswith(']'):
|
||||||
|
return True # no ptr
|
||||||
|
if addr:
|
||||||
|
if host.find(addr) >= 0: return True
|
||||||
|
if addr.find(':') >= 0: return False # IP6
|
||||||
|
a = addr.split('.')
|
||||||
|
ia = map(int,a)
|
||||||
|
h = host
|
||||||
|
m = ip3.findall(host)
|
||||||
|
if m:
|
||||||
|
g = map(int,m)[:4]
|
||||||
|
ia3 = (ia[1:],ia[:3])
|
||||||
|
if g[-3:] in ia3: return True
|
||||||
|
if g[0] == ia[3] and g[1:3] == ia[:2]: return True
|
||||||
|
if g[-2:] == ia[2:]: return True
|
||||||
|
g.reverse()
|
||||||
|
if g[:3] in ia3: return True
|
||||||
|
if g[:2] == ia[2:]: return True
|
||||||
|
if ia[2:] in (g[:2],g[-2:]): return True
|
||||||
|
for m in ip3.finditer(host):
|
||||||
|
if int(m.group()) == ia[3]:
|
||||||
|
h = host[:m.start()] + '<3>' + host[m.end():]
|
||||||
|
break
|
||||||
|
if rehmac.search(h): return True
|
||||||
|
if host.find(''.join(a[:3])) >= 0: return True
|
||||||
|
if host.find(''.join(a[1:])) >= 0: return True
|
||||||
|
x = "%02x%02x%02x%02x" % tuple(ia)
|
||||||
|
if host.lower().find(x) >= 0: return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import fileinput
|
||||||
|
import sets
|
||||||
|
seen = sets.Set()
|
||||||
|
for ln in fileinput.input():
|
||||||
|
a = ln.split()
|
||||||
|
if a[3:5] == ['connect','from']:
|
||||||
|
host = a[5]
|
||||||
|
if host.startswith('[') and host.endswith(']'):
|
||||||
|
continue # no PTR
|
||||||
|
ip = a[7][2:-2]
|
||||||
|
if ip in seen: continue
|
||||||
|
seen.add(ip)
|
||||||
|
if is_dynip(host,ip):
|
||||||
|
print '%s\t%s DYN' % (ip,host)
|
||||||
|
else:
|
||||||
|
print '%s\t%s' % (ip,host)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import time
|
||||||
|
import shelve
|
||||||
|
import thread
|
||||||
|
import logging
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
log = logging.getLogger('milter.greylist')
|
||||||
|
|
||||||
|
def quoteAddress(s):
|
||||||
|
'''Quote an address so that it's safe to store in the file-system.
|
||||||
|
Address can either be a domain name, or local part.
|
||||||
|
Returns the quoted address.'''
|
||||||
|
|
||||||
|
s = urllib.quote(s, '@_-+~!.%')
|
||||||
|
if s.startswith('.'): s = '%2e' + s[1:]
|
||||||
|
return s
|
||||||
|
|
||||||
|
class Record(object):
|
||||||
|
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
|
||||||
|
|
||||||
|
def __init__(self,timeinc=0):
|
||||||
|
now = time.time() + timeinc
|
||||||
|
self.firstseen = now
|
||||||
|
self.lastseen = now
|
||||||
|
self.cnt = 0
|
||||||
|
self.umis = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Grey[%s:%s:%s:%d]" % (
|
||||||
|
time.ctime(self.firstseen),time.ctime(self.lastseen),
|
||||||
|
self.umis,self.cnt
|
||||||
|
)
|
||||||
|
|
||||||
|
class Greylist(object):
|
||||||
|
|
||||||
|
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
||||||
|
self.ignoreLastByte = False
|
||||||
|
self.greylist_time = grey_time * 60 # minutes
|
||||||
|
self.greylist_expire = grey_expire * 3600 # hours
|
||||||
|
self.greylist_retain = grey_retain * 24 * 3600 # days
|
||||||
|
self.dbp = shelve.open(dbname,'c',protocol=2)
|
||||||
|
self.lock = thread.allocate_lock()
|
||||||
|
|
||||||
|
def clean(self,timeinc=0):
|
||||||
|
"Delete records past the retention limit."
|
||||||
|
now = time.time() + timeinc
|
||||||
|
cnt = 0
|
||||||
|
dbp = self.dbp
|
||||||
|
for key, r in dbp.iteritems():
|
||||||
|
#print key,r,time.ctime(now)
|
||||||
|
if now > r.lastseen + self.greylist_retain:
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
r = dbp[key]
|
||||||
|
now = time.time() + timeinc
|
||||||
|
if now > r.lastseen + self.greylist_retain:
|
||||||
|
del dbp[key]
|
||||||
|
cnt += 1
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
return cnt
|
||||||
|
|
||||||
|
def check(self,ip,sender,recipient,timeinc=0):
|
||||||
|
"Return number of allowed messages for greylist triple."
|
||||||
|
sender = quoteAddress(sender)
|
||||||
|
recipient = quoteAddress(recipient)
|
||||||
|
key = ip + ':' + sender + ':' + recipient
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
dbp = self.dbp
|
||||||
|
try:
|
||||||
|
r = dbp[key]
|
||||||
|
now = time.time() + timeinc
|
||||||
|
if now > r.lastseen + self.greylist_retain:
|
||||||
|
# expired
|
||||||
|
log.debug('Expired greylist: %s',key)
|
||||||
|
r = Record(timeinc)
|
||||||
|
elif now < r.firstseen + self.greylist_time + 5:
|
||||||
|
# still greylisted
|
||||||
|
log.debug('Early greylist: %s',key)
|
||||||
|
#r = Record(timeinc)
|
||||||
|
r.lastseen = now
|
||||||
|
elif r.cnt or now < r.firstseen + self.greylist_expire:
|
||||||
|
# in greylist window or active
|
||||||
|
r.lastseen = now
|
||||||
|
r.cnt += 1
|
||||||
|
log.debug('Active greylist(%d): %s',r.cnt,key)
|
||||||
|
else:
|
||||||
|
# passed greylist window
|
||||||
|
log.debug('Late greylist: %s',key)
|
||||||
|
r = Record(timeinc)
|
||||||
|
dbp[key] = r
|
||||||
|
except:
|
||||||
|
r = Record(timeinc)
|
||||||
|
dbp[key] = r
|
||||||
|
dbp.sync()
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
return r.cnt
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.dbp.close()
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import urllib
|
||||||
|
import sqlite3
|
||||||
|
import thread
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
log = logging.getLogger('milter.greylist')
|
||||||
|
|
||||||
|
_db_lock = thread.allocate_lock()
|
||||||
|
|
||||||
|
class Greylist(object):
|
||||||
|
|
||||||
|
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
||||||
|
self.ignoreLastByte = False
|
||||||
|
self.greylist_time = grey_time * 60 # minutes
|
||||||
|
self.greylist_expire = grey_expire * 3600 # hours
|
||||||
|
self.greylist_retain = grey_retain * 24 * 3600 # days
|
||||||
|
self.conn = sqlite3.connect(dbname)
|
||||||
|
self.conn.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
self.conn.execute('''create table greylist(
|
||||||
|
ip text , sender text, recipient text,
|
||||||
|
firstseen timestamp, lastseen timestamp, cnt integer, umis text,
|
||||||
|
primary key (ip,sender,recipient))''')
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
def clean(self,timeinc=0):
|
||||||
|
"Delete records past the retention limit."
|
||||||
|
now = time.time() + timeinc - self.greylist_retain
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute('delete from greylist where lastseen < ?',(now,))
|
||||||
|
cnt = cur.rowcount
|
||||||
|
self.conn.commit()
|
||||||
|
finally: cur.close()
|
||||||
|
return cnt
|
||||||
|
|
||||||
|
def check(self,ip,sender,recipient,timeinc=0):
|
||||||
|
"Return number of allowed messages for greylist triple."
|
||||||
|
_db_lock.acquire()
|
||||||
|
cur = self.conn.execute('begin immediate')
|
||||||
|
try:
|
||||||
|
cur.execute('''select firstseen,lastseen,cnt,umis from greylist where
|
||||||
|
ip=? and sender=? and recipient=?''',(ip,sender,recipient))
|
||||||
|
r = cur.fetchone()
|
||||||
|
now = time.time() + timeinc
|
||||||
|
cnt = 0
|
||||||
|
if not r:
|
||||||
|
cur.execute('''insert into
|
||||||
|
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
|
||||||
|
values(?,?,?,?,?,?,?)''', (ip,sender,recipient,now,now,0,None))
|
||||||
|
elif now > r['lastseen'] + self.greylist_retain:
|
||||||
|
# expired
|
||||||
|
log.debug('Expired greylist: %s:%s:%s',ip,sender,recipient)
|
||||||
|
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
|
||||||
|
where ip=? and sender=? and recipient=?''',
|
||||||
|
(now,now,0,None,ip,sender,recipient))
|
||||||
|
elif now < r['firstseen'] + self.greylist_time + 5:
|
||||||
|
# still greylisted
|
||||||
|
log.debug('Early greylist: %s:%s:%s',ip,sender,recipient)
|
||||||
|
#r = Record()
|
||||||
|
cur.execute('''update greylist set lastseen=?
|
||||||
|
where ip=? and sender=? and recipient=?''',
|
||||||
|
(now,ip,sender,recipient))
|
||||||
|
elif r['cnt'] or now < r['firstseen'] + self.greylist_expire:
|
||||||
|
# in greylist window or active
|
||||||
|
cnt = r['cnt'] + 1
|
||||||
|
cur.execute('''update greylist set lastseen=?,cnt=?
|
||||||
|
where ip=? and sender=? and recipient=?''',
|
||||||
|
(now,cnt,ip,sender,recipient))
|
||||||
|
log.debug('Active greylist(%d): %s:%s:%s',cnt,ip,sender,recipient)
|
||||||
|
else:
|
||||||
|
# passed greylist window
|
||||||
|
log.debug('Late greylist: %s:%s:%s',ip,sender,recipient)
|
||||||
|
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
|
||||||
|
where ip=? and sender=? and recipient=?''',
|
||||||
|
(now,now,0,None,ip,sender,recipient))
|
||||||
|
self.conn.commit()
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
_db_lock.release()
|
||||||
|
return cnt
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.conn.close()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
|
# Copyright 2001 Business Management Systems, Inc.
|
||||||
|
# This code is under the GNU General Public License. See COPYING for details.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
class PLock(object):
|
||||||
|
"A simple /etc/passwd style lock,update,rename protocol for updating files."
|
||||||
|
def __init__(self,basename):
|
||||||
|
self.basename = basename
|
||||||
|
self.fp = None
|
||||||
|
|
||||||
|
def lock(self,lockname=None,mode=0660,strict_perms=False):
|
||||||
|
"Start an update transaction. Return FILE to write new version."
|
||||||
|
self.unlock()
|
||||||
|
if not lockname:
|
||||||
|
lockname = self.basename + '.lock'
|
||||||
|
self.lockname = lockname
|
||||||
|
try:
|
||||||
|
st = os.stat(self.basename)
|
||||||
|
mode |= st.st_mode
|
||||||
|
except OSError: pass
|
||||||
|
u = os.umask(0002)
|
||||||
|
try:
|
||||||
|
fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode)
|
||||||
|
finally:
|
||||||
|
os.umask(u)
|
||||||
|
self.fp = os.fdopen(fd,'w')
|
||||||
|
try:
|
||||||
|
os.chown(self.lockname,-1,st.st_gid)
|
||||||
|
except:
|
||||||
|
if strict_perms:
|
||||||
|
self.unlock()
|
||||||
|
raise
|
||||||
|
return self.fp
|
||||||
|
|
||||||
|
def wlock(self,lockname=None):
|
||||||
|
"Wait until lock is free, then start an update transaction."
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return self.lock(lockname)
|
||||||
|
except OSError:
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
def commit(self,backname=None):
|
||||||
|
"Commit update transaction with optional backup file."
|
||||||
|
if not self.fp:
|
||||||
|
raise IOError,"File not locked"
|
||||||
|
self.fp.close()
|
||||||
|
self.fp = None
|
||||||
|
if backname:
|
||||||
|
try:
|
||||||
|
os.remove(backname)
|
||||||
|
except OSError: pass
|
||||||
|
os.link(self.basename,backname)
|
||||||
|
os.rename(self.lockname,self.basename)
|
||||||
|
|
||||||
|
def unlock(self):
|
||||||
|
"Cancel update transaction."
|
||||||
|
if self.fp:
|
||||||
|
try:
|
||||||
|
self.fp.close()
|
||||||
|
except: pass
|
||||||
|
self.fp = None
|
||||||
|
os.remove(self.lockname)
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
"""Pure Python IP6 parsing and formatting
|
||||||
|
|
||||||
|
Copyright (c) 2006 Stuart Gathman <stuart@bmsi.com>
|
||||||
|
|
||||||
|
This module is free software, and you may redistribute it and/or modify
|
||||||
|
it under the same terms as Python itself, so long as this copyright message
|
||||||
|
and disclaimer are retained in their original form.
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
#from spf import RE_IP4
|
||||||
|
import re
|
||||||
|
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
|
||||||
|
RE_IP4 = re.compile(PAT_IP4+'$')
|
||||||
|
|
||||||
|
def inet_ntop(s):
|
||||||
|
"""
|
||||||
|
Convert ip6 address to standard hex notation.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0xFFFF,0x0102,0x0304))
|
||||||
|
'::FFFF:1.2.3.4'
|
||||||
|
|
||||||
|
>>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0,0,0,0x0102,0x0304))
|
||||||
|
'1234:5678::102:304'
|
||||||
|
|
||||||
|
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0x1234,0x5678,0,0x0102,0x0304))
|
||||||
|
'::1234:5678:0:102:304'
|
||||||
|
|
||||||
|
>>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0x0102,0x0304,0,0,0))
|
||||||
|
'1234:5678:0:102:304::'
|
||||||
|
|
||||||
|
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0,0,0))
|
||||||
|
'::'
|
||||||
|
"""
|
||||||
|
# convert to 8 words
|
||||||
|
a = struct.unpack("!HHHHHHHH",s)
|
||||||
|
n = (0,0,0,0,0,0,0,0) # null ip6
|
||||||
|
if a == n: return '::'
|
||||||
|
# check for ip4 mapped
|
||||||
|
if a[:5] == (0,0,0,0,0) and a[5] in (0,0xFFFF):
|
||||||
|
ip4 = '.'.join([str(i) for i in struct.unpack("!BBBB",s[12:])])
|
||||||
|
if a[5]:
|
||||||
|
return "::FFFF:" + ip4
|
||||||
|
return "::" + ip4
|
||||||
|
# find index of longest sequence of 0
|
||||||
|
for l in (7,6,5,4,3,2,1):
|
||||||
|
e = n[:l]
|
||||||
|
for i in range(9-l):
|
||||||
|
if a[i:i+l] == e:
|
||||||
|
if i == 0:
|
||||||
|
return ':'+':%x'*(8-l) % a[l:]
|
||||||
|
if i == 8 - l:
|
||||||
|
return '%x:'*(8-l) % a[:-l] + ':'
|
||||||
|
return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:]
|
||||||
|
return "%x:%x:%x:%x:%x:%x:%x:%x" % a
|
||||||
|
|
||||||
|
def inet_pton(p):
|
||||||
|
"""
|
||||||
|
Convert ip6 standard hex notation to ip6 address.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
>>> struct.unpack('!HHHHHHHH',inet_pton('::'))
|
||||||
|
(0, 0, 0, 0, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
>>> struct.unpack('!HHHHHHHH',inet_pton('::1234'))
|
||||||
|
(0, 0, 0, 0, 0, 0, 0, 4660)
|
||||||
|
|
||||||
|
>>> struct.unpack('!HHHHHHHH',inet_pton('1234::'))
|
||||||
|
(4660, 0, 0, 0, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
>>> struct.unpack('!HHHHHHHH',inet_pton('1234::5678'))
|
||||||
|
(4660, 0, 0, 0, 0, 0, 0, 22136)
|
||||||
|
|
||||||
|
>>> struct.unpack('!HHHHHHHH',inet_pton('::FFFF:1.2.3.4'))
|
||||||
|
(0, 0, 0, 0, 0, 65535, 258, 772)
|
||||||
|
|
||||||
|
>>> struct.unpack('!HHHHHHHH',inet_pton('1.2.3.4'))
|
||||||
|
(0, 0, 0, 0, 0, 65535, 258, 772)
|
||||||
|
|
||||||
|
>>> try: inet_pton('::1.2.3.4.5')
|
||||||
|
... except ValueError,x: print x
|
||||||
|
::1.2.3.4.5
|
||||||
|
"""
|
||||||
|
if p == '::':
|
||||||
|
return '\0'*16
|
||||||
|
s = p
|
||||||
|
m = RE_IP4.search(s)
|
||||||
|
try:
|
||||||
|
if m:
|
||||||
|
pos = m.start()
|
||||||
|
ip4 = [int(i) for i in s[pos:].split('.')]
|
||||||
|
if not pos:
|
||||||
|
return struct.pack('!QLBBBB',0,65535,*ip4)
|
||||||
|
s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4)
|
||||||
|
a = s.split('::')
|
||||||
|
if len(a) == 2:
|
||||||
|
l,r = a
|
||||||
|
if not l:
|
||||||
|
r = r.split(':')
|
||||||
|
return struct.pack('!HHHHHHHH',
|
||||||
|
*[0]*(8-len(r)) + [int(s,16) for s in r])
|
||||||
|
if not r:
|
||||||
|
l = l.split(':')
|
||||||
|
return struct.pack('!HHHHHHHH',
|
||||||
|
*[int(s,16) for s in l] + [0]*(8-len(l)))
|
||||||
|
l = l.split(':')
|
||||||
|
r = r.split(':')
|
||||||
|
return struct.pack('!HHHHHHHH',
|
||||||
|
*[int(s,16) for s in l] + [0]*(8-len(l)-len(r))
|
||||||
|
+ [int(s,16) for s in r])
|
||||||
|
if len(a) == 1:
|
||||||
|
return struct.pack('!HHHHHHHH',
|
||||||
|
*[int(s,16) for s in a[0].split(':')])
|
||||||
|
except ValueError: pass
|
||||||
|
raise ValueError,p
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
## @package Milter.test
|
||||||
|
# A test framework for milters
|
||||||
|
|
||||||
|
import rfc822
|
||||||
|
import StringIO
|
||||||
|
import Milter
|
||||||
|
|
||||||
|
Milter.NOREPLY = Milter.CONTINUE
|
||||||
|
|
||||||
|
## Test mixin for unit testing milter applications.
|
||||||
|
# This mixin overrides many Milter.MilterBase methods
|
||||||
|
# with stub versions that simply record what was done.
|
||||||
|
# @since 0.9.8
|
||||||
|
class TestBase(object):
|
||||||
|
|
||||||
|
def __init__(self,logfile='test/milter.log'):
|
||||||
|
self._protocol = 0
|
||||||
|
self.logfp = open(logfile,"a")
|
||||||
|
## List of recipients deleted
|
||||||
|
self._delrcpt = []
|
||||||
|
## List of recipients added
|
||||||
|
self._addrcpt = []
|
||||||
|
## Macros defined
|
||||||
|
self._macros = { }
|
||||||
|
## The message body.
|
||||||
|
self._body = None
|
||||||
|
## True if the milter replaced the message body.
|
||||||
|
self._bodyreplaced = False
|
||||||
|
## True if the milter changed any headers.
|
||||||
|
self._headerschanged = False
|
||||||
|
## Reply codes and messages set by milter
|
||||||
|
self._reply = None
|
||||||
|
## The rfc822 message object for the current email being fed to the milter.
|
||||||
|
self._msg = None
|
||||||
|
self._symlist = [ None, None, None, None, None, None, None ]
|
||||||
|
|
||||||
|
def log(self,*msg):
|
||||||
|
for i in msg: print >>self.logfp, i,
|
||||||
|
print >>self.logfp
|
||||||
|
|
||||||
|
## Set a macro value.
|
||||||
|
# These are retrieved by the milter with getsymval.
|
||||||
|
# @param name the macro name, as passed to getsymval
|
||||||
|
# @param val the macro value
|
||||||
|
def setsymval(self,name,val):
|
||||||
|
self._macros[name] = val
|
||||||
|
|
||||||
|
def getsymval(self,name):
|
||||||
|
# FIXME: track stage, and use _symlist
|
||||||
|
return self._macros.get(name,'')
|
||||||
|
|
||||||
|
def replacebody(self,chunk):
|
||||||
|
if self._body:
|
||||||
|
self._body.write(chunk)
|
||||||
|
self._bodyreplaced = True
|
||||||
|
else:
|
||||||
|
raise IOError,"replacebody not called from eom()"
|
||||||
|
|
||||||
|
# FIXME: rfc822 indexing does not really reflect the way chg/add header
|
||||||
|
# work for a milter
|
||||||
|
def chgheader(self,field,idx,value):
|
||||||
|
if not self._body:
|
||||||
|
raise IOError,"chgheader not called from eom()"
|
||||||
|
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
|
||||||
|
if value == '':
|
||||||
|
del self._msg[field]
|
||||||
|
else:
|
||||||
|
self._msg[field] = value
|
||||||
|
self._headerschanged = True
|
||||||
|
|
||||||
|
def addheader(self,field,value,idx=-1):
|
||||||
|
if not self._body:
|
||||||
|
raise IOError,"addheader not called from eom()"
|
||||||
|
self.log('addheader: %s=%s' % (field,value))
|
||||||
|
self._msg[field] = value
|
||||||
|
self._headerschanged = True
|
||||||
|
|
||||||
|
def delrcpt(self,rcpt):
|
||||||
|
if not self._body:
|
||||||
|
raise IOError,"delrcpt not called from eom()"
|
||||||
|
self._delrcpt.append(rcpt)
|
||||||
|
|
||||||
|
def addrcpt(self,rcpt):
|
||||||
|
if not self._body:
|
||||||
|
raise IOError,"addrcpt not called from eom()"
|
||||||
|
self._addrcpt.append(rcpt)
|
||||||
|
|
||||||
|
## Save the reply codes and messages in self._reply.
|
||||||
|
def setreply(self,rcode,xcode,*msg):
|
||||||
|
self._reply = (rcode,xcode) + msg
|
||||||
|
|
||||||
|
def setsymlist(self,stage,macros):
|
||||||
|
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
|
||||||
|
# not used yet, but just for grins we save the data
|
||||||
|
a = []
|
||||||
|
for m in macros:
|
||||||
|
try:
|
||||||
|
m = m.encode('utf8')
|
||||||
|
except: pass
|
||||||
|
try:
|
||||||
|
m = m.split(' ')
|
||||||
|
except: pass
|
||||||
|
a += m
|
||||||
|
self._symlist[stage] = set(a)
|
||||||
|
|
||||||
|
## Feed a file like object to the milter. Calls envfrom, envrcpt for
|
||||||
|
# each recipient, header for each header field, body for each body
|
||||||
|
# block, and finally eom. A return code from the milter other than
|
||||||
|
# CONTINUE returns immediately with that return code.
|
||||||
|
#
|
||||||
|
# This is a convenience method, a test could invoke the callbacks
|
||||||
|
# in sequence on its own - and for some complex tests, this may
|
||||||
|
# be necessary.
|
||||||
|
# @param fp the file with rfc2822 message stream
|
||||||
|
# @param sender the MAIL FROM
|
||||||
|
# @param rcpt RCPT TO - additional recipients may follow
|
||||||
|
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
|
||||||
|
self._body = None
|
||||||
|
self._bodyreplaced = False
|
||||||
|
self._headerschanged = False
|
||||||
|
self._reply = None
|
||||||
|
msg = rfc822.Message(fp)
|
||||||
|
rc = self.envfrom('<%s>'%sender)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
for rcpt in (rcpt,) + rcpts:
|
||||||
|
rc = self.envrcpt('<%s>'%rcpt)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
line = None
|
||||||
|
for h in msg.headers:
|
||||||
|
if h[:1].isspace():
|
||||||
|
line = line + h
|
||||||
|
continue
|
||||||
|
if not line:
|
||||||
|
line = h
|
||||||
|
continue
|
||||||
|
s = line.split(': ',1)
|
||||||
|
if len(s) > 1: val = s[1].strip()
|
||||||
|
else: val = ''
|
||||||
|
rc = self.header(s[0],val)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
line = h
|
||||||
|
if line:
|
||||||
|
s = line.split(': ',1)
|
||||||
|
rc = self.header(s[0],s[1])
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
rc = self.eoh()
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
while 1:
|
||||||
|
buf = fp.read(8192)
|
||||||
|
if len(buf) == 0: break
|
||||||
|
rc = self.body(buf)
|
||||||
|
if rc != Milter.CONTINUE: return rc
|
||||||
|
self._msg = msg
|
||||||
|
self._body = StringIO.StringIO()
|
||||||
|
rc = self.eom()
|
||||||
|
if self._bodyreplaced:
|
||||||
|
body = self._body.getvalue()
|
||||||
|
else:
|
||||||
|
msg.rewindbody()
|
||||||
|
body = msg.fp.read()
|
||||||
|
self._body = StringIO.StringIO()
|
||||||
|
self._body.writelines(msg.headers)
|
||||||
|
self._body.write('\n')
|
||||||
|
self._body.write(body)
|
||||||
|
return rc
|
||||||
|
|
||||||
|
## Feed an email contained in a file to the milter.
|
||||||
|
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
|
||||||
|
# @param sender MAIL FROM
|
||||||
|
# @param rcpts RCPT TO, multiple recipients may be supplied
|
||||||
|
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
|
||||||
|
with open('test/'+fname,'r') as fp:
|
||||||
|
return self.feedFile(fp,sender,*rcpts)
|
||||||
|
|
||||||
|
## Call the connect and helo callbacks.
|
||||||
|
# The helo callback is not called if connect does not return CONTINUE.
|
||||||
|
# @param host the hostname passed to the connect callback
|
||||||
|
# @param helo the hostname passed to the helo callback
|
||||||
|
# @param ip the IP address passed to the connect callback
|
||||||
|
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
|
||||||
|
self._body = None
|
||||||
|
self._bodyreplaced = False
|
||||||
|
opts = [ Milter.CURR_ACTS,~0,0,0 ]
|
||||||
|
rc = self.negotiate(opts)
|
||||||
|
rc = super(TestBase,self).connect(host,1,(ip,1234))
|
||||||
|
if rc != Milter.CONTINUE:
|
||||||
|
self.close()
|
||||||
|
return rc
|
||||||
|
rc = self.hello(helo)
|
||||||
|
if rc != Milter.CONTINUE:
|
||||||
|
self.close()
|
||||||
|
return rc
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
|
# Copyright 2005 Business Management Systems, Inc.
|
||||||
|
# This code is under the GNU General Public License. See COPYING for details.
|
||||||
|
|
||||||
|
# The localpart of SMTP return addresses is often signed. The format
|
||||||
|
# of the signing is application specific and doesn't concern us -
|
||||||
|
# except that we wish to extract some sort of fixed string from
|
||||||
|
# the variable signature which represents the "source" of the message.
|
||||||
|
|
||||||
|
def unsign(s):
|
||||||
|
"""Attempt to unsign localpart and return original email.
|
||||||
|
No attempt is made to verify the signature.
|
||||||
|
>>> unsign('SRS0=8Y3CZ=3U=jsconnor.com=bills@bmsi.com')
|
||||||
|
'bills@jsconnor.com'
|
||||||
|
"""
|
||||||
|
# not implemented yet
|
||||||
|
return s
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
## @package Milter.utils
|
||||||
|
# Miscellaneous functions.
|
||||||
|
#
|
||||||
|
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import socket
|
||||||
|
import email.Errors
|
||||||
|
from fnmatch import fnmatchcase
|
||||||
|
from email.Header import decode_header
|
||||||
|
#import email.Utils
|
||||||
|
import rfc822
|
||||||
|
|
||||||
|
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
|
||||||
|
ip4re = re.compile(PAT_IP4+'$')
|
||||||
|
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
|
||||||
|
'|::(?:%(hex4)s:){5}%(ls32)s$'
|
||||||
|
'|(?:%(hex4)s)?::(?:%(hex4)s:){4}%(ls32)s$'
|
||||||
|
'|(?:(?:%(hex4)s:){0,1}%(hex4)s)?::(?:%(hex4)s:){3}%(ls32)s$'
|
||||||
|
'|(?:(?:%(hex4)s:){0,2}%(hex4)s)?::(?:%(hex4)s:){2}%(ls32)s$'
|
||||||
|
'|(?:(?:%(hex4)s:){0,3}%(hex4)s)?::%(hex4)s:%(ls32)s$'
|
||||||
|
'|(?:(?:%(hex4)s:){0,4}%(hex4)s)?::%(ls32)s$'
|
||||||
|
'|(?:(?:%(hex4)s:){0,5}%(hex4)s)?::%(hex4)s$'
|
||||||
|
'|(?:(?:%(hex4)s:){0,6}%(hex4)s)?::$'
|
||||||
|
% {
|
||||||
|
'ls32': r'(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|%s)'%PAT_IP4,
|
||||||
|
'hex4': r'[0-9a-f]{1,4}'
|
||||||
|
}, re.IGNORECASE)
|
||||||
|
|
||||||
|
# from spf.py
|
||||||
|
def addr2bin(s):
|
||||||
|
"""Convert a string IPv4 address into an unsigned integer."""
|
||||||
|
if s.find(':') >= 0:
|
||||||
|
try:
|
||||||
|
return bin2long6(inet_pton(s))
|
||||||
|
except:
|
||||||
|
raise socket.error("Invalid IP6 address: "+s)
|
||||||
|
try:
|
||||||
|
return struct.unpack("!L", socket.inet_aton(s))[0]
|
||||||
|
except socket.error:
|
||||||
|
raise socket.error("Invalid IP4 address: "+s)
|
||||||
|
|
||||||
|
def bin2long6(s):
|
||||||
|
"""Convert binary IP6 address into an unsigned Python long integer."""
|
||||||
|
h, l = struct.unpack("!QQ", s)
|
||||||
|
return h << 64 | l
|
||||||
|
|
||||||
|
if hasattr(socket,'has_ipv6') and socket.has_ipv6:
|
||||||
|
def inet_ntop(s):
|
||||||
|
return socket.inet_ntop(socket.AF_INET6,s)
|
||||||
|
def inet_pton(s):
|
||||||
|
return socket.inet_pton(socket.AF_INET6,s.strip())
|
||||||
|
else:
|
||||||
|
from pyip6 import inet_ntop, inet_pton
|
||||||
|
|
||||||
|
MASK = 0xFFFFFFFFL
|
||||||
|
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL
|
||||||
|
|
||||||
|
def cidr(i,n,mask=MASK):
|
||||||
|
return ~(mask >> n) & mask & i
|
||||||
|
|
||||||
|
def iniplist(ipaddr,iplist):
|
||||||
|
"""Return whether ip is in cidr list
|
||||||
|
>>> iniplist('66.179.26.146',['127.0.0.1','66.179.26.128/26'])
|
||||||
|
True
|
||||||
|
>>> iniplist('127.0.0.1',['127.0.0.1','66.179.26.128/26'])
|
||||||
|
True
|
||||||
|
>>> iniplist('192.168.0.45',['192.168.0.*'])
|
||||||
|
True
|
||||||
|
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
||||||
|
True
|
||||||
|
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
|
||||||
|
"""
|
||||||
|
if ip4re.match(ipaddr):
|
||||||
|
ipnum = addr2bin(ipaddr)
|
||||||
|
elif ip6re.match(ipaddr):
|
||||||
|
ipnum = bin2long6(inet_pton(ipaddr))
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid ip syntax:'+ipaddr)
|
||||||
|
for pat in iplist:
|
||||||
|
p = pat.split('/',1)
|
||||||
|
if ip4re.match(p[0]):
|
||||||
|
if len(p) > 1:
|
||||||
|
n = int(p[1])
|
||||||
|
else:
|
||||||
|
n = 32
|
||||||
|
if cidr(addr2bin(p[0]),n) == cidr(ipnum,n):
|
||||||
|
return True
|
||||||
|
elif ip6re.match(p[0]):
|
||||||
|
if len(p) > 1:
|
||||||
|
n = int(p[1])
|
||||||
|
else:
|
||||||
|
n = 128
|
||||||
|
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
|
||||||
|
return True
|
||||||
|
elif fnmatchcase(ipaddr,pat):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
## Split email into Fullname and address.
|
||||||
|
# This replaces <code>email.Utils.parseaddr</code> but fixes
|
||||||
|
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
|
||||||
|
#
|
||||||
|
def parseaddr(t):
|
||||||
|
"""Split email into Fullname and address.
|
||||||
|
|
||||||
|
>>> parseaddr('user@example.com')
|
||||||
|
('', 'user@example.com')
|
||||||
|
>>> parseaddr('"Full Name" <foo@example.com>')
|
||||||
|
('Full Name', 'foo@example.com')
|
||||||
|
>>> parseaddr('spam@spammer.com <foo@example.com>')
|
||||||
|
('spam@spammer.com', 'foo@example.com')
|
||||||
|
>>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>')
|
||||||
|
('God@heaven', 'jeff@spec.org')
|
||||||
|
>>> parseaddr('Real Name ((comment)) <addr...@example.com>')
|
||||||
|
('Real Name', 'addr...@example.com')
|
||||||
|
>>> parseaddr('a(WRONG)@b')
|
||||||
|
('WRONG', 'a@b')
|
||||||
|
"""
|
||||||
|
#return email.Utils.parseaddr(t)
|
||||||
|
res = rfc822.parseaddr(t)
|
||||||
|
# dirty fix for some broken cases
|
||||||
|
if not res[0]:
|
||||||
|
pos = t.find('<')
|
||||||
|
if pos > 0 and t[-1] == '>':
|
||||||
|
addrspec = t[pos+1:-1]
|
||||||
|
pos1 = addrspec.rfind(':')
|
||||||
|
if pos1 > 0:
|
||||||
|
addrspec = addrspec[pos1+1:]
|
||||||
|
return rfc822.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
|
||||||
|
if not res[1]:
|
||||||
|
pos = t.find('<')
|
||||||
|
if pos > 0 and t[-1] == '>':
|
||||||
|
addrspec = t[pos+1:-1]
|
||||||
|
pos1 = addrspec.rfind(':')
|
||||||
|
if pos1 > 0:
|
||||||
|
addrspec = addrspec[pos1+1:]
|
||||||
|
return rfc822.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def parse_addr(t):
|
||||||
|
"""Split email into user,domain.
|
||||||
|
|
||||||
|
>>> parse_addr('user@example.com')
|
||||||
|
['user', 'example.com']
|
||||||
|
>>> parse_addr('"user@example.com"')
|
||||||
|
['user@example.com']
|
||||||
|
>>> parse_addr('"user@bar"@example.com')
|
||||||
|
['user@bar', 'example.com']
|
||||||
|
>>> parse_addr('foo')
|
||||||
|
['foo']
|
||||||
|
>>> parse_addr('@mx.example.com:user@example.com')
|
||||||
|
['user', 'example.com']
|
||||||
|
>>> parse_addr('@user@example.com')
|
||||||
|
['@user', 'example.com']
|
||||||
|
"""
|
||||||
|
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
|
||||||
|
if t.startswith('"'):
|
||||||
|
if t.endswith('"'): return [t[1:-1]]
|
||||||
|
pos = t.find('"@')
|
||||||
|
if pos > 0: return [t[1:pos],t[pos+2:]]
|
||||||
|
if t.startswith('@'):
|
||||||
|
try: t = t.split(':',1)[1]
|
||||||
|
except IndexError: pass
|
||||||
|
return t.rsplit('@',1)
|
||||||
|
|
||||||
|
## Decode headers gratuitously encoded to hide the content.
|
||||||
|
# Spammers often encode headers to obscure the content from
|
||||||
|
# spam filters. This function decodes gratuitously encoded
|
||||||
|
# headers.
|
||||||
|
# @param val the raw header value
|
||||||
|
# @return the decoded value or the original raw value
|
||||||
|
|
||||||
|
def parse_header(val):
|
||||||
|
"""Decode headers gratuitously encoded to hide the content.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
h = decode_header(val)
|
||||||
|
if not len(h) or (not h[0][1] and len(h) == 1): return val
|
||||||
|
u = []
|
||||||
|
for s,enc in h:
|
||||||
|
if enc:
|
||||||
|
try:
|
||||||
|
u.append(unicode(s,enc,'replace'))
|
||||||
|
except LookupError:
|
||||||
|
u.append(unicode(s))
|
||||||
|
else:
|
||||||
|
u.append(unicode(s))
|
||||||
|
u = ''.join(u)
|
||||||
|
for enc in ('us-ascii','iso-8859-1','utf8'):
|
||||||
|
try:
|
||||||
|
return u.encode(enc)
|
||||||
|
except UnicodeError: continue
|
||||||
|
except UnicodeDecodeError: pass
|
||||||
|
except LookupError: pass
|
||||||
|
except ValueError: pass
|
||||||
|
except email.Errors.HeaderParseError: pass
|
||||||
|
return val
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
Here is a history of user visible changes to Python milter.
|
|
||||||
|
|
||||||
0.8.0 Move Milter module to subpackage.
|
|
||||||
DSN support for Three strikes rule and SPF SOFTFAIL
|
|
||||||
Move /*mime*/ and dynip to Milter subpackage
|
|
||||||
Fix SPF unknown mechanism list not cleared
|
|
||||||
Make banned extensions configurable.
|
|
||||||
Option to scan zipfiles for bad extensions.
|
|
||||||
Properly log pydspam exceptions
|
|
||||||
0.7.3 Experimental release with python2.4 support
|
|
||||||
0.7.2 Return unknown for invalid ip address in mechanism
|
|
||||||
Recognize dynamic PTR names, and don't count them as authentication.
|
|
||||||
Three strikes and yer out rule.
|
|
||||||
Block softfail by default when no PTR or HELO
|
|
||||||
Return unknown for null mechanism
|
|
||||||
Try best guess on HELO also
|
|
||||||
Expand setreply for common errors
|
|
||||||
make rhsbl.m4 hack available for sendmail.mc
|
|
||||||
0.7.1 Handle modifying mislabeled multipart messages without an exception
|
|
||||||
Support setbacklog, setmlreply
|
|
||||||
Allow multi-recipient CBV
|
|
||||||
Return TEMPFAIL for SPF softfail
|
|
||||||
0.7.0 SPF check hello name
|
|
||||||
Move pythonsock to /var/run/milter
|
|
||||||
Move milter.cfg to /etc/mail/pymilter.cfg
|
|
||||||
Check M$ style XML CID records by converting to SPF
|
|
||||||
Recognize, but never match ip6 - until we properly support it.
|
|
||||||
Option to reject when no PTR and no SPF
|
|
||||||
0.6.9 Reject invalid SRS immediately for benefit of callback verifiers
|
|
||||||
Fix include bug in spf.py
|
|
||||||
Fix check_header bug
|
|
||||||
Fix setup.py to work with python < 2.2.3, thanks to Eric S. Johansson
|
|
||||||
Test driver for SPF test suite. Fix bugs and add features to
|
|
||||||
pass most of test suite.
|
|
||||||
Use best_guess() and get_header() in bms.py for SPF support
|
|
||||||
0.6.8 Defang message/rfc822 content_type with boundary
|
|
||||||
Support SPF delegation
|
|
||||||
Reject neutral SPF result for selected domains
|
|
||||||
Support SPF default (best_guess)
|
|
||||||
Don't report "spoofed" unless rcpt looks like SRS
|
|
||||||
Check for bounce with multiple rcpts
|
|
||||||
Make dspam see Received-SPF headers
|
|
||||||
Fix sysv init for Redhat 9 and other single ps line per process systems
|
|
||||||
0.6.7 Fix failure to remove explicit unix socket thanks to Alexander again.
|
|
||||||
Support SRS forgery detection.
|
|
||||||
Detect thread resource starvation in Milter.py.
|
|
||||||
Decode obfuscated subject headers.
|
|
||||||
0.6.6 Another memory leak plugged by Alexander Kourakos.
|
|
||||||
Support SPF checking: http://spf.pobox.com
|
|
||||||
Hello blacklist
|
|
||||||
RPM compiled for python2.3 and sendmail-8.12
|
|
||||||
0.6.5 Plug memory leak in wrap_connect thanks to Alexander Kourakos.
|
|
||||||
Support progress notification.
|
|
||||||
Log Received header for trusted relay.
|
|
||||||
Support wildcard user for smart alias.
|
|
||||||
0.6.4 Exempt entire domains.
|
|
||||||
Tweak SMTP error codes reported.
|
|
||||||
Suppress traceback for Dspam lock timeouts.
|
|
||||||
Dspam internal mail for dspam users.
|
|
||||||
Match hostname for internal connection test, even if no ipaddr.
|
|
||||||
Fix for not saving defang of false positive triggered rejecting it
|
|
||||||
as a virus from self.
|
|
||||||
Size limit for dspam to work around dspam-2.6.5.2 bug.
|
|
||||||
(dspam-2.8 still showstopper buggy for libdspam API.)
|
|
||||||
Whitelist for dspam.
|
|
||||||
Reject list for dspam (REJECT rather than quarantine SCREENed
|
|
||||||
spam for listed domains).
|
|
||||||
Report dspam header changes to sendmail, fix headerChange
|
|
||||||
to handle deleting absent header.
|
|
||||||
dspam feature requires pydspam-1.1.5
|
|
||||||
0.6.3 dspam screening (with pydspam-1.1.4)
|
|
||||||
Don't write "defang" file for false positive feedback
|
|
||||||
0.6.2 Work around email package bug in get_filename().
|
|
||||||
add dspam_exempt list to milter.cfg
|
|
||||||
REJECT messages with missing MIME boundaries (almost always spam)
|
|
||||||
DISCARD messages which any dspam user flags as spam
|
|
||||||
start.sh was calling python instead of python2 on Linux
|
|
||||||
0.6.1 Work with python-2.2.3
|
|
||||||
Integrate full dspam application
|
|
||||||
0.6.0 Use email package in python-2.2.2
|
|
||||||
0.5.6 Include dspam interface for Bayesian filtering
|
|
||||||
0.5.5 Allow passing None to setreply and chgheader thanks to George Graf.
|
|
||||||
Experimental IPv6 support thanks to Deron Meranda.
|
|
||||||
Allow removing callbacks by passing None to set_XXX_callback.
|
|
||||||
Recognize internal connections in bms.py.
|
|
||||||
Give users a clue when rejecting banned subjects.
|
|
||||||
0.5.4 Wiretap redirection feature, smart alias feature, QUARANTINE support
|
|
||||||
0.5.3 Tweak to run under 2.2 in production
|
|
||||||
0.5.2 Fix and add to unit test another parsing failure.
|
|
||||||
0.5.1 Properly handle modifications to rfc822 attachments.
|
|
||||||
Handle encoded rfc822 attachments.
|
|
||||||
0.5.0 Use config file so users don't have to keep syncing the
|
|
||||||
bms.py script. Keep bms.py marked as %config for a while
|
|
||||||
to avoid wiping out their customizations just yet.
|
|
||||||
0.4.5 Work with sgmlop package to speed up HTML parsing.
|
|
||||||
Reduce various local hacks to config variables.
|
|
||||||
0.4.4 Bug fixes for HTML encoding.
|
|
||||||
0.4.3 Handle quoted-printable HTML attachments. Remove entire
|
|
||||||
attachment when HTML can't be parsed.
|
|
||||||
0.4.2 Parse HTML attachments to remove <script ...>...</script>.
|
|
||||||
Klez virus uses malformed MIME part separators to prevent
|
|
||||||
the multifile module and other virus scanners from seeing its
|
|
||||||
HTML attachment (which contains Javascript and VBScript). Outhouse
|
|
||||||
happily accepts and executes the malformed attachments, but
|
|
||||||
we still kill the Klez virus because we:
|
|
||||||
Defang attachment when any Content-Type attribute ends with
|
|
||||||
a banned extension - one of the Outhouse bugs exploited by the
|
|
||||||
Klez virus. Outhouse really, really stinks . . .
|
|
||||||
0.4.1 Bug fix from Jason Erikson for NULL hostaddr in connect callback.
|
|
||||||
0.4.0 New check_attachments(msg,check) function in mime module allows
|
|
||||||
filtering based on attachment contents. Distribution now includes
|
|
||||||
bms.py, an example milter used in production - including use of the
|
|
||||||
new check_attachments(msg,check) API.
|
|
||||||
Report hostname in WARNING.TXT.
|
|
||||||
More parameter list bug fixes.
|
|
||||||
|
|
||||||
0.3.10 Parse quotes in parameter lists to handle embedded ';'.
|
|
||||||
Move test data to subdirectory, write non-junit output to
|
|
||||||
log file in test subdirectory.
|
|
||||||
0.3.9 Handle non-multipart messages with executable content in sample.py,
|
|
||||||
add more extensions to banned list.
|
|
||||||
0.3.8 Handle malformed Content-Type in mime.py. Test viruses have
|
|
||||||
been deactivated by deleting most of the viral code.
|
|
||||||
0.3.7 Put back hint on running sample.py. Add .bat as banned extension.
|
|
||||||
More sample spam filtering logic.
|
|
||||||
0.3.6 Ran through pychecker-0.8.5. Most systems will name the sendmail
|
|
||||||
user library (used by the milter extension module) 'libsm', but AIX
|
|
||||||
still needs to call it 'libsmutil' because there is a system library
|
|
||||||
called 'libsm'.
|
|
||||||
0.3.5 Enhanced logging. Fix bug in sample milter where headers were
|
|
||||||
included in body when removing a virus.
|
|
||||||
0.3.4 Tested distribution on RH6.2 and updated sample.py and docs.
|
|
||||||
Tested with gcc-2.95.2, python-2.1.1, sendmail-8.11.6-2.6.x
|
|
||||||
The RH6.2 spec file to enable libmilter for sendmail-8.11.6
|
|
||||||
can be obtained from http://www.bmsi.com/linux/sendmail-rhmilter.spec
|
|
||||||
The SRPM can be obtained from http://www.redhat.com
|
|
||||||
|
|
||||||
0.3.3 Remove reference to sa_len - not supported by linux.
|
|
||||||
|
|
||||||
0.3.2 Rename and add more hints to the sample milter.
|
|
||||||
|
|
||||||
0.3.1 Pass a more useful hostaddr to the connect callback.
|
|
||||||
|
|
||||||
0.3 Interface now uses a milterContext extension object instead of
|
|
||||||
an index. A PyThreadContext is now created for each milterContext so that
|
|
||||||
"simultaneously" processing multiple messages at once (as often happens
|
|
||||||
on a busy server) actually works.
|
|
||||||
|
|
||||||
Many milter methods are now object methods of the milterContext
|
|
||||||
extension object. No compatibility API is provided for this change due
|
|
||||||
to the limited user base at this stage. The setname method has been removed,
|
|
||||||
and the name is now passed to register.
|
|
||||||
|
|
||||||
A simple class to provide an OO wrapper to the milter API is
|
|
||||||
provided.
|
|
||||||
|
|
||||||
A simple class to parse multipart mime messages into parts and replace
|
|
||||||
selected parts is provided. The sample filter will eventually use the mimelib
|
|
||||||
package instead, but mimelib currently requires reading the entire message
|
|
||||||
into memory.
|
|
||||||
|
|
||||||
A sample filter that replaces attachments with naughty extensions
|
|
||||||
with a warning message is provided.
|
|
||||||
-253
@@ -1,253 +0,0 @@
|
|||||||
%define name milter
|
|
||||||
%define version 0.8.0
|
|
||||||
%define release 3.RH7
|
|
||||||
# what version of RH are we building for?
|
|
||||||
%define redhat9 0
|
|
||||||
%define redhat7 1
|
|
||||||
%define redhat6 0
|
|
||||||
|
|
||||||
# Options for Redhat version 6.x:
|
|
||||||
# rpm -ba|--rebuild --define "rh6 1"
|
|
||||||
%{?rh6:%define redhat7 0}
|
|
||||||
%{?rh6:%define redhat6 1}
|
|
||||||
|
|
||||||
# some systems dont have initrddir defined
|
|
||||||
%{?_initrddir:%define _initrddir /etc/rc.d/init.d}
|
|
||||||
|
|
||||||
%if %{redhat9}
|
|
||||||
%define sysvinit milter.rc
|
|
||||||
%else # Redhat 7.x and earlier (multiple ps lines per thread)
|
|
||||||
%define sysvinit milter.rc7
|
|
||||||
%endif
|
|
||||||
# RH9, other systems (single ps line per process)
|
|
||||||
%ifos Linux
|
|
||||||
%define python python2.4
|
|
||||||
%else
|
|
||||||
%define python python
|
|
||||||
%endif
|
|
||||||
|
|
||||||
Summary: Python interface to sendmail milter API
|
|
||||||
Name: %{name}
|
|
||||||
Version: %{version}
|
|
||||||
Release: %{release}
|
|
||||||
Source: %{name}-%{version}.tar.gz
|
|
||||||
#Patch: %{name}-%{version}.patch
|
|
||||||
Copyright: GPL
|
|
||||||
Group: Development/Libraries
|
|
||||||
BuildRoot: %{_tmppath}/%{name}-buildroot
|
|
||||||
Prefix: %{_prefix}
|
|
||||||
Vendor: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
Packager: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
Url: http://www.bmsi.com/python/milter.html
|
|
||||||
Requires: %{python} >= 2.4, sendmail >= 8.12.10
|
|
||||||
%ifos Linux
|
|
||||||
Requires: chkconfig
|
|
||||||
%endif
|
|
||||||
BuildRequires: %{python}-devel , sendmail-devel >= 8.12.10
|
|
||||||
|
|
||||||
%description
|
|
||||||
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.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup
|
|
||||||
#%patch -p1
|
|
||||||
|
|
||||||
%build
|
|
||||||
env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build
|
|
||||||
|
|
||||||
%install
|
|
||||||
rm -rf $RPM_BUILD_ROOT
|
|
||||||
%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
|
|
||||||
mkdir -p $RPM_BUILD_ROOT/var/log/milter
|
|
||||||
mkdir -p $RPM_BUILD_ROOT/etc/mail
|
|
||||||
mkdir $RPM_BUILD_ROOT/var/log/milter/save
|
|
||||||
cp bms.py strike3.txt softfail.txt $RPM_BUILD_ROOT/var/log/milter
|
|
||||||
cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg
|
|
||||||
|
|
||||||
# logfile rotation
|
|
||||||
mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d
|
|
||||||
cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF'
|
|
||||||
/var/log/milter/milter.log {
|
|
||||||
copytruncate
|
|
||||||
compress
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# purge saved defanged message copies
|
|
||||||
mkdir -p $RPM_BUILD_ROOT/etc/cron.daily
|
|
||||||
%ifos aix4.1
|
|
||||||
R=
|
|
||||||
%else
|
|
||||||
R='-r'
|
|
||||||
%endif
|
|
||||||
cat >$RPM_BUILD_ROOT/etc/cron.daily/milter <<'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
find /var/log/milter/save -mtime +7 | xargs $R rm
|
|
||||||
EOF
|
|
||||||
chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter
|
|
||||||
|
|
||||||
%ifos aix4.1
|
|
||||||
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
cd /var/log/milter
|
|
||||||
# uncomment to enable sgmlop if installed
|
|
||||||
#export PYTHONPATH=/usr/local/lib/python2.1/site-packages
|
|
||||||
exec /usr/local/bin/python bms.py >>milter.log 2>&1
|
|
||||||
EOF
|
|
||||||
%else
|
|
||||||
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF'
|
|
||||||
#!/bin/sh
|
|
||||||
cd /var/log/milter
|
|
||||||
exec >>milter.log 2>&1
|
|
||||||
%{python} bms.py &
|
|
||||||
echo $! >/var/run/milter/milter.pid
|
|
||||||
EOF
|
|
||||||
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
|
|
||||||
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter
|
|
||||||
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF'
|
|
||||||
/^python=/
|
|
||||||
c
|
|
||||||
python="%{python}"
|
|
||||||
.
|
|
||||||
w
|
|
||||||
q
|
|
||||||
EOF
|
|
||||||
%endif
|
|
||||||
chmod a+x $RPM_BUILD_ROOT/var/log/milter/start.sh
|
|
||||||
|
|
||||||
mkdir -p $RPM_BUILD_ROOT/var/run/milter
|
|
||||||
mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
|
|
||||||
cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
|
|
||||||
|
|
||||||
%ifos aix4.1
|
|
||||||
%post
|
|
||||||
mkssys -s milter -p /var/log/milter/start.sh -u 25 -S -n 15 -f 9 -G mail || :
|
|
||||||
|
|
||||||
%preun
|
|
||||||
if [ $1 = 0 ]; then
|
|
||||||
rmssys -s milter || :
|
|
||||||
fi
|
|
||||||
%else
|
|
||||||
%post
|
|
||||||
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
|
|
||||||
/sbin/chkconfig --add milter
|
|
||||||
|
|
||||||
%preun
|
|
||||||
if [ $1 = 0 ]; then
|
|
||||||
/sbin/chkconfig --del milter
|
|
||||||
fi
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%clean
|
|
||||||
rm -rf $RPM_BUILD_ROOT
|
|
||||||
|
|
||||||
%files -f INSTALLED_FILES
|
|
||||||
%defattr(-,root,root)
|
|
||||||
%doc README NEWS TODO CREDITS sample.py
|
|
||||||
/etc/logrotate.d/milter
|
|
||||||
/etc/cron.daily/milter
|
|
||||||
%ifos aix4.1
|
|
||||||
%defattr(-,smmsp,mail)
|
|
||||||
%else
|
|
||||||
/etc/rc.d/init.d/milter
|
|
||||||
%defattr(-,mail,mail)
|
|
||||||
%endif
|
|
||||||
%dir /var/log/milter
|
|
||||||
%dir /var/run/milter
|
|
||||||
%dir /var/log/milter/save
|
|
||||||
%config /var/log/milter/start.sh
|
|
||||||
%config /var/log/milter/bms.py
|
|
||||||
%config /var/log/milter/strike3.txt
|
|
||||||
%config /var/log/milter/softfail.txt
|
|
||||||
%config(noreplace) /etc/mail/pymilter.cfg
|
|
||||||
/usr/share/sendmail-cf/hack/rhsbl.m4
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* Mon Jun 06 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-3
|
|
||||||
- properly log pydspam exceptions
|
|
||||||
* Sat Jun 04 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-2
|
|
||||||
- Include default softfail, strike3 templates
|
|
||||||
* Wed May 25 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-1
|
|
||||||
- Move Milter module to subpackage.
|
|
||||||
- DSN support for Three strikes rule and SPF SOFTFAIL
|
|
||||||
- Move /*mime*/ and dynip to Milter subpackage
|
|
||||||
- Fix SPF unknown mechanism list not cleared
|
|
||||||
- Make banned extensions configurable.
|
|
||||||
- Option to scan zipfiles for bad extensions.
|
|
||||||
* Tue Feb 08 2005 Stuart Gathman <stuart@bmsi.com> 0.7.3-1.EL3
|
|
||||||
- Support EL3 and Python2.4 (some scanning/defang support broken)
|
|
||||||
* Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1
|
|
||||||
- Fix various SPF bugs
|
|
||||||
- Recognize dynamic PTR names, and don't count them as authentication.
|
|
||||||
- Three strikes and yer out rule.
|
|
||||||
- Block softfail by default unless valid PTR or HELO
|
|
||||||
- Return unknown for null mechanism
|
|
||||||
- Return unknown for invalid ip address in mechanism
|
|
||||||
- Try best guess on HELO also
|
|
||||||
- Expand setreply for common errors
|
|
||||||
- make rhsbl.m4 hack available for sendmail.mc
|
|
||||||
* Sun Aug 22 2004 Stuart Gathman <stuart@bmsi.com> 0.7.1-1
|
|
||||||
- Handle modifying mislabeled multipart messages without an exception
|
|
||||||
- Support setbacklog, setmlreply
|
|
||||||
- allow multi-recipient CBV
|
|
||||||
- return TEMPFAIL for SPF softfail
|
|
||||||
* Fri Jul 23 2004 Stuart Gathman <stuart@bmsi.com> 0.7.0-1
|
|
||||||
- SPF check hello name
|
|
||||||
- Move pythonsock to /var/run/milter
|
|
||||||
- Move milter.cfg to /etc/mail/pymilter.cfg
|
|
||||||
- Check M$ style XML CID records by converting to SPF
|
|
||||||
- Recognize, but never match ip6 until we properly support it.
|
|
||||||
- Option to reject when no PTR and no SPF
|
|
||||||
* Fri Apr 09 2004 Stuart Gathman <stuart@bmsi.com> 0.6.9-1
|
|
||||||
- Validate spf.py against test suite, and add Received-SPF support to spf.py
|
|
||||||
- Support best_guess for SPF
|
|
||||||
- Reject numeric hello names
|
|
||||||
- Preserve case of local part in sender
|
|
||||||
- Make libmilter timeout a config option
|
|
||||||
- Fix setup.py to work with python < 2.2.3
|
|
||||||
* Tue Apr 06 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-3
|
|
||||||
- Reject invalid SRS immediately for benefit of callback verifiers
|
|
||||||
- Fix include bug in spf.py
|
|
||||||
* Tue Apr 06 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-2
|
|
||||||
- Bug in check_header
|
|
||||||
* Mon Apr 05 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-1
|
|
||||||
- Don't report spoofed unless rcpt looks like SRS
|
|
||||||
- Check for bounce with multiple rcpts
|
|
||||||
- Make dspam see Received-SPF headers
|
|
||||||
- Make sysv init work with RH9
|
|
||||||
* Thu Mar 25 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-3
|
|
||||||
- Forgot to make spf_reject_neutral global in bms.py
|
|
||||||
* Wed Mar 24 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-2
|
|
||||||
- Defang message/rfc822 content_type with boundary
|
|
||||||
- Support SPF delegation
|
|
||||||
- Reject neutral SPF result for selected domains
|
|
||||||
* Tue Mar 23 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-1
|
|
||||||
- SRS forgery check. Detect thread resource starvation.
|
|
||||||
- Properly remove local socket with explicit type.
|
|
||||||
- Decode obfuscated subject headers.
|
|
||||||
* Wed Mar 11 2004 Stuart Gathman <stuart@bmsi.com> 0.6.6-2
|
|
||||||
- init script bug with python2.3
|
|
||||||
* Wed Mar 10 2004 Stuart Gathman <stuart@bmsi.com> 0.6.6-1
|
|
||||||
- SPF checking, hello blacklist
|
|
||||||
* Mon Mar 08 2004 Stuart Gathman <stuart@bmsi.com> 0.6.5-2
|
|
||||||
- memory leak in envfrom and envrcpt
|
|
||||||
* Mon Mar 01 2004 Stuart Gathman <stuart@bmsi.com> 0.6.5-1
|
|
||||||
- progress notification
|
|
||||||
- memory leak in connect
|
|
||||||
- trusted relay
|
|
||||||
* Thu Feb 19 2004 Stuart Gathman <stuart@bmsi.com> 0.6.4-2
|
|
||||||
- smart alias wildcard patch, compile for sendmail-8.12
|
|
||||||
* Thu Dec 04 2003 Stuart Gathman <stuart@bmsi.com> 0.6.4-1
|
|
||||||
- many fixes for dspam support
|
|
||||||
* Wed Oct 22 2003 Stuart Gathman <stuart@bmsi.com> 0.6.3
|
|
||||||
- dspam SCREEN feature
|
|
||||||
- streamline dspam false positive handling
|
|
||||||
* Mon Sep 01 2003 Stuart Gathman <stuart@bmsi.com> 0.6.1
|
|
||||||
- Full dspam support added
|
|
||||||
* Mon Aug 26 2003 Stuart Gathman <stuart@bmsi.com>
|
|
||||||
- Use New email module
|
|
||||||
* Fri Jun 27 2003 Stuart Gathman <stuart@bmsi.com>
|
|
||||||
- Add dspam module
|
|
||||||
Reference in New Issue
Block a user