Skip to content

Commit 3258f68

Browse files
author
Sandro Mello
committed
version 0.1.2 fixes/features
1 parent 6ec2437 commit 3258f68

File tree

1 file changed

+46
-41
lines changed

1 file changed

+46
-41
lines changed

‎src/themismilter.py‎

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
from themis.marshalredis import MarshalRedis
1919

2020
import Milter, time, logging.config
21-
import re, sys, yaml, os
21+
import re, sys, yaml, os, spf
2222

2323
# TODO: Count receive messages by header
2424
# TODO: Implement greylist functionality
2525
# TODO: Rest interface for configuring
2626
# TODO: Send UDP requests containing statistics
2727
# TODO: Test performance: Anex, list, alias, severeal rcpts
28+
# TODO: Log inverted destination -> OK
29+
# TODO: RCPT and SENDER objects from postfix could be UPPER cases, should match insensitive -> OK
2830

2931
class ThemisMilter(Milter.Base):
3032
REDIS, LOGGER, FEATS = [None, None, None]
@@ -71,7 +73,7 @@ def connect(self, IPname, family, hostaddr):
7173
# (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
7274
# (self, 'ip6.mxout.example.com', AF_INET6,
7375
# ('3ffe:80e8:d8::1', 4720, 1, 0) )
74-
self.log.debug('CONNECT - From %s at %s in ID %s' % (IPname, hostaddr[0], self.id))
76+
self.log.debug('CONNECT - ID %s From %s at %s ' % (self.id, IPname, hostaddr[0]))
7577
#self.log("connect from %s at %s in ID: %s" % (IPname, hostaddr, self.milter.id) )
7678
self.from_ipaddress = hostaddr[0]
7779
return Milter.CONTINUE
@@ -82,10 +84,10 @@ def hello(self, heloname):
8284

8385
def envfrom(self, mailfrom, *str):
8486
try:
85-
mta_hostname = None
87+
self.mta_hostname = None
8688
if self.gconf.policiesByServerPoolFeature:
87-
mta_hostname = self.getsymval('{j}')
88-
self.log.debug('CONNECT - policiesByServerPoolFeature enabled, mta_hostname: %s' % mta_hostname)
89+
self.mta_hostname = self.getsymval('{j}')
90+
self.log.debug('CONNECT - ID %s policiesByServerPoolFeature enabled, mta_hostname: %s' % (self.id, self.mta_hostname))
8991

9092
if self.gconf.messagesBySecFeature:
9193
now = datetime.now()
@@ -94,14 +96,15 @@ def envfrom(self, mailfrom, *str):
9496

9597
self.redis.hincrby('requestsbysec:global', 'second:%s' % now_in_seconds)
9698

97-
self.policies = self.policy.get_all_data_policies(mta_hostname)
99+
self.policies = self.policy.get_all_data_policies(self.mta_hostname, self.gconf.fallbackToNonPoolPolicies)
98100

99101
self.milter_headers, self.recipients, self.subject = [dict(), list(), None]
100102
self.saslauth = self.getsymval('{auth_authen}') # authenticated user
101103
self.saslauth_domain = None
102104
if self.saslauth:
105+
self.saslauth = self.saslauth.lower()
103106
if not '@' in self.saslauth:
104-
self.log.info('ENVFROM - Got a broken SASLUsername: %s' % self.saslauth)
107+
self.log.info('ENVFROM - ID %s Got a broken SASLUsername: %s' % (self.id, self.saslauth))
105108

106109
# TODO: Try to fix the broken sasl accounts
107110
self.saslauth_domain = '@broken_sasl.tld'
@@ -111,6 +114,7 @@ def envfrom(self, mailfrom, *str):
111114
self.saslauth_domain = '@' + self.saslauth.split('@')[1]
112115
# ('From Name', 'user@domain.tld')
113116
_, self.mailfrom = parseaddr(mailfrom)
117+
self.mailfrom = self.mailfrom.lower()
114118

115119
if not self.mailfrom:
116120
# Blank mailfrom
@@ -119,7 +123,7 @@ def envfrom(self, mailfrom, *str):
119123
self.mailfrom = self.mailfrom + '@mailfrom.tld'
120124
self.mailfrom_domain = '@' + self.mailfrom.split('@')[1]
121125
except Exception, e:
122-
self.log.error('ENVRCPT - BYPASS - Error processing envfrom')
126+
self.log.error('ENVRCPT - BYPASS - %s Error processing envfrom' % self.id)
123127
self.log.exception(e)
124128
return Milter.ACCEPT
125129

