1#------------------------------------------------------------------------------ 2# askremote.py 3# 4# (C) 2001-2006 by Marco Paganini (paganini@paganini.net) 5# 6# This file is part of ASK - Active Spam Killer 7# 8# ASK is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; either version 2 of the License, or 11# (at your option) any later version. 12# 13# ASK is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with ASK; if not, write to the Free Software 20# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21# 22# $Id: askremote.py,v 1.59 2006/01/09 04:22:27 paganini Exp $ 23#------------------------------------------------------------------------------ 24 25import os 26import os.path 27import re 28import string 29import tempfile 30import time 31import md5 32import re 33import asklog 34import askconfig 35import askmessage 36import askmail 37import HTMLParser 38 39#------------------------------------------------------------------------------ 40 41class AskRemote: 42 """ 43 This class deals with remote execution messages. 44 45 Attributes: 46 47 - askmsg: AskMessage Object 48 - config: CONFIG object 49 - log: LOG object 50 """ 51 52 #---------------------------------------------------------------------------------- 53 def __init__(self, askmsg, config, log): 54 """ 55 Initializes the class instance (Duh!) 56 """ 57 58 ## Initialize the LOG, CONFIG and MAIL objects 59 60 self.config = config 61 self.askmsg = askmsg 62 self.log = log 63 64 self.user_command = '' ## The command as sent by the user 65 self.user_args = '' ## The command arguments 66 67 self.edit_help = """ 68You've requested to edit one of your ASK lists. To complete the 69request: 70 711) Hit the Reply button. 72 732) The original contents of your list file are shown between the delimiters 74 below. Edit the contents at will but do not remove the "start" and "end" 75 delimiters. 76 773) Send the mail back. 78 79ASK knows how to handle most "quoting" chars (the ">" signs your mailer 80inserts before each line). They'll be removed automatically. 81 82ASK will refuse to save the list if your original list was modified in the 83meantime (for instance, if someone replied to a confirmation message and 84was added to the list). In that case, another message will be sent back 85indicating this fact and you'll be presented the opportunity to re-edit the 86list. 87 88Some list syntax examples: 89 90#Match all users at sourceforge.net 91from .*@sourceforge.net 92 93#Match all users at all hosts in the fsf.org domain 94from .*@.*fsf.org 95""" 96 97 self.edit_failed = """ 98EDIT FAILED! 99 100Your original ASK list (white/ignore) was modified on your server 101after you made the original edit request. This is normally caused by a user 102responding to a confirmation (which causes his email to be added to the 103list). Please edit your list text below and re-submit the request. 104 105""" 106 #------------------------------------------------------------------------------ 107 def set_user_command(self, str): 108 """ 109 Saves the passed string (usually the user command as found in the 110 "Subject:" field) to the "user_command" attribute. 111 """ 112 113 self.user_command = str 114 115 #------------------------------------------------------------------------------ 116 def get_user_command(self, str): 117 """ 118 Returns the command saved in the "user_command" attribute. 119 """ 120 121 return(self.user_command) 122 123 #------------------------------------------------------------------------------ 124 def set_user_args(self, str): 125 """ 126 Saves the passed string (usually the user command argument as found in the 127 "Subject:" field) to the "user_args" attribute. 128 """ 129 130 self.user_args = str 131 132 #------------------------------------------------------------------------------ 133 def get_user_args(self): 134 """ 135 Returns the command saved in the "user_args" attribute. 136 """ 137 138 return(self.user_args) 139 140 #------------------------------------------------------------------------------ 141 def is_remote_command(self): 142 """ 143 Returns true if the current message is a remote command request. False 144 otherwise. 145 """ 146 147 self.log.write(1, " is_remote_command(): Verifying the subject...") 148 return self.process_remote_commands(check_only = 1) 149 150 #------------------------------------------------------------------------------ 151 def process_remote_commands(self, check_only = 0): 152 """ 153 Verify if the current message contains remote commands. If so, process 154 accordingly. Returns true if delivery happened, false otherwise. 155 156 A special case happens when the 'check_only' parameter is set. In this 157 case, the method will return 1 if the current email contains remote 158 commands or 0 otherwise. 159 """ 160 161 ## If the message does not come from us, ignore it 162 if not self.askmsg.is_from_ourselves(): 163 self.log.write(10, " process_remote_commands(): Message is not from ourselves") 164 return 0 165 166 ## IMPORTANT NOTE: 167 ## 168 ## cmds *MUST* be defined locally as it references objects defined within its 169 ## own instance. Defining cmds as self.cmds in the constructor will create a cyclic 170 ## reference that prevents the deletion of this object. 171 172 cmds = [ 173 ("ask process q", self.process_queue), 174 ("ask queue report", self.process_textmode_commands), 175 ("ask edit whitelist[#:]([a-f0-9]{32})$", self.edit_whitelist), 176 ("ask edit ignorelist[#:]([a-f0-9]{32})$", self.edit_ignorelist), 177 ("ask edit blacklist[#:]([a-f-0-9]{32})$", self.edit_ignorelist), ## Compat ## 178 ("ask edit whitelist", self.edit_whitelist), 179 ("ask edit ignorelist", self.edit_ignorelist), 180 ("ask edit blacklist", self.edit_ignorelist), ## Compat ## 181 ("ask help", self.send_help), 182 ("ask command forward[#:]([a-f0-9]{32})$", self.command_forward_mail), 183 ("ask command delete[#:]([a-f0-9]{32})$", self.command_delete_mail), 184 ("ask command whitelist[#:]([a-f0-9]{32})$", self.command_whitelist), 185 ("ask command ignorelist[#:]([a-f0-9]{32})$", self.command_ignorelist), 186 ("ask command blacklist[#:]([a-f0-9]{32})$", self.command_ignorelist), ## Compat ## 187 ] 188 189 subject = self.askmsg.get_subject() 190 self.log.write(10, " process_remote_commands(): Subject=" + subject) 191 192 ret = 0 ## Default == No delivery 193 194 for (subject_re, method) in cmds: 195 res = re.search(subject_re, subject, re.IGNORECASE) 196 197 if res: 198 self.log.write(1, " process_remote_commands(): Found subject=\"%s\"" % subject) 199 200 if check_only: 201 self.log.write(1, " process_remote_commands(): Checking only. Returning true") 202 ret = 1 203 break 204 205 ## Save the command and the argument requested by the user 206 self.set_user_command(subject) 207 208 if string.find(subject_re, "(") != -1: 209 self.set_user_args(res.group(1)) 210 self.log.write(10, " process_remote_commands(): User args = %s" % res.group(1)) 211 else: 212 self.set_user_args(None) 213 self.log.write(10, " process_remote_commands(): User args = None") 214 215 216 method() ## Execute method 217 ret = 1 ## All methods cause some type of delivery 218 break 219 220 del cmds 221 return ret 222 223 #------------------------------------------------------------------------------ 224 def command_forward_mail(self): 225 """ 226 Delivers the queued email to the user's mailbox, unless we're operating 227 in 'procmail' or 'filter' mode *and* using text mode for the remote commands. 228 In that case, the file will be re-sent to the user's primary address using sendmail 229 instead. This is necessary to make allow procmail/filter users to dequeue multiple 230 messages at once. The email has the X-ASK-Auth header added, so it will 231 pass directly thru the next invocation of ASK and be correctly processed 232 by the mail filter. 233 """ 234 235 ## Set the effective MD5 to the one passed on the Subject 236 self.askmsg.set_conf_md5(self.get_user_args()) 237 238 if ((self.config.procmail_mode or self.config.filter_mode) and 239 (not self.config.rc_remote_cmd_htmlmail)): 240 self.log.write(10, " command_forward_mail: Text mode AND procmail/filter. Will forward to self") 241 via_smtp = 1 242 else: 243 self.log.write(10, " command_forward_mail: Delivering directly") 244 via_smtp = 0 245 246 self.askmsg.dequeue_mail("Forwarded by Command", 247 mailbox = self.config.rc_mymailbox, 248 via_smtp = via_smtp) 249 250 #------------------------------------------------------------------------------ 251 def command_delete_mail(self): 252 """ 253 Deletes the email pointed to by the user_args. 254 """ 255 256 ## Set the effective MD5 to the one passed on the Subject 257 self.askmsg.set_conf_md5(self.get_user_args()) 258 259 self.askmsg.delete_mail 260 261 #------------------------------------------------------------------------------ 262 def process_textmode_commands(self): 263 """ 264 This function will go through the mail text and process everything that 265 looks like a remote command in textmode. 266 """ 267 268 self.askmsg.fh.seek(0) 269 270 while 1: 271 272 buf = self.askmsg.fh.readline() 273 274 if (buf == ''): 275 break 276 277 ## Look for anything like N... Id: MD5 278 res = re.search("([nibwrd]).*Id: ([a-f0-9]{32})", buf, re.IGNORECASE) 279 280 if not res: 281 continue 282 283 action = res.group(1) 284 md5 = res.group(2) 285 286 self.log.write(1, " process_textmode_commands(): Action [%s], MD5 [%s]" % (action, md5)) 287 288 ## We set the user_args to the md5 we need. Note that the 289 ## idea was to 'encapsulate' this in such a way that we 290 ## don't have to mess with 'md5' inside the askmessage 291 ## object, but for now, it's still confusing so we'll 292 ## set on both. 293 294 ## MD5 used by this module's methods 295 self.set_user_args(md5) 296 297 ## MD5 used by the askmessage class 298 self.askmsg.set_conf_md5(md5) 299 300 if self.askmsg.confirmation_msg_queued(): 301 if action == 'I': ## Add sender to IgnoreList 302 self.command_ignorelist() 303 elif action == 'B': ## Old "Blacklist" command now adds to ignorelist 304 self.command_ignorelist() 305 elif action == 'W': ## Add sender to Whitelist 306 self.command_whitelist() 307 elif action == 'R': ## Remove mail from queue 308 self.askmsg.delete_mail() 309 elif action == 'D': ## Deliver but don't whitelist 310 self.command_forward_mail() 311 else: 312 self.log.write(1, " process_textmode_commands(): Queued message was not found. Ignoring...") 313 314 #------------------------------------------------------------------------------ 315 def process_queue(self, htmlmode = -1): 316 """ 317 Will go through the queue and send the user a list of options 318 available for each message. If the 'textmode' parameter is set, 319 the email will be sent in text format (as opposed to HTML). 320 """ 321 322 ## If no htmlmode is specified, use the settings found 323 ## in self.config.rc_remote_cmd_htmlmail 324 325 if htmlmode == -1: 326 htmlmode = self.config.rc_remote_cmd_htmlmail 327 328 aMail = askmail.AskMail(self.config, self.log) 329 aMail.fullname = self.config.rc_myfullname 330 aMail.mailfrom = self.config.rc_mymails[0] 331 332 tempFile = "%s.%d" % (tempfile.mktemp(), os.getpid()) 333 tempFileHandle = open(tempFile, "w") 334 335 queueDir = self.config.rc_msgdir 336 queueFiles = os.listdir(queueDir) 337 338 ## Reverse sort list of files by mtime 339 340 def mtime_file(x,queue=queueDir): return(os.stat(queue + "/" + x)[8], x) ## Convert to (mtime,filename) 341 def strip_mtime(x): return(x[1]) ## Strip mtime 342 343 queueFiles = map(mtime_file, queueFiles) ## Convert to (mtime,filename)[] 344 queueFiles.sort() ## sort (on mtime) 345 queueFiles.reverse() ## Descending 346 queueFiles = map(strip_mtime, queueFiles) ## Strip mtime 347 348 ## 349 350 if htmlmode: 351 tempFileHandle.write("<html>\n") 352 tempFileHandle.write("<body>\n") 353 354 if len(queueFiles) > 0: 355 356 if not htmlmode: 357 tempFileHandle.write(""" 358ASK QUEUE REPORT 359 360These are the contents of your ASK queue. These emails are sitting in the 361queue waiting for a confirmation from the sender. Change the "N" on the left 362of each email with the desired action. Actions are: 363 364N - Do Nothing. Leave it queued. 365D - Deliver this message to my In-Box. 366W - Deliver this message to my In-Box and add sender to Whitelist. 367R - Delete this message from the Queue. 368I - Delete this message from the Queue and ignore future emails. 369 370Just edit the message as you wish and reply. Quotes are OK. 371Queue contents: 372 373""") 374 #--- 375 376 for oneMessageFile in queueFiles: 377 378 aFileHandle = open(queueDir + "/" + oneMessageFile, "r") 379 aMessage = askmessage.AskMessage(self.config, self.log) 380 aMessage.read(aFileHandle) 381 aFileHandle.close() 382 383 ## Printable Date & Time 384 385 msg_date = "" 386 try: 387 msg_date = time.ctime() 388 except: 389 pass 390 391 if not msg_date: 392 msg_date = "[Invalid]" 393 394 ## Get last "Received: from" header that contains an IP and 395 ## is not from localhost (127.x.x.x) 396 397 received_from = '' 398 399 headerlist = aMessage.msg.getallmatchingheaders("Received") 400 headerlist.reverse() 401 402 for header in headerlist: 403 header = string.strip(header) 404 405 if (re.search(" from.*[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*", header) and 406 (not re.search(" from.*127\.[0-9]*\.[0-9]*\.[0-9]*", header))): 407 received_from = header[9:] ## Strip "Received:" 408 break 409 410 ## Get all X-ASK-Info headers. 411 412 askinfolist = [] 413 414 for header in aMessage.msg.getallmatchingheaders("X-ASK-Info"): 415 header = string.strip(header) 416 askinfolist.append(header[12:]) ## Strip "X-ASK-Info:" 417 418 ## File Size 419 420 filesize = os.path.getsize(queueDir + "/" + oneMessageFile) 421 filesize_str = "%d bytes" % filesize 422 423 if filesize > 1024: 424 filesize = filesize / 1024 425 filesize_str = "%s KB" % filesize 426 427 if filesize > 1048576: 428 filesize = filesize / 1024 429 filesize_str = "%s MB" % filesize 430 431 432 ## Grab the MD5 from the filename (user may have changed key, etc, etc) 433 res = re.search("([a-f0-9]{32})", oneMessageFile, re.IGNORECASE) 434 435 if not res: 436 self.log.write(1, " process_queue(): Could not find MD5 for file %s. Ignored" % oneMessageFile) 437 continue 438 439 file_md5 = res.group(1) 440 441 self.log.write(10, " process_queue(): filename=%s, file_md5=%s" % (oneMessageFile, file_md5)) 442 443 if htmlmode: 444 tempFileHandle.write('<hr><br>\n') 445 tempFileHandle.write('<table border=0 width="100%">\n') 446 447 tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>From:</b></td>\n') 448 tempFileHandle.write('<td>%s</td>\n' % aMessage.strip_html(aMessage.get_sender()[1])) 449 tempFileHandle.write('<td></td>\n') 450 tempFileHandle.write('<tr>\n') 451 452 tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>Subject:</b></td>\n') 453 tempFileHandle.write('<td>%s</td>\n' % aMessage.strip_html(aMessage.get_subject())) 454 tempFileHandle.write('<td></td>\n') 455 tempFileHandle.write('<tr>\n') 456 457 tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>Date:</b></td>\n') 458 tempFileHandle.write('<td>%s</td>\n' % msg_date) 459 tempFileHandle.write('<td></td>\n') 460 tempFileHandle.write('<tr>\n') 461 462 tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>Received:</b></td>\n') 463 tempFileHandle.write('<td>%s</td>\n' % received_from) 464 tempFileHandle.write('<td></td>\n') 465 tempFileHandle.write('<tr>\n') 466 467 first = 1 468 for x_ask_info in askinfolist: 469 if first: 470 tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>X-ASK-Info:</b></td>\n') 471 first = 0 472 else: 473 tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><br></td>\n') 474 475 tempFileHandle.write('<td>%s</td>\n' % x_ask_info) 476 tempFileHandle.write('<td></td>\n') 477 tempFileHandle.write('<tr>\n') 478 479 tempFileHandle.write('<td width="5%" bgcolor="#B0B0FF"><b>Size:</b></td>\n') 480 tempFileHandle.write('<td>%s</td>\n' % filesize_str) 481 tempFileHandle.write('<td></td>\n') 482 tempFileHandle.write('<tr>\n') 483 484 ## Read Preview 485 486 summary = aMessage.summary(300) 487 488 tempFileHandle.write('<td colspan=3 bgcolor="#F0F0F0">\n') 489 tempFileHandle.write('<font size=-1><b>Message Preview</b><p>\n') 490 491 tempFileHandle.write(summary) 492 493 tempFileHandle.write('</td>\n') 494 tempFileHandle.write('</tr>\n') 495 496 tempFileHandle.write('<td align="left" colspan=3>\n') 497 tempFileHandle.write('<table border=0 width="100%">\n') 498 tempFileHandle.write('<td align="center" bgcolor="#E0E0E0"><font size="-1"><a href="mailto:%s?subject=ask command forward:%s">Deliver to my Inbox</font></td>\n' % (self.config.rc_mymails[0], file_md5)) 499 tempFileHandle.write('<td align="center" bgcolor="#E0E0E0"><font size="-1"><a href="mailto:%s?subject=ask command whitelist:%s">Deliver to my Inbox<br>and Add Sender to Whitelist</font></td>\n' % (self.config.rc_mymails[0], file_md5)) 500 tempFileHandle.write('<td align="center" bgcolor="#E0E0E0"><font size="-1"><a href="mailto:%s?subject=ask command delete:%s">Delete Message<br>From the Queue</font></td>\n' % (self.config.rc_mymails[0], file_md5)) 501 tempFileHandle.write('<td align="center" bgcolor="#E0E0E0"><font size="-1"><a href="mailto:%s?subject=ask command ignorelist:%s">Delete Message from the Queue<br>and Ignore Future e-mails</font></td>\n' % (self.config.rc_mymails[0], file_md5)) 502 tempFileHandle.write('</table>') 503 504 tempFileHandle.write('</td>\n') 505 tempFileHandle.write('</tr>\n') 506 507 tempFileHandle.write('</table>\n') 508 tempFileHandle.write('<br><br>\n') 509 510 del aMessage 511 512 else: 513 514 ## Default action == D (delete) if the file is older than 515 ## rc_remote_cmd_max_age days. 516 517 filetime = os.path.getmtime(os.path.join(queueDir, oneMessageFile)) 518 now = time.time() 519 520 if (filetime < (now - (self.config.rc_remote_cmd_max_age * 86400))): 521 default_action = "R" 522 else: 523 default_action = "N" 524 525 tempFileHandle.write("%s Id: %s\n" % (default_action, file_md5)) 526 tempFileHandle.write(" From: %s\n" % aMessage.get_sender()[1]) 527 tempFileHandle.write(" Subject: %-65.65s\n" % aMessage.get_subject()) 528 tempFileHandle.write(" Date: %s, Size: %s\n" % (msg_date, filesize_str)) 529 530 ## X-ASK-Info 531 532 for x_ask_info in askinfolist: 533 tempFileHandle.write(" X-ASK-Info: %-65.65s\n" % x_ask_info) 534 535 ## Read summary 536 summary = aMessage.summary(300) 537 538 tempFileHandle.write("\n") 539 tempFileHandle.write("%s\n" % summary) 540 541 tempFileHandle.write("\n------------------------------------------------------------\n\n") 542 543 else: 544 tempFileHandle.write("The message queue is empty\n") 545 546 #--- 547 548 if htmlmode: 549 tempFileHandle.write("</body>\n") 550 tempFileHandle.write("</html>\n") 551 552 tempFileHandle.close() 553 554 aMail.deliver_mail(mailbox = self.config.rc_mymailbox, 555 mailto = self.config.rc_mymails[0], 556 subject = "ASK queue report", 557 body_filenames = [tempFile], 558 custom_headers = [ "X-ASK-Auth: %s" % self.askmsg.generate_auth(), "Precedence: bulk" ], 559 html_mail = htmlmode) 560 os.unlink(tempFile) 561 562 #------------------------------------------------------------------------------ 563 def send_help(self): 564 """ 565 Sends the help file the the sender. 566 """ 567 568 boundary_text = "=_ASKMessageSegment-AOK_=" 569 mail_to = self.config.rc_mymails[0] 570 571 help_text = """ 572ASK HELP Message 573 574ASK recognizes the following "special" subjects: 575 576ask help 577 Returns this message 578 579ask process queue 580 Sends you a list of mail in your queue and lets you act on them 581 582ask edit whitelist 583 Allows you to edit your whitelist 584 585ask edit ignorelist 586 Allows you to edit your ignorelist 587 588For more information about ASK (Active Spam Killer), please visit: 589http://www.paganini.net/ask 590""" 591 ################ END PLAINTEXT - START HTML ############# 592 593 help_html = """ 594<html> 595 <head> 596 <title>ASK HELP</title> 597 <meta http-equiv="content-type" content="text/html\; charset=ISO-8859-1"> 598</head> 599<body> 600<br> 601ASK understands several commands. All these commands are 602issued by sending mail to yourself with special subjects. These subjects are: 603<p> 604<dl> 605 <dt><a href="mailto:%s?subject=ask%%20help">ask help</a></dt> 606 <dd><p>Returns this message</dd> 607 <br> 608 609 <dt><a href="mailto:%s?subject=ask%%20process%%20queue">ask process queue</a></dt> 610 <dd><p>Sends you a list of mail in your queue and lets you act on them</dd> 611 <br> 612 613 <dt><a href="mailto:%s?subject=ask%%20edit%%20whitelist">ask edit whitelist</a></dt> 614 <dd><p>Allows you to edit your whitelist</dd> 615 <br> 616 617 <dt><a href="mailto:%s?subject=ask%%20edit%%20ignorelist">ask edit ignorelist</a></dt> 618 <dd><p>Allows you to edit your ignorelist</dd> 619</dl> 620<p> 621For more information about ASK (Active Spam Killer), please visit 622<a href="http://www.paganini.net/ask">ASK's Homepage</a> 623<p> 624</body> 625</html> 626""" % (mail_to, mail_to, mail_to, mail_to) 627 628 full_message = help_text + "\n--" + boundary_text + "\nContent-Type: text/plain; charset=\"iso-8859-1\"\n\n" + help_text + "\n--" + boundary_text + "\nContent-Type: text/html; charset=\"iso-8859-1\"\n\n" + help_html + "\n--" + boundary_text + "--\n\n" 629 tempFile = "%s.%d" % (tempfile.mktemp(), os.getpid()) 630 tempFileHandle = open(tempFile, "w") 631 tempFileHandle.write(full_message) 632 tempFileHandle.close() 633 634 aMail = askmail.AskMail(self.config, self.log) 635 636 aMail.fullname = self.config.rc_myfullname 637 aMail.mailfrom = mail_to 638 639 aMail.deliver_mail(mailbox = self.config.rc_mymailbox, 640 mailto = mail_to, 641 subject = "ASK Help", 642 body_filenames = [tempFile], 643 custom_headers = ["Content-Type: multipart/alternative;\n\tboundary=\"" + boundary_text + "\"", 644 "MIME-Version: 1.0", 645 "X-ASK-Auth: %s" % self.askmsg.generate_auth(), 646 "Precedence: bulk" ]) 647 os.unlink(tempFile) 648 649 #------------------------------------------------------------------------------ 650 def command_ignorelist(self): 651 """ 652 Adds the message pointed at by the md5 value to the 653 ignorelist and then deletes the message. 654 """ 655 656 ## Set the effective MD5 to the one passed on the Subject 657 self.askmsg.set_conf_md5(self.get_user_args()) 658 659 queueFileName = self.askmsg.queue_file(self.askmsg.conf_md5) 660 661 ## Create a new AskMessage instance with the queued file 662 aMessage = askmessage.AskMessage(self.config, self.log) 663 queueFilehandle = open(queueFileName, "r") 664 665 aMessage.read(queueFilehandle) 666 queueFilehandle.close() 667 668 self.log.write(1, " command_ignorelist(): Adding message %s to ignorelist" % queueFileName) 669 aMessage.add_to_ignorelist() 670 671 self.askmsg.delete_mail() 672 673 #------------------------------------------------------------------------------ 674 def command_whitelist(self): 675 """ 676 Adds the message pointed at by the md5 stored in 677 user_args to the whitelist, and delivers the message. 678 """ 679 680 ## Set the effective MD5 to the one passed on the Subject 681 self.askmsg.set_conf_md5(self.get_user_args()) 682 683 ## Create a new AskMessage instance with the queued file, 684 ## so we can get add the sender's email to the whitelist. 685 686 queued_fname = self.askmsg.queue_file(self.askmsg.conf_md5) 687 askmsg = askmessage.AskMessage(self.config, self.log) 688 queued_fh = open(queued_fname, "r") 689 690 askmsg.read(queued_fh) 691 queued_fh.close() 692 693 self.log.write(1, " command_whitelist(): Adding message %s to whitelist" % queued_fname) 694 askmsg.add_to_whitelist() 695 696 ## Dequeue and remove the queued file 697 self.command_forward_mail() 698 699 #------------------------------------------------------------------------------ 700 def edit_whitelist(self): 701 """ 702 Sends the whitelist file to the sender. 703 """ 704 705 whitelistFileName = self.config.rc_whitelist[0] 706 self.__edit_file(whitelistFileName, "Whitelist") 707 708 #------------------------------------------------------------------------------ 709 def edit_ignorelist(self): 710 """ 711 Sends the ignorelist file to the sender. 712 """ 713 ignorelistFileName = self.config.rc_ignorelist[0] 714 self.__edit_file(ignorelistFileName, "Ignorelist") 715 716 #------------------------------------------------------------------------------ 717 def __edit_file(self, file_path, file_description): 718 """ 719 Do the actual work of checking if the file exists, matching 720 the md5 value from self.askmsg, and then calling 721 __save_file and __send_file as appropriate 722 """ 723 724 message_subject = "ASK Edit " + file_description 725 726 ## User args will contain the file MD5 if this is a request to 727 ## Save the file or None if it's a request to Send the file to self 728 729 if os.path.exists(file_path): 730 if self.get_user_args(): 731 if self.fileMatchesMD5(file_path, self.get_user_args()): 732 self.__save_file(file_path) 733 self.__send_file(file_path, message_subject, "FILE SAVED!\n\n" + self.edit_help) 734 else: 735 self.__send_file(file_path, message_subject, self.edit_failed + "\n" + self.edit_help) 736 else: 737 self.log.write(1, " edit_file(): Sending whitelist to self...") 738 self.__send_file(file_path, message_subject, self.edit_help) 739 else: 740 self.log.write(1, " edit_file(): Sending whitelist to self...") 741 self.__send_file(file_path, message_subject, self.edit_help) 742 743 #------------------------------------------------------------------------------ 744 def fileMatchesMD5(self, fileName, asciiMD5): 745 """ 746 Check to see if the file at the given path matches the md5 747 checksum 748 """ 749 750 if (os.access(fileName, os.F_OK) != 1): 751 return 0 752 753 fileHandle = open(fileName, "r") 754 755 ## Create a new MD5 object 756 md5sum = md5.new() 757 758 while 1: 759 buf = fileHandle.readline() 760 761 if (buf == ''): 762 break 763 764 md5sum.update(buf) 765 766 fileHandle.close() 767 768 ascii_digest = '' 769 binary_digest = md5sum.digest() 770 771 for ch in range(0,len(binary_digest)): 772 ascii_digest = ascii_digest + "%02.2x" % ord(binary_digest[ch]) 773 774 return ascii_digest == asciiMD5 775 776 #------------------------------------------------------------------------------ 777 def __save_file(self, fileName): 778 """ 779 Saves part of the current message between delimiters to the passed 780 filename. "Quoting" chars are removed in the process. 781 """ 782 783 self.log.write(1, " Saving file %s" % fileName) 784 785 fileHandle = open(fileName + ".new", "w") 786 787 buf = '' 788 self.askmsg.fh.seek(0) 789 790 ## Read all the way until "-- start file" tag is found 791 792 while 1: 793 buf = self.askmsg.fh.readline() 794 795 if (buf == ''): 796 fileHandle.close() 797 return 0 798 799 buf = string.strip(buf) 800 buf = self.__dequote(buf) 801 802 if string.find(buf, "--- start file") == 0: 803 break 804 805 ## Read contents until "--- end file" tag is found 806 807 while 1: 808 buf = self.askmsg.fh.readline() 809 810 if (buf == ''): 811 fileHandle.close() 812 return 0 813 814 buf = string.strip(buf) 815 buf = self.__dequote(buf) 816 817 if string.find(buf, "--- end file") == 0: 818 break 819 820 fileHandle.write("%s\n" % buf) 821 822 823 fileHandle.close() 824 825 # Let's be atomic 826 os.rename(fileName + ".new", fileName) 827 828 #------------------------------------------------------------------------------ 829 def __send_file(self, filename, subject, bonus_text = ""): 830 """ 831 Sends the given filename with the given subject. 832 This file will be in the appropriate format to be edited and 833 resubmitted for update 834 """ 835 836 localTempFile = "%s.%d" % (tempfile.mktemp(), os.getpid()) 837 localTempFileHandle = open(localTempFile, "w") 838 839 localTempFileHandle.write("%s\n" % bonus_text) 840 localTempFileHandle.write("\n") 841 localTempFileHandle.write("--- start file %s ---\n" % filename) 842 843 ## Better create a file if it isn't there yet 844 if not os.path.exists(filename): 845 sendFileHandle = open(filename, "w") 846 sendFileHandle.close() 847 848 sendFileHandle = open(filename, "r") 849 850 ## Create a new MD5 object 851 md5sum = md5.new() 852 853 while 1: 854 buf = sendFileHandle.readline() 855 856 if (buf == ''): 857 break 858 859 localTempFileHandle.write(buf) 860 md5sum.update(buf) 861 862 ascii_digest = '' 863 binary_digest = md5sum.digest() 864 865 for ch in range(0,len(binary_digest)): 866 ascii_digest = ascii_digest + "%02.2x" % ord(binary_digest[ch]) 867 868 869 localTempFileHandle.write("--- end file %s ---\n" % filename) 870 localTempFileHandle.close() 871 sendFileHandle.close() 872 873 aMail = askmail.AskMail(self.config, self.log) 874 aMail.fullname = self.config.rc_myfullname 875 aMail.mailfrom = self.config.rc_mymails[0] 876 877 aMail.deliver_mail(mailbox = self.config.rc_mymailbox, 878 mailto = self.config.rc_mymails[0], 879 subject = "%s#%s" % (subject, ascii_digest), 880 body_filenames = [localTempFile], 881 custom_headers = [ "X-ASK-Auth: %s" % self.askmsg.generate_auth(), "Precedence: bulk" ]) 882 883 os.unlink(localTempFile) 884 885 #------------------------------------------------------------------------------ 886 def __dequote(self, str): 887 """ 888 This method will remove most quoting chars inserted by mail agents and 889 return the unquoted part. 890 """ 891 892 res = re.match("^([ \t]*[|>:}][ \t]*)+(.*)", str) 893 894 if res: 895 return res.group(2) 896 else: 897 return str 898 899