1818from themis .marshalredis import MarshalRedis
1919
2020import 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
2931class 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