@@ -134,7 +138,7 @@ def envrcpt(self, to, *str):
134138
_, rcpt = parseaddr(rcptinfo[0])
135139
self.recipients.append(rcpt)
136140
except Exception, e:
137-
self.log.error('ENVRCPT - BYPASS - Error processing recipients')
141+
self.log.error('ENVRCPT - BYPASS - %s Error processing recipients' % self.id)
138142
self.log.exception(e)
139143
return Milter.ACCEPT
140144

@@ -148,20 +152,21 @@ def header(self, name, hval):
148152
if name == 'Subject':
149153
self.subject = hval or ':::blank_subject:::'
150154
except Exception, e:
151-
self.log.warning('HEADER - Error processing headers')
155+
self.log.warning('HEADER - %s Error processing headers' % self.id)
152156
self.log.exception(e)
153157
return Milter.CONTINUE
154158

155159
@Milter.noreply
156160
def eoh(self):
157161
self.queue_id = self.getsymval('{i}')
162+
self.queue_id = ':'.join((self.queue_id, str(self.id)))
158163
self.log.debug('ENVFROM - %s - Mail from: %s SASLUsername: %s' % (self.queue_id, self.mailfrom, self.saslauth or 'NULL'))
159164

160165
try:
161166
for hdr, hdr_value in self.milter_headers.items():
162167
self.log.debug('HEADER - %s - %s | %s' % (self.queue_id, hdr, hdr_value))
163168
except Exception, e:
164-
self.log.warning('EOH - Could not debug headers. %s' % e)
169+
self.log.warning('EOH - %s Could not debug headers. %s' % (self.queue_id, e))
165170
self.log.exception(e)
166171

167172
rcptlog = ', '.join(self.recipients)
@@ -175,22 +180,20 @@ def body(self, chunk):
175180
return Milter.CONTINUE
176181

177182
def eom(self):
178-
# TODO: add custom header - http://stackoverflow.com/questions/17558552/how-do-i-add-custom-field-to-python-log-format-string
179-
eom_log_header = 'EOM - %s - ' % self.queue_id
183+
eom_log_header = 'EOM - %s - %s - ' % (self.mta_hostname, self.queue_id)
180184
try:
181185
if not self.policies:
182186
self.log.warning(eom_log_header + 'BYPASS - Could not find any policies')
183187
return Milter.ACCEPT
184188

185189
for pdata in self.policies:
186190
self.namespace = self.gconf.global_namespace
187-
188191
if pdata.spf:
189192
try:
190193
spfresult, spfcode, spftext = spf.check(i=self.from_ipaddress, s=self.mailfrom, h=self.heloname)
191194
self.addheader('Received-SPF', spfresult)
192195
process_action = False
193-
if self.spfStrictRejectFeature:
196+
if self.gconf.spfStrictRejectFeature:
194197
if spfresult in ['softfail', 'fail', 'neutral', '', 'none']:
195198
process_action = True
196199
else:
@@ -213,20 +216,20 @@ def eom(self):
213216
rgxp = pdata.actionheaders[hdr_key].pop(0)
214217

215218
if re.match(r'%s' % rgxp, hdr_value):
216-
self.log.debug(eom_log_header + 'MATCH, regexp %s value %s' % (rgxp, hdr_value))
219+
self.log.info(eom_log_header + 'MATCH, regexp %s value %s' % (rgxp, hdr_value))
217220
for actionheader in pdata.actionheaders[hdr_key]:
218221
new_hdr, new_hdr_value = actionheader
219-
self.log.debug(eom_log_header + 'Adding header %s with value %s' % (new_hdr, new_hdr_value))
222+
self.log.info(eom_log_header + 'Adding header %s with value %s' % (new_hdr, new_hdr_value))
220223
self.addheader(new_hdr, new_hdr_value)
221224
else:
222-
self.log.debug(eom_log_header + 'NOT MATCH, regexp %s value %s' % (rgxp, hdr_value))
225+
self.log.info(eom_log_header + 'NOT MATCH, regexp %s value %s' % (rgxp, hdr_value))
223226
except Exception, e:
224227
self.log.error(eom_log_header + 'Error processing action headers: %s' % e)
225228
self.log.exception(e)
226229
break
227230

