Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
P
pymilter
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Deploy
Releases
Container registry
Model registry
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
misc
pymilter
Commits
d34efa39
Commit
d34efa39
authored
Oct 10, 2005
by
Stuart Gathman
Browse files
Options
Downloads
Patches
Plain Diff
Configure SPF policy via sendmail access file.
parent
36b5b4e6
No related branches found
No related tags found
No related merge requests found
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
bms.py
+153
-48
153 additions, 48 deletions
bms.py
milter.cfg
+5
-0
5 additions, 0 deletions
milter.cfg
neutral.txt
+34
-0
34 additions, 0 deletions
neutral.txt
with
192 additions
and
48 deletions
bms.py
+
153
−
48
View file @
d34efa39
#!/usr/bin/env python
# A simple milter that has grown quite a bit.
# $Log$
# Revision 1.26 2005/10/07 03:23:40 customdesigned
# Banned users option. Experimental feature to supply Sender when
# missing and MFROM domain doesn't match From. Log cipher bits for
# SMTP AUTH. Sketch access file feature.
#
# Revision 1.25 2005/09/08 03:55:08 customdesigned
# Handle perverse MFROM quoting.
#
...
...
@@ -269,6 +274,7 @@ import traceback
import
ConfigParser
import
time
import
re
import
anydbm
import
Milter.dsn
as
dsn
from
Milter.dynip
import
is_dynip
as
dynip
...
...
@@ -336,6 +342,8 @@ spf_accept_softfail = ()
spf_accept_fail
=
()
spf_best_guess
=
False
spf_reject_noptr
=
False
supply_sender
=
False
access_file
=
None
time_format
=
'
%Y%b%d %H:%M:%S %Z
'
timeout
=
600
cbv_cache
=
{}
...
...
@@ -412,6 +420,7 @@ def read_config(list):
'
hashlength
'
:
'
8
'
,
'
reject_spoofed
'
:
'
no
'
,
'
reject_noptr
'
:
'
no
'
,
'
supply_sender
'
:
'
no
'
,
'
best_guess
'
:
'
no
'
,
'
dspam_internal
'
:
'
yes
'
})
...
...
@@ -487,7 +496,7 @@ def read_config(list):
# spf section
global
spf_reject_neutral
,
spf_best_guess
,
SRS
,
spf_reject_noptr
global
spf_accept_softfail
,
spf_accept_fail
global
spf_accept_softfail
,
spf_accept_fail
,
supply_sender
,
access_file
if
spf
:
spf
.
DELEGATE
=
cp
.
getdefault
(
'
spf
'
,
'
delegate
'
)
spf_reject_neutral
=
cp
.
getlist
(
'
spf
'
,
'
reject_neutral
'
)
...
...
@@ -495,6 +504,8 @@ def read_config(list):
spf_accept_fail
=
cp
.
getlist
(
'
spf
'
,
'
accept_fail
'
)
spf_best_guess
=
cp
.
getboolean
(
'
spf
'
,
'
best_guess
'
)
spf_reject_noptr
=
cp
.
getboolean
(
'
spf
'
,
'
reject_noptr
'
)
supply_sender
=
cp
.
getboolean
(
'
spf
'
,
'
supply_sender
'
)
access_file
=
cp
.
getdefault
(
'
spf
'
,
'
access_file
'
)
srs_config
=
cp
.
getdefault
(
'
srs
'
,
'
config
'
)
if
srs_config
:
cp
.
read
([
srs_config
])
srs_secret
=
cp
.
getdefault
(
'
srs
'
,
'
secret
'
)
...
...
@@ -565,6 +576,76 @@ def parse_header(val):
except
email
.
Errors
.
HeaderParseError
:
pass
return
val
class
SPFPolicy
(
object
):
"
Get SPF policy by result, defaulting to classic policy from pymilter.cfg
"
def
__init__
(
self
,
domain
):
self
.
domain
=
domain
.
lower
()
if
access_file
:
try
:
acf
=
anydbm
.
open
(
access_file
,
'
r
'
)
except
:
acf
=
None
else
:
acf
=
None
self
.
acf
=
acf
def
getPolicy
(
self
,
pfx
):
acf
=
self
.
acf
if
not
acf
:
return
None
try
:
return
acf
[
pfx
+
self
.
domain
]
except
KeyError
:
try
:
return
acf
[
pfx
]
except
KeyError
:
return
None
def
getFailPolicy
(
self
):
policy
=
self
.
getPolicy
(
'
SPF-Fail:
'
)
if
not
policy
:
if
self
.
domain
in
spf_accept_fail
:
policy
=
'
CBV
'
else
:
policy
=
'
REJECT
'
return
policy
def
getNonePolicy
(
self
):
policy
=
self
.
getPolicy
(
'
SPF-None:
'
)
if
not
policy
:
if
spf_reject_noptr
:
policy
=
'
REJECT
'
else
:
policy
=
'
CBV
'
return
policy
def
getSoftfailPolicy
(
self
):
policy
=
self
.
getPolicy
(
'
SPF-Softfail:
'
)
if
not
policy
:
if
self
.
domain
in
spf_accept_softfail
:
policy
=
'
OK
'
elif
self
.
domain
in
spf_reject_neutral
:
policy
=
'
REJECT
'
else
:
policy
=
'
CBV
'
return
policy
def
getNeutralPolicy
(
self
):
policy
=
self
.
getPolicy
(
'
SPF-Neutral:
'
)
if
not
policy
:
if
self
.
domain
in
spf_reject_neutral
:
policy
=
'
REJECT
'
policy
=
'
OK
'
return
policy
def
getPermErrorPolicy
(
self
):
policy
=
self
.
getPolicy
(
'
SPF-PermError:
'
)
if
not
policy
:
policy
=
'
REJECT
'
return
policy
def
getPassPolicy
(
self
):
policy
=
self
.
getPolicy
(
'
SPF-Pass:
'
)
if
not
policy
:
policy
=
'
OK
'
return
policy
class
bmsMilter
(
Milter
.
Milter
):
"""
Milter to replace attachments poisonous to Windows with a WARNING message,
check SPF, and other anti-forgery features, and implement wiretapping
...
...
@@ -776,13 +857,14 @@ class bmsMilter(Milter.Milter):
'
SPF fail: see http://openspf.com/why.html?sender=%s&ip=%s
'
%
(
q
.
s
,
q
.
i
))
res
,
code
,
txt
=
q
.
check
()
q
.
result
=
res
if
res
==
'
unknown
'
and
q
.
perm_error
and
q
.
perm_error
.
ext
:
if
res
in
(
'
unknown
'
,
'
permerror
'
)
and
q
.
perm_error
and
q
.
perm_error
.
ext
:
self
.
cbv_needed
=
q
# report SPF syntax error to sender
res
,
code
,
txt
=
q
.
perm_error
.
ext
# extended (lax processing) result
txt
=
'
EXT:
'
+
txt
if
res
in
(
'
none
'
,
'
softfail
'
,
'
deny
'
,
'
fail
'
):
p
=
SPFPolicy
(
q
.
o
)
if
res
in
(
'
none
'
,
'
softfail
'
,
'
deny
'
,
'
fail
'
,
'
neutral
'
):
if
self
.
mailfrom
!=
'
<>
'
:
# check hello name via spf
# check hello name via spf
unless spf pass
h
=
spf
.
query
(
self
.
connectip
,
''
,
self
.
hello_name
,
receiver
=
receiver
)
hres
,
hcode
,
htxt
=
h
.
check
()
if
hres
in
(
'
deny
'
,
'
fail
'
,
'
neutral
'
,
'
softfail
'
):
...
...
@@ -798,6 +880,7 @@ class bmsMilter(Milter.Milter):
and
not
dynip
(
self
.
hello_name
,
self
.
connectip
):
hres
,
hcode
,
htxt
=
h
.
best_guess
()
else
:
hres
=
res
ores
=
res
if
spf_best_guess
and
res
==
'
none
'
:
#self.log('SPF: no record published, guessing')
q
.
set_default_explanation
(
...
...
@@ -809,34 +892,43 @@ class bmsMilter(Milter.Milter):
else
:
res
,
code
,
txt
=
q
.
best_guess
()
receiver
+=
'
: guessing
'
if
q
.
perm_error
:
if
q
.
perm_error
:
# FIXME: should never happen?
res
,
code
,
txt
=
q
.
perm_error
.
ext
# extended result
txt
=
'
EXT:
'
+
txt
if
self
.
missing_ptr
and
res
in
(
'
neutral
'
,
'
none
'
)
and
hres
!=
'
pass
'
:
if
spf_reject_noptr
:
if
self
.
missing_ptr
and
ores
==
'
none
'
and
res
!=
'
pass
'
\
and
hres
!=
'
pass
'
:
policy
=
p
.
getNonePolicy
()
if
policy
==
'
CBV
'
:
if
self
.
mailfrom
!=
'
<>
'
:
q
.
result
=
ores
self
.
cbv_needed
=
q
# accept, but inform sender via DSN
elif
policy
!=
'
OK
'
:
self
.
log
(
'
REJECT: no PTR, HELO or SPF
'
)
self
.
setreply
(
'
550
'
,
'
5.7.1
'
,
'
You must have a reverse lookup or publish SPF: http://spf.pobox.com
'
,
'
Contact your mail administrator IMMEDIATELY! Your mail server is
'
,
'
severely misconfigured. It has no PTR record (dynamic PTR records
'
,
"
You must have a reverse lookup or publish SPF: http://spf.pobox.com
"
,
"
Contact your mail administrator IMMEDIATELY! Your mail server is
"
,
"
severely misconfigured. It has no PTR record (dynamic PTR records
"
,
"
that contain your IP don
'
t count), an invalid HELO, and no SPF record.
"
)
return
Milter
.
REJECT
if
self
.
mailfrom
!=
'
<>
'
:
self
.
cbv_needed
=
q
if
res
in
(
'
deny
'
,
'
fail
'
):
if
hres
==
'
pass
'
and
q
.
o
in
spf_accept_fail
:
policy
=
p
.
getFailPolicy
()
if
hres
==
'
pass
'
and
policy
==
'
CBV
'
:
if
self
.
mailfrom
!=
'
<>
'
:
self
.
cbv_needed
=
q
el
se
:
el
if
policy
!=
'
OK
'
:
self
.
log
(
'
REJECT: SPF %s %i %s
'
%
(
res
,
code
,
txt
))
self
.
setreply
(
str
(
code
),
'
5.7.1
'
,
txt
)
# A proper SPF fail error message would read:
# forger.biz [1.2.3.4] is not allowed to send mail with the domain
# "forged.org" in the sender address. Contact <postmaster@forged.org>.
return
Milter
.
REJECT
if
res
==
'
softfail
'
and
not
q
.
o
in
spf_accept_softfail
:
if
self
.
missing_ptr
and
hres
!=
'
pass
'
:
if
spf_reject_noptr
or
q
.
o
in
spf_reject_neutral
:
if
res
==
'
softfail
'
:
policy
=
p
.
getSoftfailPolicy
()
if
policy
==
'
CBV
'
and
hres
==
'
pass
'
:
if
self
.
mailfrom
!=
'
<>
'
:
self
.
cbv_needed
=
q
elif
policy
!=
'
OK
'
:
self
.
log
(
'
REJECT: SPF %s %i %s
'
%
(
res
,
code
,
txt
))
self
.
setreply
(
'
550
'
,
'
5.7.1
'
,
'
SPF softfail: If you get this Delivery Status Notice, your email
'
,
...
...
@@ -846,9 +938,12 @@ class bmsMilter(Milter.Milter):
'
notify your administrator of the problem immediately.
'
)
return
Milter
.
REJECT
if
res
==
'
neutral
'
and
q
.
o
in
spf_reject_neutral
:
policy
=
p
.
getNeutralPolicy
()
if
policy
==
'
CBV
'
and
hres
==
'
pass
'
:
if
self
.
mailfrom
!=
'
<>
'
:
self
.
cbv_needed
=
q
if
res
==
'
neutral
'
and
q
.
o
in
spf_reject_neutral
:
elif
policy
!=
'
OK
'
:
self
.
log
(
'
REJECT: SPF neutral for
'
,
q
.
s
)
self
.
setreply
(
'
550
'
,
'
5.7.1
'
,
'
mail from %s must pass SPF: http://spf.pobox.com/why.html
'
%
q
.
o
,
...
...
@@ -859,12 +954,20 @@ class bmsMilter(Milter.Milter):
'
servers for %s should accomplish this.
'
%
q
.
o
)
return
Milter
.
REJECT
if
res
==
'
unknown
'
:
if
res
in
(
'
unknown
'
,
'
permerror
'
):
policy
=
p
.
getPermErrorPolicy
()
if
policy
==
'
CBV
'
and
hres
==
'
pass
'
:
if
self
.
mailfrom
!=
'
<>
'
:
self
.
cbv_needed
=
q
elif
policy
!=
'
OK
'
:
self
.
log
(
'
REJECT: SPF %s %i %s
'
%
(
res
,
code
,
txt
))
# latest SPF draft recommends 5.5.2 instead of 5.7.1
self
.
setreply
(
str
(
code
),
'
5.5.2
'
,
txt
)
self
.
setreply
(
str
(
code
),
'
5.5.2
'
,
txt
,
'
There is a fatal syntax error in the SPF record for %s
'
%
q
.
o
,
'
We cannot accept mail from %s until this is corrected.
'
%
q
.
o
)
return
Milter
.
REJECT
if
res
==
'
error
'
:
if
res
in
(
'
error
'
,
'
temperror
'
)
:
self
.
log
(
'
TEMPFAIL: SPF %s %i %s
'
%
(
res
,
code
,
txt
))
self
.
setreply
(
str
(
code
),
'
4.3.0
'
,
txt
)
return
Milter
.
TEMPFAIL
...
...
@@ -1043,10 +1146,10 @@ class bmsMilter(Milter.Milter):
for
name
,
val
in
self
.
new_headers
:
self
.
fp
.
write
(
"
%s: %s
\n
"
%
(
name
,
val
))
# add new headers to buffer
self
.
fp
.
write
(
"
\n
"
)
# terminate headers
self
.
fp
.
seek
(
0
)
# log when neither sender nor from domains matches mail from domain
if
self
.
mailfrom
!=
'
<>
'
:
if
supply_sender
and
self
.
mailfrom
!=
'
<>
'
:
mf_domain
=
self
.
canon_from
.
split
(
'
@
'
)[
-
1
]
self
.
fp
.
seek
(
0
)
msg
=
rfc822
.
Message
(
self
.
fp
)
for
rn
,
hf
in
msg
.
getaddrlist
(
'
from
'
)
+
msg
.
getaddrlist
(
'
sender
'
):
t
=
parse_addr
(
hf
)
...
...
@@ -1321,8 +1424,10 @@ class bmsMilter(Milter.Milter):
q
=
self
.
cbv_needed
if
q
.
result
in
(
'
softfail
'
,
'
fail
'
,
'
deny
'
):
template_name
=
'
softfail.txt
'
elif
q
.
result
==
'
unknown
'
:
elif
q
.
result
in
(
'
unknown
'
,
'
permerror
'
)
:
template_name
=
'
permerror.txt
'
elif
q
.
result
==
'
neutral
'
:
template_name
=
'
neutral.txt
'
else
:
template_name
=
'
strike3.txt
'
rc
=
self
.
send_dsn
(
q
,
msg
,
template_name
)
...
...
This diff is collapsed.
Click to expand it.
milter.cfg
+
5
−
0
View file @
d34efa39
...
...
@@ -93,7 +93,12 @@ reject_spoofed = 0
# or an important sender is screwed up. Must have valid HELO, however.
;accept_fail = custhelp.com
# use sendmail access file or similar format for detailed spf policy
# This will override any defaults set above
;access_file = /etc/mail/access.db
# Add MAIL FROM as Sender when Sender is missing and From domain
# doesn't match MAIL FROM. Outlook and other email clients will then display
# something like: "Sent by sender@domain.com on behalf of from@example.com"
;supply_sender = 0
# features intended to clean up outgoing mail
[scrub]
...
...
This diff is collapsed.
Click to expand it.
neutral.txt
0 → 100644
+
34
−
0
View file @
d34efa39
Subject: SPF %(result)s (POSSIBLE FORGERY)
This is an automatically generated Delivery Status Notification.
THIS IS A WARNING MESSAGE ONLY.
YOU DO *NOT* NEED TO RESEND YOUR MESSAGE.
Delivery to the following recipients has been delayed.
%(rcpt)s
Subject: %(subject)s
Received-SPF: %(spf_result)s
Your sender policy (or lack thereof) indicated that the above email was not
sent via an authorized SMTP server, but may still be legitimate. Since there
is no positive confirmation that the message is really from you, we have
to give it extra scrutiny - including verifying that the sender really
exists by sending you this DSN. We will remember this sender and not
bother you again for while. You can avoid this message entirely for
legitimate mail by using an authorized SMTP server. Contact your mail
administrator and ask how to configure your email client to use an
authorized server.
If you never sent the above message, then your domain has been forged.
Your mail admin needs to publish a strict SPF record so that I can reject
those forgeries instead of bugging you about them.
If you need further assistance, please do not hesitate to contact me.
Kind regards,
postmaster@%(receiver)s
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
sign in
to comment