/
askmessage.py
1340 lines (1024 loc) · 40.3 KB
/
askmessage.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#------------------------------------------------------------------------------
# askmessage.py
#
# (C) 2001-2006 by Marco Paganini (paganini@paganini.net)
#
# This file is part of ASK - Active Spam Killer
#
# ASK is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# ASK is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ASK; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id: askmessage.py,v 1.106 2006/01/09 04:22:26 paganini Exp $
#------------------------------------------------------------------------------
import os
import os.path
import sys
import string
import rfc822
import hashlib
import re
import time
import tempfile
from email.header import decode_header
import asklog
import askconfig
import askmail
import asklock
#------------------------------------------------------------------------------
class AskMessage:
"""
This is the main ASK class.
Attributes:
- tmpfile: Temporary file containing the mail message
- fh: File handle to the temporary file (do a seek(0) before using)
- binary_digest: MD5 digest in binary format
- ascii_digest: MD5 digest in ascii format
- conf_md5: MD5 digest found in the subject
- msg: rfc822 object
- md5sum: md5sum object
- log: LOG object
- config: CONFIG object
- mail: MAIL object
"""
#----------------------------------------------------------------------------------
def __init__(self, config, log):
"""
Initializes the class instance. We need at least a valid CONFIG object
and a valid LOG object.
"""
self.fh = ''
self.tmpfile = ''
self.binary_digest = '';
self.ascii_digest = '';
self.list_match = '';
self.set_conf_md5('');
## Initialize the LOG, CONFIG and MAIL objects
self.config = config
self.log = log
self.mail = askmail.AskMail(self.config, self.log)
self.mail.fullname = self.config.rc_myfullname;
#----------------------------------------------------------------------------------
def __del__(self):
if self.fh:
self.fh.close();
#self.log.write(10, " __del__(): Removing %s" % self.tmpfile)
os.unlink(self.tmpfile)
#----------------------------------------------------------------------------------
def set_conf_md5(self, md5):
"""
Allows external callers to change the notion of the 'conf_md5',
or the md5sum retrieved from the subject line. This is useful
for instance, when you wish to process different (queued) files
using the methods in this class.
"""
self.conf_md5 = md5
#----------------------------------------------------------------------------------
def read(self, input_fh):
"""
This function will read all data from the passed 'input_fh' into a
temporary file. This file will then be used for all sorts of operations
needed by this class.
"""
## tmpfile must be on the same filesystem as ASK's private dir!
## We'll use 'rename' later to change it to the definitive location
self.tmpfile = "%s/%s.%d.msg" % (self.config.rc_tmpdir, os.path.basename(tempfile.mktemp()), os.getpid())
## Create the temp filename and reopen as read/write
self.fh = open(self.tmpfile, "w")
self.fh.close()
self.fh = open(self.tmpfile, "r+b")
## Create a new MD5 object
self.md5sum = hashlib.md5()
## Copy all the data from the passed file object to the new file
in_headers = 1
while 1:
buf = input_fh.readline()
if (buf == ''):
break
## End of headers?
if len(string.strip(buf)) == 0:
in_headers = 0
## Remove X-ASK-Action headers
if in_headers and re.search("^X-ASK-Action:", buf, re.IGNORECASE):
continue
self.md5sum.update(buf)
self.fh.write(buf)
## Add the md5key to the hash and produce the ASCII digest
if (self.config.rc_md5_key != ''):
self.md5sum.update(self.config.rc_md5_key)
self.ascii_digest = ''
binary_digest = self.md5sum.digest()
for ch in range(0,len(binary_digest)):
self.ascii_digest = self.ascii_digest + "%02.2x" % ord(binary_digest[ch])
## Initialize the rfc822 object for our workfile
self.fh.seek(0)
self.msg = rfc822.Message(self.fh, 1)
#----------------------------------------------------------------------------------
def get_real_sender(self):
"""
This is just like "get_sender", but the "X-Primary-Address" is not taken into
account. Useful for cases where you don't want to use this address (when sending
an email back, for instance).
"""
return(self.get_sender(ignore_header = "X-Primary-Address"))
#----------------------------------------------------------------------------------
def get_sender(self, ignore_header=""):
"""
Returns a tuple in the format (sender_name, sender_email). Some processing is
done to treat X-Primary-Address/Resent-From/Reply-To/From. Headers in "ignore_header"
are not checked.
"""
headers = ["X-Primary-Address", "Resent-From", "Reply-To", "From"];
## Remove unwanted headers
def fn_remove(x, y = ignore_header): return(x != y)
headers = filter(fn_remove, headers)
sender_name = ''
sender_mail = ''
for hdr in headers:
(sender_name, sender_mail) = self.msg.getaddr(hdr)
if sender_name == None: sender_name = ''
if sender_mail == None: sender_mail = ''
## Decode sender name and return if found
if sender_mail:
if sender_name:
sender_name = decode_header(sender_name)
sender_name = sender_name[0][0]
break
return(string.rstrip(sender_name), string.rstrip(sender_mail))
#----------------------------------------------------------------------------------
def get_recipients(self):
"""
Returns a list of tuples in the format (sender_name, sender_email). Note that
the list includes the contents of the TO, CC and BCC fields.
"""
to_list = self.msg.getaddrlist("To") ## To:
cc_list = self.msg.getaddrlist("Cc") ## Cc:
bcc_list = self.msg.getaddrlist("Bcc") ## Bcc:
recipients = []
## Master list of recipients
recipients.extend(to_list)
recipients.extend(cc_list)
recipients.extend(bcc_list)
return(recipients)
#----------------------------------------------------------------------------------
def get_subject(self):
"""
Returns the subject of the message, or '' if none is defined.
"""
subject = decode_header(self.msg.getheader("Subject", ""))
return(subject[0][0])
#----------------------------------------------------------------------------------
def get_date(self):
"""
Returns a tuple containing the broken down date (from the "Date:" field)
The tuple contains (year, month, day, hour, minute, second, weekday, julianday,
dst_flag) and can be fed directly to time.mktime or time.strftime.
"""
## Lots of messages have broken dates
try:
tuple = rfc822.parsedate(self.msg.getheader("Date", ""))
except:
tuple = ''
return(tuple)
#------------------------------------------------------------------------------
def get_message_id(self):
"""
Returns the "Message-Id" field or '' if none is defined.
"""
return(self.msg.getheader("Message-Id", ""))
#------------------------------------------------------------------------------
def __inlist(self, filenames, sender=None, recipients=None, subj=None):
"""
Checks if sender email, recipients or subject match one of the regexps
in the given filename array/tuple. If sender/recipient/subject are not
specified, the defaults for the current instance will be used.
"""
## Fill in defaults
if sender == None:
sender = self.get_sender()[1]
if recipients == None:
recipients = self.get_recipients()
if subj == None:
subj = self.get_subject()
## If the sender is one of our emails, we immediately return false.
## This allows the use of "from @ourdomain.com" without having to
## worry about matching from ouremail@ourdomain.com
if self.is_from_ourselves(sender):
self.log.write(10, " __inlist(): We are the sender (%s). No match performed." % sender)
self.list_match = ""
return 0
## Test each filename in turn
for fname in filenames:
## If the file does not exist, ignore
self.log.write(5, " __inlist(): filename=%s, sender=%s, dest=%s, subject=%s"% (fname, sender, recipients, subj))
if (os.access(fname, os.R_OK) == 0):
self.log.write(5, " __inlist(): %s does not exist (or is unreadable). Ignoring..." % fname)
continue
fh = open(fname, "r")
while 1:
buf = fh.readline()
if (buf == ''):
break
buf = string.lower(string.rstrip(buf))
## Ignore comments and blank lines
if (buf != '' and buf[0] != '#'):
## self.log.write(20," __inlist(): regexp=[%s]" % buf)
## "from re" matches sender
## "to re" matches destination (to, bcc, cc)
## "subject re" matches subject
## "header re" matches a full regexp in the headers
## "re" matches sender (previous behavior)
strlist = []
## from
res = re.match("from\s*(.*)", buf, re.IGNORECASE)
if res:
## No blank 'from'
if not res.group(1):
continue
regex = self.__regex_adjust(res.group(1))
strlist.append(sender)
else:
## to
res = re.match("to\s*(.*)", buf, re.IGNORECASE)
if res:
regex = self.__regex_adjust(res.group(1))
## Append all recipients
for (recipient_name, recipient_email) in recipients:
strlist.append(recipient_email)
else:
## subject
res = re.match("subject\s*(.*)", buf, re.IGNORECASE)
if res:
regex = res.group(1)
strlist.append(subj)
else:
## header
res = re.match("header\s*(.*)", buf, re.IGNORECASE)
if res:
regex = res.group(1)
strlist.extend(self.msg.headers)
else:
regex = self.__regex_adjust(buf)
strlist.append(sender)
## Match...
for str in strlist:
try:
if (regex and re.search(regex, str, re.IGNORECASE)):
self.log.write(5, " __inlist(): found with re=%s" % buf)
self.list_match = buf
fh.close()
return 1
except:
self.log.write(1," __inlist(): WARNING: Invalid regular expression: \"%s\". Ignored." % regex)
else:
self.log.write(20, " __inlist(): ignoring line [%s]" % buf)
fh.close()
self.list_match = ""
return 0
#------------------------------------------------------------------------------
def __regex_adjust(self, str):
"""
Returns the "adjusted" regular expression to be used in matching
email items. If the regular expression itself matches xxx@xxx, we
assume a full address match is desired (we try to match using ^re$ or
else r@boo.org would also match spammer@boo.org). Otherwise, just use
a regular regexp to match anywhere.
"""
if re.search(".+@.+", str):
return("^" + str + "$")
else:
return(str)
#------------------------------------------------------------------------------
def is_from_ourselves(self, email = None):
"""
Returns true if the sender is one of our emails. Use 'email' instead of
the current sender if specified.
"""
if not email:
email = string.lower(self.get_sender()[1])
if (email in map(string.lower, self.config.rc_mymails)):
return 1
else:
return 0
#------------------------------------------------------------------------------
def is_in_whitelist(self):
"""
Returns true if this message is in the whitelist. False otherwise
"""
if self.__inlist(self.config.rc_whitelist):
return 1
else:
return 0
#------------------------------------------------------------------------------
def is_in_ignorelist(self):
"""
Returns true if this message is in the ignorelist. False otherwise
"""
if self.__inlist(self.config.rc_ignorelist):
return 1
else:
return 0
#------------------------------------------------------------------------------
def has_our_key(self):
"""
Returns true if this message contains our mailkey. False otherwise
"""
if self.__contains_string(self.config.rc_mailkey):
return 1
else:
return 0
#------------------------------------------------------------------------------
def is_from_mailerdaemon(self):
"""
Returns true if this message comes from MAILER-DAEMON. False otherwise
"""
if re.match("mailer-daemon|postmaster@|<>", self.get_sender(ignore_header = "Reply-To")[1], re.IGNORECASE):
return 1
else:
return 0
#------------------------------------------------------------------------------
def sent_by_ask(self):
"""
Looks for the X-AskVersion rfc822 header. If one is found, we assume ASK
sent the message and we return true. Otherwise, we return false.
"""
if self.__contains_string("^X-AskVersion:"):
return 1
else:
return 0
#------------------------------------------------------------------------------
def valid_smtp_sender(self):
"""
Uses mail.smtp_validate to check the sender's address. Return true if
probably valid, 0 if definitely invalid.
"""
ret = self.mail.smtp_validate(email = self.get_sender()[1],
envelope_from = self.get_matching_recipient())
self.log.write(5, " valid_smtp_sender: SMTP authentication returned %d" % ret)
## Returns possibly valid in case of errors.
if ret != 0:
return 1
else:
return 0
#------------------------------------------------------------------------------
def deliver_mail(self, x_ask_info="Delivering Mail"):
"""
Delivers the current mail to the mailbox contained in self.config.rc_mymailbox
or to stdout if in filter or procmail mode.
"""
## Deliver to stdout if using procmail/filter
if self.config.procmail_mode or self.config.filter_mode:
mailbox = "-"
else:
mailbox = self.config.rc_mymailbox
self.log.write(1, " deliver_mail(): Delivering mail")
self.mail.deliver_mail_file(mailbox,
self.tmpfile,
x_ask_info = x_ask_info,
custom_headers = [ "X-ASK-Auth: " + self.generate_auth() ])
return 0
#------------------------------------------------------------------------------
def junk_mail(self, x_ask_info="Junk"):
"""
Queues the current mail with a status of "Junk" or delivers to
rc_junkmailbox if is defined in the config file.
"""
if self.config.rc_junkmailbox:
self.log.write(1," junk_mail(): Saving message to %s" % self.config.rc_junkmailbox)
self.mail.deliver_mail_file(self.config.rc_junkmailbox,
self.tmpfile,
x_ask_info = x_ask_info,
custom_headers = [ "X-ASK-Auth: " + self.generate_auth() ])
else:
self.log.write(1, " junk_mail(): Queueing Junk Message")
self.queue_mail(x_ask_info = x_ask_info)
return 0
#------------------------------------------------------------------------------
def bulk_mail(self, x_ask_info="Bulk"):
"""
Queues the current mail with a status of "Bulk" or delivers to
rc_bulkmailbox if is defined in the config file.
"""
if self.config.rc_bulkmailbox:
self.log.write(1," bulk_mail(): Saving message to %s" % self.config.rc_bulkmailbox)
self.mail.deliver_mail_file(self.config.rc_bulkmailbox,
self.tmpfile,
x_ask_info = x_ask_info,
custom_headers = [ "X-ASK-Auth: " + self.generate_auth() ])
else:
self.log.write(1, " bulk_mail(): Queueing Bulk Message")
self.queue_mail(x_ask_info = x_ask_info)
return 0
#------------------------------------------------------------------------------
def queue_mail(self, x_ask_info="Message Queued"):
"""
Queues the current message. The queue directory and calculated MD5
are used to generate the filename. Note that the MD5 reflect the original
contents of the message (not the queued one, as headers are added as needed).
If the message queue directory (rc_askmsg) ends in "/", we assume the
queue will be in mqueue format.
"""
self.log.write(5, " queue_mail(): x_ask_info = %s" % x_ask_info)
self.log.write(5, " queue_mail(): The MD5 checksum for %s is %s" % (self.tmpfile, self.ascii_digest))
## If the msgdir ends in "/" we assume a Maildir style queue. If
## not, we have a regular queue.
if self.config.queue_is_maildir:
self.log.write(5, " queue_mail(): Maildir format. Queue dir = %s" % self.config.rc_msgdir)
self.mail.deliver_mail_file(self.config.rc_msgdir,
self.tmpfile,
x_ask_info = x_ask_info,
uniq = self.ascii_digest,
custom_headers = [ "X-ASK-Auth: " + self.generate_auth() ])
else:
queueFileName = self.queue_file()
self.log.write(5, " queue_mail(): Mailbox format. Queue file = %s" % queueFileName)
self.mail.deliver_mail_file(queueFileName,
self.tmpfile,
x_ask_info,
custom_headers = [ "X-ASK-Auth: " + self.generate_auth() ])
return 0
#------------------------------------------------------------------------------
def send_confirmation(self):
"""
Sends a confirmation message back to the sender.
"""
queueFileName = self.queue_file()
self.mail.mailfrom = self.get_matching_recipient()
self.log.write(1, " send_confirmation(): Sending confirmation from %s to %s" % (self.mail.mailfrom, self.get_real_sender()[1]))
## Add Precedence: bulk and (if appropriate) "In-Reply-To"
header_list = [ "X-ASK-Auth: %s" % self.generate_auth(), "Precedence: bulk", "Content-Type: text/plain; charset=\"iso-8859-1\"", "Content-Transfer-Encoding: 8bit" ]
msgid = self.get_message_id()
if msgid:
header_list.append("In-Reply-To: %s" % msgid)
if (self.__check_confirm_list(self.get_real_sender()[1])):
self.mail.send_mail(self.get_real_sender()[1],
"Please confirm (conf#%s)" % self.ascii_digest,
self.config.rc_confirm_filenames,
[queueFileName],
custom_headers = header_list,
max_attach_lines = self.config.rc_max_attach_lines)
self.log.write(1, " send_confirmation(): Confirmation sent to %s..." % self.get_real_sender()[1])
else:
self.log.write(1, " send_confirmation(): too many confirmations already sent to %s..." % self.get_real_sender()[1])
#------------------------------------------------------------------------------
def __check_confirm_list(self, address):
"""
Adds address to the list of confirmation targets.
Returns true if this confirmation should be sent.
Returns false otherwise.
"""
name = address + "\n"
queue_file_name = "%s/.ask-loop-control.dat" % self.config.rc_askdir
## Confirmation list file will be created if it does not exist
if not os.path.exists(queue_file_name):
queue_file_handle = open(queue_file_name, "w")
queue_file_handle.close()
## Lock
queue_file_handle = asklock.AskLock()
lockf = ""
if self.config.rc_lockfile != "":
lockf = self.config.rc_lockfile + "." + os.path.basename(queue_file_name)
else:
lockf = queue_file_name
queue_file_handle.open(queue_file_name, "r+", lockf)
## Read all stuff from .ask-loop-control.dat
if os.path.exists(queue_file_name):
queue_file_handle.seek(0)
confirm_list = queue_file_handle.readlines()
else:
confirm_list = []
confirm_list.append(name)
## Trim list
if len(confirm_list) > self.config.rc_max_confirmation_list:
del confirm_list[0:self.config.rc_max_confirmation_list - self.config.rc_min_confirmation_list]
## Rewrite trimmed list and release lock
queue_file_handle.seek(0)
queue_file_handle.truncate(0)
queue_file_handle.writelines(confirm_list)
queue_file_handle.close()
## More than max_confirmations?
if (confirm_list.count(name) > self.config.rc_max_confirmations):
return 0
return 1
#------------------------------------------------------------------------------
def queue_file(self, md5str=""):
"""
Returns the full path to the queue file of the current message
(self.ascii_digest). The 'md5' parameter can be used to override
the MD5 checksum. If the queue is in maildir format, the queue
directory will be opened and the first file containing the MD5
will be returned.
"""
if not md5str:
md5str = self.ascii_digest
if self.config.queue_is_maildir:
## Original Path if Maildir
maildir = string.replace(self.config.rc_msgdir, "/cur", "")
maildir = string.replace(maildir, "/new", "")
maildir = string.replace(maildir, "/tmp", "")
## /cur
file_list = filter(lambda x,md5str_in = md5str: (string.find(x, md5str_in) != -1), os.listdir(maildir + "cur"))
if len(file_list) != 0:
return(os.path.join(maildir + "cur", file_list[0]))
## /new
file_list = filter(lambda x,md5str_in = md5str: (string.find(x, md5str_in) != -1), os.listdir(maildir + "new"))
if len(file_list) != 0:
return(os.path.join(maildir + "new", file_list[0]))
## Nothing found, return blank
return ""
else:
return "%s/ask.msg.%s" % (self.config.rc_msgdir, md5str)
#------------------------------------------------------------------------------
def discard_mail(self, x_ask_info="Discard", mailbox = '', via_smtp = 0):
"""
This method will deliver the current message to stdout and append a
'X-ASK-Action: Discard' header to it. This will only happen if we're
operating in "filter" mode.
"""
if not self.config.filter_mode:
return
self.log.write(10, " discard_mail: Sending email to stdout with X-ASK-Action: Discard")
self.mail.deliver_mail_file("-",
self.tmpfile,
x_ask_info = x_ask_info,
custom_headers = [ "X-ASK-Action: Discard",
"X-ASK-Auth: " + self.generate_auth() ])
return 0
#------------------------------------------------------------------------------
def dequeue_mail(self, x_ask_info="Message dequeued", mailbox = '', via_smtp = 0):
"""
Dequeues (delivers) mail in the queue directory to the current user.
The queued message will be directly appended to the mailbox. The 'mailbox'
parameter can be specified to force delivery to something different than
the default config.rc_mymailbox.
"""
if not mailbox:
## Deliver to stdout if using procmail/filter
if self.config.procmail_mode or self.config.filter_mode:
mailbox = "-"
else:
mailbox = self.config.rc_mymailbox
queueFileName = self.queue_file(self.conf_md5)
self.log.write(1, " dequeue_mail(): Delivering mail from %s to mailbox %s" % (queueFileName, mailbox))
if via_smtp:
self.mail.mailfrom = self.config.rc_mymails[0]
self.mail.send_mail_file(self.config.rc_mymails[0],
queueFileName,
x_ask_info = x_ask_info,
custom_headers = [ "X-ASK-Auth: " + self.generate_auth() ])
else:
self.mail.deliver_mail_file(mailbox,
queueFileName,
x_ask_info = x_ask_info,
custom_headers = [ "X-ASK-Auth: " + self.generate_auth() ])
os.unlink(queueFileName)
return 0
#------------------------------------------------------------------------------
def delete_mail(self, x_ask_info="No further info"):
"""
Deletes queued file in the queue directory.
"""
queueFileName = self.queue_file(self.conf_md5)
os.unlink(queueFileName)
self.log.write(1, " delete_mail(): Queued file %s deleted" % queueFileName)
return 0
#------------------------------------------------------------------------------
def is_queued(self):
"""
Checks if a queued message exists matching the current MD5 signature.
"""
queueFileName = self.queue_file()
if (os.access(queueFileName, os.F_OK) == 1):
self.log.write(1, " is_queued(): File %s found. Message is queued" % queueFileName)
return 1
else:
self.log.write(1, " is_queued(): File %s not found. Message is not queued" % queueFileName)
return 0
#------------------------------------------------------------------------------
def confirmation_msg_queued(self):
"""
Returns true if the current message is a confirmation message AND
a queued message exists. False otherwise.
"""
queueFileName = self.queue_file(self.conf_md5)
if (os.access(queueFileName, os.F_OK) == 1):
self.log.write(1, " confirmation_msg_queued(): File %s found. Message is queued" % queueFileName)
return 1
else:
self.log.write(1, " confirmation_msg_queued(): File %s not found. Message is not queued" % queueFileName)
return 0
#------------------------------------------------------------------------------
def is_confirmation_return(self):
"""
Checks whether the message subject is a confirmation. If so, self.conf_md5
will be set to the MD5 hash found in the subject true will be returned.
"""
subject = self.get_subject()
self.log.write(10, " is_confirmation_return(): Subject=" + subject)
res = re.search("\(conf[#:]([a-f0-9]{32})\)", subject, re.IGNORECASE)
if (res == None):
self.log.write(1, " is_confirmation_return(): Didn't find conf#MD5 tag on subject")
self.conf_md5 = ''
return 0
else:
self.log.write(1, " is_confirmation_return(): Found conf$md5 tag, MD5=%s" % res.group(1))
self.conf_md5 = res.group(1)
return 1
#------------------------------------------------------------------------------
def add_queued_msg_to_whitelist(self):
"""
Adds the sender in the message pointed to by 'conf_md5' to the whitelist.
"""
queueFileName = self.queue_file(self.conf_md5)
## Create a new AskMessage instance with the queued file
aMessage = AskMessage(self.config, self.log)
queueFilehandle = open(queueFileName, "r")
aMessage.read(queueFilehandle)
queueFilehandle.close()
self.log.write(1, " add_queued_msg_to_whitelist(): Adding message %s to whitelist" % queueFileName)
aMessage.add_to_whitelist()
#------------------------------------------------------------------------------
def add_to_whitelist(self, regex = None):
"""
Adds the current sender or the optional 'regex' to the whitelist.
"""
if not regex:
regex = self.get_sender()[1]
self.__add_to_list(self.config.rc_whitelist, regex)
return 0
#------------------------------------------------------------------------------
def add_to_ignorelist(self, regex = None):
"""
Adds the current sender or the optional 'regex' to the ignorelist.
"""
if not regex:
regex = self.get_sender()[1]
self.__add_to_list(self.config.rc_ignorelist, regex)
return 0
#------------------------------------------------------------------------------
def remove_from_whitelist(self, email):
"""
Remove the passed email from the whitelist.
"""
self.__remove_from_list(self.config.rc_whitelist, email)
return 0
#------------------------------------------------------------------------------
def remove_from_ignorelist(self, email):
"""
Remove the passed email from the ignorelist.
"""
self.__remove_from_list(self.config.rc_ignorelist, email)
return 0
#-----------------------------------------------------------------------------
def __add_to_list(self, filenames, email=None):
"""
Adds the specified 'email' to the first filename in the array 'filenames'.
Defaults to using the current sender if none is specified.
"""
## Defaults
if email == None:
email = self.get_sender()[1]
## Make sure it's not one of our own emails...
if self.is_from_ourselves(email):
self.log.write(1, " __add_to_list(): \"%s\" is listed as one of our emails. It will not be added to the list (%s)" % (email, filenames[0]))
return -1
## Do not add if it's already there... (Compare sender only)
if self.__inlist(filenames,
sender = email,
recipients = [("*IGNORE*","*IGNORE*")],
subj = "*IGNORE*"):
self.log.write(1, " __add_to_list(): \"%s\" is already present in \"%s\". It will not be re-added" % (email, filenames[0]))
return -1
self.log.write(10, " __add_to_list(): filename=%s, email=%s" % (filenames[0], email))
lck = asklock.AskLock()
lockf = ""
if self.config.rc_lockfile != "":
lockf = self.config.rc_lockfile + "." + os.path.basename(filenames[0])
lck.open(filenames[0], "a", lockf)
## We suppose the file is unlocked when we get here...
lck.write("from " + self.__escape_regex(email) + "\n")
lck.close()
self.log.write(1, " __add_to_list(): \"%s\" added to %s" % (email, filenames[0]))
return 0
#-----------------------------------------------------------------------------
def __remove_from_list(self, filenames, email=None):
"""
Removes the specified 'email' from the first filename in the array
'filenames'. Note that entries that would allow this list to match are
NOT removed, but instead the email passed is used as the regexp in the
match. This is so to avoid removing more generic regexps added by
the user.
Note that unlike "add_to_list", email is a mandatory parameter
(for security reasons).
"""
## Make sure it's not one of our own emails...
if self.is_from_ourselves(email):
self.log.write(1, " __remove_from_list: \"%s\" is listed as one of our emails. It will not be removed from the list (%s)" % (email, filenames[0]))
return -1
self.log.write(10, " __remove_from_list: filename=%s, email=%s" % (filenames[0], email))
lck = asklock.AskLock()
lockf = ""
if self.config.rc_lockfile != "":
lockf = self.config.rc_lockfile + "." + os.path.basename(filenames[0])
lck.open(filenames[0], "r+", lockf)
## We read the whole file in memory and re-write without
## the passed email. Note that we expect the file to fit in memory.
oldlist = lck.readlines()
newlist = []
## email must be alone on a line
email = "^" + email + "$"
for regex in listarray:
if not re.match(regex, email, re.IGNORECASE):
newlist.append(regex)
lck.seek(0)
lck.writelines(newlist)
lck.close()
return 0
#------------------------------------------------------------------------------
def invalid_sender(self):
"""
Performs some basic checking on the sender's email and return true if
it seems to be invalid.
"""
sender_email = self.get_sender()[1]
if (sender_email == '' or string.find(sender_email, '@') == -1 or string.find(sender_email,'|') != -1):
self.log.write(1, " invalid_sender(): Malformed 'From:' line (%s). " % sender_email)
return 1
else:
return 0
#------------------------------------------------------------------------------
def is_mailing_list_message(self):
"""
We try to identify (using some common headers) whether this message comes
from a mailing list or not. If it does, we return 1. Otherwise, return 0
"""
if (self.msg.getheader("Mailing-List",'') != '' or
self.msg.getheader("List-Id",'') != '' or
self.msg.getheader("List-Help",'') != '' or
self.msg.getheader("List-Post",'') != '' or
self.msg.getheader("Return-Path",'') == '<>' or
re.match("list|bulk", self.msg.getheader("Precedence",''), re.IGNORECASE) or
re.match(".*(majordomo|listserv|listproc|netserv|owner|bounce|mmgr|autoanswer|request|noreply|nobody).*@", self.msg.getheader("From",''), re.IGNORECASE)):
return 1
else:
return 0