228231
if pdata.onlyheaders:
229-
self.log.debug(eom_log_header + 'BYPASS - Accepting connection, policy validate only headers')
232+
self.log.info(eom_log_header + 'BYPASS - Accepting connection, policy validate only headers')
230233
return Milter.ACCEPT
231234

232235
jailby_namespace = pdata.jailby
@@ -240,7 +243,7 @@ def eom(self):
240243

241244
self.gconf.ai_namespace = self.namespace
242245
jailby_namespace = ':'.join((pdata.pool_name, jailby_namespace))
243-
self.log.debug(eom_log_header + 'Pool policy Name: %s namespace: %s jailby_namespace: %s' % (pdata.pool_name, self.namespace, jailby_namespace) )
246+
self.log.info(eom_log_header + 'Pool policy Name: %s namespace: %s jailby_namespace: %s' % (pdata.pool_name, self.namespace, jailby_namespace) )
244247

245248
self.log.info(eom_log_header + 'Executing policy: %s Pool Policy: %s' % (pdata.policy_name, pdata.pool_policy))
246249

@@ -285,7 +288,8 @@ def eom(self):
285288
for rcpt in self.recipients:
286289
rcpt_domain = '@' + rcpt.split('@')[1]
287290
if self.policy.hasmember(pdata.destination, [rcpt_domain], pdata.inverted_destination):
288-
self.log.info(eom_log_header + 'DEST_MATCH - Recipient: %s Policy: %s' % (rcpt_domain, pdata.policy_name))
291+
self.log.debug(eom_log_header + 'DEST_MATCH - Recipient: %s Policy: %s Inverted: %s' % (rcpt_domain,
292+
pdata.policy_name, pdata.inverted_destination))
289293
# Bypass complex, only bypass if source and destination match
290294
if pdata.type == 'bypass+':
291295
recipient_bypass_match = True
@@ -304,7 +308,7 @@ def eom(self):
304308

305309
if not is_source_match:
306310
# This is only necessary to validate if an ipaddress belongs to a CIDR
307-
for group_src_member in self.policy.getgroup(pdata.source):
311+
for group_src_member in self.policy.getgroupips(pdata.source):
308312
self.log.debug(eom_log_header + 'Looping through groups. group: %s member(s): %s' % (pdata.source, group_src_member))
309313
try:
310314
if IPNetwork is type(isvalidtype(group_src_member)):
@@ -315,19 +319,19 @@ def eom(self):
315319

316320
if is_source_match:
317321
local_milter_from_object = self.milter_from_object
318-
self.log.info(eom_log_header + 'SOURCE_MATCH - group_source_name: %s milter_from_object: %s from_ipaddress: %s mailfrom_domain: %s mailfrom: %s invert: %s'
322+
self.log.debug(eom_log_header + 'SOURCE_MATCH - group_source_name: %s milter_from_object: %s from_ipaddress: %s mailfrom_domain: %s mailfrom: %s invert: %s'
319323
% (pdata.source, local_milter_from_object, self.from_ipaddress, self.mailfrom_domain, self.mailfrom, pdata.inverted_source))
320324

321325
# Bypass by the source (simple bypass) or by recipient (complex)
322326
if pdata.type == 'bypass' or recipient_bypass_match == True:
323327
self.log.info(eom_log_header + 'BYPASS - Source, Destination or both matched. Policy: %s' % pdata.policy_name)
324328
return Milter.ACCEPT
325329
else:
326-
self.log.info(eom_log_header + 'SOURCE_NOT_MATCH - milter_from_object: %s group_src_member: %s invert_source: %s jailby: %s'
327-
% (self.milter_from_object, group_src_member, pdata.inverted_source, pdata.jailby))
330+
self.log.debug(eom_log_header + 'SOURCE_NOT_MATCH - milter_from_object: %s source_group: %s invert_source: %s jailby: %s'
331+
% (self.milter_from_object, pdata.source, pdata.inverted_source, pdata.jailby))
328332

329333
if is_dest_match and is_source_match:
330-
self.log.info(eom_log_header + 'SOURCE_AND_DEST_MATCH')
334+
self.log.debug(eom_log_header + 'SOURCE_AND_DEST_MATCH')
331335

332336
metadata_namespace = ':'.join((self.namespace, 'metadata', local_milter_from_object))
333337
metadata = self.redis.hgetall(metadata_namespace) or ThemisMetaData.METADATA_DEFAULT_VALUES
@@ -366,18 +370,24 @@ def eom(self):
366370
rate = RateLimiter(self.redis, jailby_namespace, conditions)
367371
free, wait = rate.acquire(local_milter_from_object, block_size=rcpt_count, block=False)
368372

369-
self.log.info(eom_log_header + 'Rate limiting object: %s. Namespace: %s Block Size: %s'
370-
% (local_milter_from_object, jailby_namespace, rcpt_count))
371-
372-
if not free:
373+
full_log = "ISFREE:%s mobj:%s plcy:%s ispool:%s prio:%s nspace:%s rcptsize:%s jact:%s cond:%s \
374+
f-ip:%s stphr:%s psrc:%s pdest:%s mfrom:%s mfromdom:%s invsrc:%s invdest:%s" % (
375+
free, local_milter_from_object, pdata.policy_name, pdata.pool_policy, pdata.priority,
376+
jailby_namespace, rcpt_count, pdata.jailaction, conditions, self.from_ipaddress, pdata.stophere,
377+
pdata.source, pdata.destination, self.mailfrom, self.mailfrom_domain, pdata.inverted_source,
378+
pdata.inverted_destination )
379+
if free:
380+
self.log.info(eom_log_header + 'RATING->> ' + full_log)
381+
else:
373382
if self.gconf.global_custom_block:
374383
# Custom blocking time. More than one condition will use only custom_block
375384
wait = int(rate.is_manual_block(local_milter_from_object))
376385
if not wait >= 0:
377386
wait = rate.block(local_milter_from_object, minutes=self.gconf.global_custom_block)
378387

379388
self.set_block_stats(metadata_namespace, pdata.blockprobation)
380-
self.log.info(eom_log_header + 'BLOCK:%s - %s seconds left.' % (local_milter_from_object, wait))
389+
self.log.warning(eom_log_header + 'RATING->> ' + full_log + ' wait:%s' % wait)
390+
#self.log.debug(eom_log_header + 'LIMIT REACHED:%s - %s seconds left.' % (local_milter_from_object, wait))
381391
# Proccess milter action: reject, quarantine, addheader...
382392
return self.milter_action(pdata, wait, eom_log_header)
383393

@@ -455,7 +465,7 @@ def eom(self):
455465
self.log.info(eom_log_header + 'SOURCE_AND_DEST_NOTMATCH')
456466
if pdata.stophere and is_dest_match and is_source_match:
457467
# Stop processing additional policies
458-
self.log.info(eom_log_header + 'STOPHERE - Stopping additional policies, stopped on: %s' % pdata.policy_name)
468+
self.log.debug(eom_log_header + 'STOPHERE - Stopping additional policies, stopped on: %s' % pdata.policy_name)
459469
break
460470

461471
except Exception, e:
@@ -474,11 +484,13 @@ def abort(self):
474484
def milter_action(self, pdata, wait=0, log_header=''):
475485
if pdata.jailaction == 'block':
476486
self.setreply('550', '5.7.1', pdata.replydata % wait)
487+
self.log.warning(log_header + 'REJECT blocked for %s second(s)' % wait)
477488
return Milter.REJECT
478489
elif pdata.jailaction == 'hold':
490+
self.log.warning(log_header + 'HOLD holding for %s second(s)' % wait)
479491
self.quarantine('Themis policy milter')
480492
elif pdata.jailaction == 'monitor':
481-
self.log.info(log_header + 'BYPASS - MONITORING')
493+
self.log.warning(log_header + 'BYPASS for %s second(s)' % wait)
482494
return Milter.ACCEPT
483495

484496
def set_block_stats(self, metadata_namespace, block_probation):
@@ -492,14 +504,7 @@ def set_block_stats(self, metadata_namespace, block_probation):
492504
return self.redis.hset(metadata_namespace, 'block_count', 1)
493505

494506
def match(self, group_src_member, invert=False):
495-
# It's a domain
496-
if re.match(r'@[\w\.]+$', group_src_member):
497-
result = group_src_member == self.mailfrom_domain
498-
# It's an account
499-
elif re.match(r'[\w\.]+@[\w\.]+$', group_src_member):
500-
result = group_src_member == self.mailfrom
501-
else:
502-
result = IPAddress(self.from_ipaddress) in IPNetwork(group_src_member)
507+
result = IPAddress(self.from_ipaddress) in IPNetwork(group_src_member)
503508

504509
if invert:
505510
return not result

0 commit comments

Comments
 (0)