home *** CD-ROM | disk | FTP | other *** search
/ MacAddict 108 / MacAddict108.iso / Software / Internet & Communication / JunkMatcher 1.5.5.dmg / JunkMatcher.app / Contents / Resources / MatcherProxy.py < prev    next >
Encoding:
Python Source  |  2005-06-01  |  54.4 KB  |  1,373 lines

  1. #
  2. #  MatcherProxy.py
  3. #  JunkMatcher
  4. #
  5. #  Created by Benjamin Han on 2/2/05.
  6. #  Copyright (c) 2005 Benjamin Han. All rights reserved.
  7. #
  8.  
  9. # This program is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU General Public License
  11. # as published by the Free Software Foundation; either version 2
  12. # of the License, or (at your option) any later version.
  13.  
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17. # GNU General Public License for more details.
  18.  
  19. # You should have received a copy of the GNU General Public License
  20. # along with this program; if not, write to the Free Software
  21. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  22.  
  23. import copy, cPickle, shutil
  24.  
  25. from Foundation import *
  26. from AppKit import *
  27.  
  28. from consts import *
  29. from Matcher import *
  30. from installFactoryPatterns import *
  31.  
  32. # read-only EmailDB for GUI
  33. globalObjects.emailDBReadFlag = True
  34.  
  35.  
  36. PACKAGE_PLUGIN_PATH = '%sJunkMatcher.mailbundle/Contents/Resources/' % ROOT_PATH
  37.  
  38. _receivedDatePat = re.compile(r'(\d+-\d\d-\d\d \d\d:\d\d:\d\d)(?:\.\d+)?')
  39. _ipPat = re.compile(r'[\d.]+')
  40.  
  41. viewsList = [VIEW_SUBJECT, VIEW_SENDER, VIEW_HEADERS, VIEW_BODY, 
  42.              VIEW_FILENAMES, VIEW_CHARSETS, VIEW_RENDERING]
  43. viewsDict = dict(zip(viewsList, range(7)))
  44. logAttributeNames = ['receivedDate', 'k', 'sender', 'subject', 'junkFlag', 'matchResult']
  45. null = NSNull.null()
  46.  
  47. sys.path.insert(0, PACKAGE_PLUGIN_PATH)
  48. try:
  49.     from pluginConsts import *
  50.     pluginMissing = False
  51. except:
  52.     # TO-DO: maybe warn users that the plugin is not installed properly?
  53.     pluginMissing = True
  54. jmClientModule = None   # to load absolutely minimal # of modules at startup!
  55.  
  56.  
  57. def _sendCommandToPlugin (cmd, arg = None):
  58.     """Sends a command via a JMClient; returns the reply string if successful, otherwise
  59.     returns False."""
  60.     global jmClientModule   # TO-DO: why do I need this?
  61.     
  62.     if pluginMissing: return False
  63.     
  64.     if jmClientModule is None:
  65.         jmClientModule = __import__('JMClient')
  66.  
  67.     jmc = jmClientModule.JMClient('%sserver.port' % TMP_PATH, block = False)
  68.     if jmc.port != -1:
  69.         return jmc.send(cmd, arg)
  70.     return False
  71.     
  72.  
  73. class MatcherProxy (NSObject):
  74.     def init (self):
  75.         self = super(MatcherProxy, self).init()
  76.         if self:
  77.             self.matcher = Matcher(False)
  78.             self.metaPatternsInitialized = False
  79.             self.logReadPosition = 0
  80.             self.whitelistInitialized = False
  81.  
  82.             self.propertiesChanged = self.patternsChanged = self.metaPatternsChanged = self.testsChanged = False
  83.         return self
  84.  
  85.     def setAttribute_withValue_ (self, name, value):
  86.         """This is basically for Obj-C side of PyObjC bridge so we can set an instance variable"""
  87.         setattr(self, name, value)
  88.  
  89.     def prepareFeedbackScript (self):
  90.         jmVersion = NSBundle.mainBundle().objectForInfoDictionaryKey_(u'CFBundleVersion')
  91.         sysInfo = 'JunkMatcher: %s; %s' % (jmVersion, __import__('sysInfo').sysInfo())
  92.         
  93.         return open('%sfeedbackAppleScript.txt' % ROOT_PATH).read() % sysInfo.replace('"', '\\"')
  94.  
  95.     # ----------------------------------------------------------------------
  96.     # for the first run
  97.     # ----------------------------------------------------------------------
  98.  
  99.     def firstRun (self):
  100.         # back the old JM stuff (1.19c or earlier)        
  101.         try:
  102.             # remove the old JunkMatcher Central applescript
  103.             os.remove('%sScripts/Mail Scripts/JunkMatcher Central___ctl-J.scpt' % HOMELIB_PATH)
  104.         except:
  105.             pass
  106.  
  107.         pluginPath = '%sMail/Bundles/JunkMatcher.mailbundle/' % HOMELIB_PATH
  108.         oldConfPath = '%sContents/Resources/junkMatcher/conf/' % pluginPath
  109.         if os.path.exists(oldConfPath):
  110.             # configurations of 1.19c/earlier exist - migrating them
  111.             NSLog(u'Backing up old JunkMatcher patterns.')
  112.  
  113.             try:
  114.                 __import__('backupOldSettings').backupOldSettings(oldConfPath,
  115.                                                                   os.path.expanduser('~/Desktop/'))
  116.             except Exception, e:
  117.                 printException('Exception when backing up old JM settings', e)
  118.  
  119.         # patch tests file since new versions could have a different set of properties
  120.         self.matcher.tests.patchPropertiesAgainstDefaults()
  121.  
  122.         # these shouldn't be necessary cuz Mail.app should not be running as of now
  123.         # but it doesn't hurt to do this
  124.         _sendCommandToPlugin(JM_CMD_LOAD_PROPERTIES)
  125.         _sendCommandToPlugin(JM_CMD_LOAD_TESTS)
  126.         
  127.     # ----------------------------------------------------------------------
  128.     # for installing the plugin
  129.     # ----------------------------------------------------------------------
  130.  
  131.     def enableMailBundleSupport_ (self, isTiger = True):
  132.         os.system('defaults write com.apple.mail EnableBundles -bool YES')
  133.         if isTiger:
  134.             os.system('defaults write com.apple.mail BundleCompatibilityVersion -int 2')
  135.         else:
  136.             os.system('defaults write com.apple.mail BundleCompatibilityVersion -int 1')
  137.  
  138.     def installPlugin_ (self, isTiger = True):
  139.         """Installs JunkMatcher.mailbundle; returns True iff the installation is successful."""
  140.         targetPath = '%sMail/Bundles' % HOMELIB_PATH
  141.  
  142.         # we asked users to quit Mail.app - now we'll nuke all JMServer processes by force
  143.         pidSet = __import__('nukeJMServer').nukeJMServer()
  144.         if pidSet:
  145.             NSLog(u'JMServer.py processes killed: %s' % ', '.join(pidSet))
  146.  
  147.         # create Library/Mail/Bundles if it doesn't exist
  148.         if not os.path.exists(targetPath):
  149.             try:
  150.                 os.makedirs(targetPath, 0755)
  151.             except:
  152.                 return False
  153.  
  154.         # remove the old Library/Mail/Bundles/JunkMatcher.mailbundle if it's there
  155.         targetPath += '/JunkMatcher.mailbundle'
  156.         if os.path.exists(targetPath):
  157.             try:
  158.                 NSLog(u'Trying to remove %s' % targetPath)
  159.                 shutil.rmtree(targetPath)
  160.             except:
  161.                 return False
  162.  
  163.         # copy over the stock version of the plugin
  164.         try:
  165.             shutil.copytree('%sJunkMatcher.mailbundle' % ROOT_PATH, targetPath, True)
  166.         except:
  167.             return False
  168.  
  169.         # enable bundle support in Mail.app
  170.         self.enableMailBundleSupport_(isTiger)
  171.  
  172.         return True
  173.  
  174.     # ----------------------------------------------------------------------
  175.     # for installing the Mail rules
  176.     # ----------------------------------------------------------------------
  177.  
  178.     def repairMailRules_usingTemplate_inTiger_ (self, mailRules, template, isTiger):
  179.         """Repairs mailRules using template; returns True if mailRules is modified."""
  180.         return __import__('mailRules').repairMailRules(mailRules, template, isTiger)
  181.  
  182.     def isMailRunning (self):
  183.         return len(os.popen('ps axjww | grep Mail\.app | grep ^%s | grep -v grep' % os.environ['USER']).read().strip()) != 0
  184.  
  185.     # ----------------------------------------------------------------------
  186.     # for installing the factory version of patterns
  187.     # ----------------------------------------------------------------------
  188.  
  189.     def installFactoryPatterns (self):
  190.         """Returns True if change did occur."""
  191.         ret = installFactoryPatterns(self.matcher.tests.patterns,
  192.                                      globalObjects.metaPatterns,
  193.                                      self.matcher.tests)
  194.         if ret:
  195.             self.patternsChanged = self.metaPatternsChanged = self.testsChanged = True
  196.             
  197.             NSLog(u'Factory versions of meta patterns and patterns are installed.')
  198.         else:
  199.             NSLog(u'The current meta patterns and patterns are identical to the factory versions.')
  200.             
  201.         return ret
  202.  
  203.     # ----------------------------------------------------------------------
  204.     # for messages
  205.     # ----------------------------------------------------------------------
  206.  
  207.     def setMessageFromFile_toDictionary_badTagArray_hiddenUrlArray_vacuousTagArray_ (self, msgFN,
  208.                                                                                      messageViewDictionary,
  209.                                                                                      badTagArray,
  210.                                                                                      hiddenUrlArray,
  211.                                                                                      vacuousTagArray):
  212.         """Returns tuple (receivedDateString, htmlFlag); htmlFlag is '1' iff the message is an HTML-based one."""
  213.         self.msg = Message(open(msgFN).read())
  214.         return self._loadMessage(messageViewDictionary, badTagArray, hiddenUrlArray, vacuousTagArray)
  215.  
  216.     def setMessageFromKey_withReceivedDateString_toDictionary_badTagArray_hiddenUrlArray_vacuousTagArray_ (self, key,
  217.                                                                                                            receivedDateString,
  218.                                                                                                            messageViewDictionary,
  219.                                                                                                            badTagArray,
  220.                                                                                                            hiddenUrlArray,
  221.                                                                                                            vacuousTagArray):
  222.         """Returns tuple (receivedDateString, htmlFlag); htmlFlag is '1' iff the message is an HTML-based one."""
  223.  
  224.         # remember I use a peculiar way to pass in the received date: as a prefix following with '@'!
  225.         # this is so that I can pass as a single argument from Mail.app to the server
  226.         if receivedDateString is not None:
  227.             self.msg = Message('%s%s' % (encodeText(receivedDateString), globalObjects.emailDB.getEntry(key)))
  228.         else:
  229.             self.msg = Message(globalObjects.emailDB.getEntry(key))                    
  230.             
  231.         return self._loadMessage(messageViewDictionary, badTagArray, hiddenUrlArray, vacuousTagArray)
  232.  
  233.     def reloadMessageToDictionary_badTagArray_hiddenUrlArray_vacuousTagArray_ (self,
  234.                                                                                messageViewDictionary,
  235.                                                                                badTagArray,
  236.                                                                                hiddenUrlArray,
  237.                                                                                vacuousTagArray):
  238.         """Reload the message from self.msg.
  239.  
  240.         ASSUMPTION: self.msg must have been initialized (via either
  241.           setMessageFromFile_toDictionary_badTagArray_hiddenUrlArray_vacuousTagArray_() or
  242.           setMessageFromKey_withReceivedDateString_toDictionary_badTagArray_hiddenUrlArray_vacuousTagArray_()."""
  243.         return self._loadMessage(messageViewDictionary, badTagArray, hiddenUrlArray, vacuousTagArray)        
  244.  
  245.     def _loadMessage (self, messageViewDictionary, badTagArray, hiddenUrlArray, vacuousTagArray):
  246.         messageViewDictionary.clear()
  247.         del badTagArray[:]
  248.         del hiddenUrlArray[:]
  249.         del vacuousTagArray[:]
  250.  
  251.         # correct messages must have headers too!
  252.         if self.msg and self.msg.m and len(self.msg.headers):
  253.             if self.msg.isHTML:
  254.                 htmlSrc = self.msg.htmlBody.htmlSrc
  255.                 
  256.                 htmlBody = self.msg.htmlBody
  257.                 if len(htmlBody.badTagList):
  258.                     contentWithBadTags = self.msg.htmlBody.contentWithoutEntities
  259.                     badTagArray.extend(htmlBody.badTagList)
  260.                 else:
  261.                     contentWithBadTags = null
  262.  
  263.                 if len(htmlBody.hiddenURLList):
  264.                     contentWithHiddenURLs = self.msg.htmlBody.contentWithoutEntities
  265.                     hiddenUrlArray.extend(htmlBody.hiddenURLList)
  266.                 else:
  267.                     contentWithHiddenURLs = null
  268.  
  269.                 if len(htmlBody.vacuousTagList):
  270.                     contentWithVacuousTags = self.msg.htmlBody.contentWithoutBadTags
  271.                     vacuousTagArray.extend(htmlBody.vacuousTagList)
  272.                 else:
  273.                     contentWithVacuousTags = null
  274.                 
  275.             else:                
  276.                 htmlSrc = null
  277.                 contentWithBadTags = null
  278.                 contentWithHiddenURLs = null
  279.                 contentWithVacuousTags = null
  280.  
  281.             if self.msg.receivedDate:
  282.                 # get rid of the annoying trailing decimal seconds
  283.                 mo = _receivedDatePat.search(str(self.msg.receivedDate))
  284.                 d = NSDate.dateWithString_(u'%s +0000' % mo.group(1))
  285.                 receivedDateString = d.description()
  286.             else:
  287.                 receivedDateString = u'(date unknown)'
  288.  
  289.             if self.msg.isHTML: htmlFlag = '1'
  290.             else: htmlFlag = '0'
  291.                 
  292.             messageViewDictionary['subject'] = self.msg.subject
  293.             messageViewDictionary['sender'] = self.msg.sender
  294.             messageViewDictionary['headers'] = self.msg.headers
  295.             messageViewDictionary['body'] = self.msg.body
  296.             messageViewDictionary['filenames'] = self.msg.filenames
  297.             messageViewDictionary['charsets'] = self.msg.charsets
  298.             messageViewDictionary['rendering'] = self.msg.rendering
  299.             messageViewDictionary['recipients'] = u', '.join(self.msg.decodedRecipients)
  300.             messageViewDictionary['htmlSrc'] = htmlSrc
  301.             messageViewDictionary['contentWithBadTags'] = contentWithBadTags
  302.             messageViewDictionary['contentWithHiddenURLs'] = contentWithHiddenURLs
  303.             messageViewDictionary['contentWithVacuousTags'] = contentWithVacuousTags
  304.  
  305.         else:
  306.             receivedDateString = None
  307.             htmlFlag = None
  308.  
  309.         return (receivedDateString, htmlFlag)
  310.  
  311.     def indexOfView_ (self, view):
  312.         return viewsDict[view]
  313.  
  314.     # ----------------------------------------------------------------------
  315.     # for tests (properties and patterns)
  316.     # ----------------------------------------------------------------------
  317.  
  318.     def _loadTestsToArray (self, tests):
  319.         # initialize viewsInUse dictionaries for all patterns
  320.         for p in self.matcher.tests.patterns.values():
  321.             if hasattr(p, 'viewsInUse'): p.viewsInUse.clear()
  322.             else: p.viewsInUse = {}
  323.         
  324.         self.getStatsFromPlugin()   # get the latest stats
  325.         for test in self.matcher.tests:
  326.             i = test.propertyOrPattern
  327.             d = {}
  328.             d['kind'] = test.isPattern
  329.             d['onFlag'] = test.isOn
  330.             d['hardFlag'] = test.isHard
  331.             d['matched'] = False
  332.             d['test'] = test        # a Test object
  333.  
  334.             if test.isPattern:
  335.                 view = test.view
  336.                 viewIdx = viewsDict[view]
  337.                 d['htmlFlag'] = test.isHTML
  338.                 d['name'] = i.name
  339.                 d['content'] = i.origPattern
  340.                 d['view'] = viewIdx
  341.  
  342.                 testRecord = i.testRecords.get(view)
  343.                 if not testRecord:
  344.                     # usually this shouldn't happen (inconsistency between patterns
  345.                     # and tests files)                    
  346.                     testRecord = TestRecord()
  347.                     i.testRecords[view] = testRecord
  348.                 
  349.                 d['pr'] = u'%.1f/%.1f' % (testRecord.precision() * 100.0,
  350.                                           testRecord.recall() * 100.0)
  351.                 d['managed'] = i.isManaged                
  352.  
  353.                 i.viewsInUse[viewIdx] = d
  354.  
  355.             else:
  356.                 d['name'] = u'Property: %s' % i.name
  357.                 d['content'] = i.contentString()
  358.                 d['pr'] = u'%.1f/%.1f' % (i.testRecord.precision() * 100.0,
  359.                                           i.testRecord.recall() * 100.0)
  360.                 d['lastMatchAll'] = u'(N/A)'
  361.                 d['managed'] = True
  362.  
  363.             tests.append(d)
  364.  
  365.         tests[0]['first'] = True      # mark the first property
  366.  
  367.     def loadTests (self):
  368.         """Returns a list for showing all tests in an NSTableView.
  369.  
  370.         ASSUMPTION: will be called only ONCE (in the beginning)."""
  371.         
  372.         # DON'T TRY TO MERGE loadTests() and reloadTestsToArray_() - I've tried and
  373.         # drag-n-drop crashed like hell! The difference is that 'ret' is a Python list,
  374.         # and if you do it just by reloadTestsToArray_(), you need to use an NSMutableArray.
  375.          
  376.         ret = []
  377.         self._loadTestsToArray(ret)        
  378.         return ret
  379.  
  380.     def reloadTestsToArray_ (self, tests):
  381.         """Load self.matcher.tests into 'tests' for showing the tests in
  382.         an NSTableView."""
  383.         del tests[:]
  384.         self._loadTestsToArray(tests)        
  385.  
  386.     def findPatternWithID_andView_inTestArray_ (self, idStr, view, testArray):
  387.         viewIdx = viewsDict[view]
  388.         for idx, test in enumerate(testArray):
  389.             if test['kind'] \
  390.                and test['content']== idStr \
  391.                and test['view'] == viewIdx:
  392.                 return idx
  393.  
  394.         return -1
  395.     
  396.     def updateStatsInTests_ (self, tests):
  397.         for d in tests:
  398.             test = d['test']
  399.             i = test.propertyOrPattern
  400.             if test.isPattern:
  401.                 view = test.view
  402.                 d['pr'] = u'%.1f/%.1f' % (i.testRecords[view].precision() * 100.0,
  403.                                           i.testRecords[view].recall() * 100.0)
  404.             else:
  405.                 d['pr'] = u'%.1f/%.1f' % (i.testRecord.precision() * 100.0,
  406.                                           i.testRecord.recall() * 100.0)
  407.  
  408.     def getStatsFromPlugin (self):
  409.         """Update the statistics with the fresh stats obtained from the running plugin
  410.         (if any); returns True if update did occur."""
  411.         f1 = self._getPropertiesStatsFromPlugin()
  412.         f2 = self._getPatternsStatsFromPlugin()
  413.         return (f1 or f2)
  414.  
  415.     def _getPropertiesStatsFromPlugin (self):
  416.         statsString = _sendCommandToPlugin(JM_CMD_GET_STATS_PROPERTIES)
  417.         if statsString is not False:
  418.             self.matcher.tests.properties.setStats(statsString)
  419.             return True
  420.         return False
  421.     
  422.     def _getPatternsStatsFromPlugin (self):
  423.         statsString = _sendCommandToPlugin(JM_CMD_GET_STATS_PATTERNS)
  424.         if statsString is not False:
  425.             self.matcher.tests.patterns.setStats(statsString)
  426.             return True
  427.         return False
  428.  
  429.     def _getSiteDBFromPlugin (self):
  430.         siteDBString = _sendCommandToPlugin(JM_CMD_GET_SITEDB)
  431.         if siteDBString is not False:
  432.             globalObjects.siteDB.loadFromString(siteDBString)
  433.             return True
  434.         return False
  435.  
  436.     def checkPattern_noMetaPattern_ (self, pat, noMetaPattern):
  437.         """Return None if everything is fine; otherwise returns a string explaining
  438.         the error occurring in pat."""
  439.         if pat.find('\n') != -1:
  440.             return u'Your pattern contains a newline (\'\\n\') character.'
  441.         
  442.         if noMetaPattern:
  443.             try:
  444.                 p = re.compile(pat)
  445.             except:
  446.                 return u'Syntax error in the regular expression.'
  447.  
  448.             if mpPat.search(pat) is not None:
  449.                 return u'You cannot use meta patterns here to define a pattern.'
  450.             
  451.         else:
  452.             try:
  453.                 pat = Pattern(None, None, pat)   # a pattern with no name and no TestRecord
  454.                 p = pat.pattern
  455.             except Exception, e:
  456.                 if isinstance(e, JMExceptionMetaPattern):
  457.                     return u'Meta pattern "%s" is not defined.' % e.info
  458.                 return u'Syntax error in the regular expression.'
  459.  
  460.         if p.search('') is not None:
  461.             return u'Regular expression you entered matches empty strings.'
  462.  
  463.         return None
  464.  
  465.     def matchPattern_withText_ (self, pat, txt):
  466.         """Returns a list of spans (i.e., tuples (start index, end index))."""
  467.         return Pattern(None, None, pat).runWithText(txt)
  468.  
  469.     def matchAll_ (self, onlyPatterns):
  470.         if onlyPatterns:
  471.             self.matcher.setMode(MATCHER_MODE_PATTERNS)
  472.         else:
  473.             self.matcher.setMode(MATCHER_MODE_ALL)
  474.  
  475.         matchResult = self.matcher.run(self.msg)
  476.         retList = []
  477.         if type(matchResult.verdict) is bool:   # otherwise it's whitelisted
  478.             for m in filter(lambda m:m.isPositive, matchResult):
  479.                 if hasattr(m, 'info'):
  480.                     info = m.info
  481.                 else:
  482.                     info = None
  483.  
  484.                 if hasattr(m, 'view'):
  485.                     view = m.view
  486.                 else:
  487.                     view = None
  488.  
  489.                 retList.append((m.testIdx, info, view))
  490.  
  491.         return (matchResult.verdict, retList)
  492.  
  493.     def canAddPattern_ (self, pattern):
  494.         """Returns True if the name is ok to add."""
  495.         return self.matcher.tests.patterns.get(pattern) is None
  496.  
  497.     def addPattern_withName_forView_withTableContent_ (self, pattern, name, view, d):
  498.         """Add a new pattern for a specified view; returns the newly added test (the pattern)."""
  499.         # create a new pattern
  500.         p = Pattern(name, {view:TestRecord()}, pattern)
  501.         p.viewsInUse = {viewsDict[view]:d}   # only one view is in use for now
  502.         self.matcher.tests.patterns[pattern] = p
  503.  
  504.         # update tests
  505.         test = Test(p, [view, u'_S', '1'])
  506.         self.matcher.tests.append(test)
  507.  
  508.         # update metaPatterns in p and relevant entries in the meta pattern table
  509.         if self.metaPatternsInitialized:
  510.             mps = globalObjects.metaPatterns
  511.             p.metaPatterns = mps.findAll(pattern)
  512.             for mpName in p.metaPatterns:
  513.                 mps[mpName][-1][p] = null
  514.  
  515.         self.patternsChanged = True
  516.         self.testsChanged = True
  517.         return test
  518.  
  519.     def updatePatternObject_withPattern_ (self, p, pattern):
  520.         p.changePattern(pattern)
  521.         p.isManaged = False     # any update makes a pattern "user" (non-managed)
  522.  
  523.         # update metaPatterns in p and relevant entries in the meta pattern table
  524.         if self.metaPatternsInitialized:
  525.             mps = globalObjects.metaPatterns
  526.  
  527.             # for all of the old meta patterns, remove p from their corresponding entries
  528.             # in the meta pattern table
  529.             for mpName in p.metaPatterns:
  530.                 del mps[mpName][-1][p]
  531.  
  532.             # for all of the new meta patterns, add p to their corresponding entries
  533.             # in the meta pattern table
  534.             p.metaPatterns = mps.findAll(pattern)
  535.             for mpName in p.metaPatterns:
  536.                 mps[mpName][-1][p] = null
  537.  
  538.         self.patternsChanged = True
  539.         self.testsChanged = True        # cuz we use the regular expressions as the IDs of these patterns
  540.  
  541.     def removeTest_ (self, test):
  542.         self.matcher.tests.remove(test)
  543.         p = test.propertyOrPattern      # it has to be a pattern
  544.         del p.viewsInUse[viewsDict[test.view]]
  545.         del p.testRecords[test.view]
  546.         
  547.         if len(p.viewsInUse) == 0:
  548.             # p is no longer used in any test - delete it
  549.             
  550.             # first update p's records in all the meta patterns it uses, if necessary
  551.             if self.metaPatternsInitialized:
  552.                 mps = globalObjects.metaPatterns
  553.  
  554.                 # for all of the meta patterns p uses, remove p from their corresponding entries
  555.                 # in the meta pattern table
  556.                 for mpName in p.metaPatterns:
  557.                     del mps[mpName][-1][p]
  558.             
  559.             # IMPORTANT: this has to come after the metaPatterns update
  560.             # cuz this can potentially delete a Pattern instance, which is used as
  561.             # keys in mps (see the line using mps[mpName][-1][p])
  562.             del self.matcher.tests.patterns[p.origPattern]
  563.             self.patternsChanged = True
  564.  
  565.         self.testsChanged = True
  566.  
  567.     def spawnRow_ (self, row):
  568.         """Set up the views and other details for a spawned test.
  569.  
  570.         ASSUMPTIONS: (1) it's a pattern; (2) at least an unused view exists"""
  571.         test = row['test']
  572.         p = test.propertyOrPattern
  573.  
  574.         # find the first available view
  575.         viewsInUse = p.viewsInUse
  576.         for viewIdx in range(7):
  577.             if viewsInUse.get(viewIdx) is None: break
  578.         view = viewsList[viewIdx]
  579.  
  580.         # create a new TestRecord for the view
  581.         p.testRecords[view] = TestRecord()
  582.         
  583.         newTest = copy.copy(test)   # this won't copy test.propertyOrPattern
  584.         newTest.view = view
  585.         self.matcher.tests.append(newTest)
  586.  
  587.         newRow = dict(row)
  588.         newRow['matched'] = False
  589.         newRow['test'] = newTest
  590.         newRow['view'] = viewIdx
  591.         newRow['pr'] = u'0.0/0.0'
  592.  
  593.         viewsInUse[viewIdx] = newRow
  594.  
  595.         self.patternsChanged = True
  596.         self.testsChanged = True
  597.  
  598.         return newRow
  599.             
  600.     def moveTestFromIndex_toIndex_ (self, fromIdx, toIdx):
  601.         """Move a test at fromIdx to toIdx."""
  602.         tests = self.matcher.tests
  603.         test = tests[fromIdx]
  604.         del tests[fromIdx]
  605.         tests.insert(toIdx, test)
  606.  
  607.         self.testsChanged = True
  608.  
  609.     def changeTestRow_toViewIdx_ (self, row, viewIdx):
  610.         test = row.valueForKey_(u'test')
  611.         pattern = test.propertyOrPattern
  612.         viewsInUse = pattern.viewsInUse
  613.         oldViewIdx = row.valueForKey_(u'view')
  614.         newView = viewsList[viewIdx]
  615.         thisRow = viewsInUse[oldViewIdx]   # remember in the viewsInUse dictionary the values are "rows" initialized at Python side
  616.  
  617.         # change the TestRecords of the pattern
  618.         testRecords = pattern.testRecords
  619.         del testRecords[viewsList[oldViewIdx]]
  620.         testRecords[newView] = TestRecord()
  621.  
  622.         # change the attributes of the test
  623.         test.view = newView
  624.  
  625.         # remove the old view and add the new view for GUI
  626.         del viewsInUse[oldViewIdx]
  627.         viewsInUse[viewIdx] = thisRow      # we can't add 'row' here cuz that's an _NSControllerObjectProxy from objc
  628.  
  629.         # change the data shown in the selected row
  630.         row.setValue_forKey_(viewIdx, u'view')
  631.         row.setValue_forKey_(u'0.0/0.0', u'pr')
  632.  
  633.         self.patternsChanged = True
  634.         self.testsChanged = True
  635.  
  636.     def saveTests (self):
  637.         if self.propertiesChanged:
  638.             self.propertiesChanged = False
  639.             self._getPropertiesStatsFromPlugin()   # get the latest stats
  640.             self.matcher.tests.properties.writeToFile()
  641.             _sendCommandToPlugin(JM_CMD_LOAD_PROPERTIES)
  642.  
  643.         if self.metaPatternsChanged:
  644.             self.metaPatternsChanged = False
  645.             self.patternsChanged = True            # force to reload patterns
  646.             globalObjects.metaPatterns.writeToFile()
  647.             _sendCommandToPlugin(JM_CMD_LOAD_META)
  648.  
  649.         if self.patternsChanged:
  650.             self.patternsChanged = False
  651.             self._getPatternsStatsFromPlugin()     # get the latest stats
  652.             self.matcher.tests.patterns.writeToFile()
  653.             _sendCommandToPlugin(JM_CMD_LOAD_PATTERNS)
  654.  
  655.         if self.testsChanged:
  656.             self.testsChanged = False
  657.             self.matcher.tests.writeToFile()
  658.             _sendCommandToPlugin(JM_CMD_LOAD_TESTS)
  659.  
  660.     # ----------------------------------------------------------------------
  661.     # for test inspector
  662.     # ----------------------------------------------------------------------
  663.  
  664.     def getPropertySettingsFromRow_ (self, testRow):
  665.         r = testRow.valueForKey_(u'test').propertyOrPattern
  666.         testRecord = r.testRecord
  667.         if r.recipientPattern is None: rPat = None
  668.         else: rPat = r.recipientPattern.pattern
  669.         
  670.         return {'name': r.name,
  671.                 'idStr': r.__class__.__name__,
  672.                 'rPat': rPat,
  673.                 'time': testRecord.readTime(),
  674.                 'numbers': u'%d/%d; %d/%d' % testRecord.readNumbers(),
  675.                 'pr': u'%.3f/%.3f' % (testRecord.precision() * 100.0,
  676.                                       testRecord.recall() * 100.0)}
  677.     
  678.     def getPatternSettingsFromRow_toArray_ (self, testRow, patternStats):
  679.         p = testRow.valueForKey_(u'test').propertyOrPattern
  680.         testRecords = p.testRecords
  681.  
  682.         if p.recipientPattern is None: rPat = None
  683.         else: rPat = p.recipientPattern.pattern
  684.         if p.encodingPattern is None: ePat = None
  685.         else: ePat = p.encodingPattern.pattern
  686.  
  687.         del patternStats[:]
  688.         for view in viewsList:
  689.             testRecord = testRecords.get(view)
  690.             if testRecord:
  691.                 n1, n2, n3, n4 = testRecord.readNumbers()
  692.                 patternStats.append({'view': view,
  693.                                      'time': testRecord.readTime(),
  694.                                      'n1': n1,
  695.                                      'n2': n2,
  696.                                      'n3': n3,
  697.                                      'n4': n4})
  698.  
  699.         return {'name': p.name,
  700.                 'idStr': p.origPattern,
  701.                 'rPat': rPat,
  702.                 'ePat': ePat,
  703.                 'pat': p.origPattern}
  704.  
  705.     def updatePatternObject_withName_ (self, p, name):
  706.         p.name = name
  707.         p.isManaged = False     # any update makes a pattern "user" (non-managed)
  708.         self.patternsChanged = True
  709.  
  710.     def updatePatternObject_withAuxPattern_forKind_ (self, p, pat, kind):
  711.         if kind == 'rPat':
  712.             p.changeRecipientPattern(pat)
  713.         else: p.changeEncodingPattern(pat)
  714.         self.patternsChanged = True
  715.  
  716.     def updatePropertyObject_withRecipientPattern_ (self, r, pat):
  717.         r.changeRecipientPattern(pat)
  718.         self.propertiesChanged = True
  719.  
  720.     def getPropertyWithName_ (self, name):
  721.         return self.matcher.tests.properties[name]
  722.  
  723.     def setCheckPhishingURLInWhitelistedEmail_ (self, flag):
  724.         self.matcher.tests.properties[u'PropertyPhishingURL'].checkWhitelistedEmail = flag
  725.         self.propertiesChanged = True
  726.  
  727.     # ----------------------------------------------------------------------
  728.     # for meta patterns
  729.     # ----------------------------------------------------------------------
  730.  
  731.     def loadMetaPatternsToArray_ (self, metaPatterns):
  732.         """Load meta patterns into NSMutableArray tests for showing them in
  733.         an NSTableView.
  734.  
  735.         NOTE: this also sets up Pattern.metaPatterns (a set of names of the meta
  736.         patterns used in a Pattern)."""
  737.  
  738.         mps = globalObjects.metaPatterns
  739.         keys = mps.keys()
  740.         keys.sort()
  741.  
  742.         if self.metaPatternsInitialized:
  743.             # this is a reload
  744.             metaPatterns.removeAllObjects()
  745.             for name in keys:
  746.                 d = mps[name][-1]
  747.                 if type(d) is dict:
  748.                     # clear the additional dictionary we added in the last load
  749.                     mps[name][-1].clear()
  750.                 else:
  751.                     # we haven't added a trailing dict (this is probably a new meta pattern)
  752.                     mps[name] = mps[name] + ({}, )
  753.         else:
  754.             self.metaPatternsInitialized = True
  755.  
  756.             # add an additional dictionary to each meta pattern tuple (for recording which patterns
  757.             # use this meta pattern)
  758.             # TO-DO: I use dictionaries instead of sets here because PyObjC doesn't bridge
  759.             #    sets.Set yet.
  760.             for name in keys:
  761.                 mps[name] = mps[name] + ({}, )
  762.  
  763.         # update attribute 'metaPatterns' in each Pattern, and the dict in each meta pattern
  764.         # tuple (recording what patterns use this meta pattern)
  765.         for p in self.matcher.tests.patterns.values():
  766.             p.metaPatterns = mps.findAll(p.origPattern)
  767.             for mpName in p.metaPatterns:
  768.                 mps[mpName][-1][p] = null
  769.  
  770.         for name in keys:
  771.             mp = mps[name]
  772.             
  773.             # n flag is True iff it can be modified (i.e., it's *not* a reserved meta pattern)
  774.             metaPatterns.addObject_({'name':name, 'pat':mp[0], 'n':not mp[1], 'managed':mp[2],
  775.                                      'inUse':mp[-1]})
  776.         
  777.     def canAddMetaPatternName_ (self, metaPatternName):
  778.         """Returns True if the name is ok to add."""
  779.         return globalObjects.metaPatterns.get(metaPatternName) is None
  780.  
  781.     def addMetaPatternName_pattern_ (self, metaPatternName, pat):
  782.         inUseDict = {}
  783.         # not a reserved meta pattern, not a managed meta pattern
  784.         globalObjects.metaPatterns[metaPatternName] = (pat, False, False, inUseDict)
  785.         self.metaPatternsChanged = True
  786.         return inUseDict
  787.     
  788.     def updateMetaPatternName_pattern_ (self, metaPatternName, pat):
  789.         mps = globalObjects.metaPatterns
  790.         mp = mps[metaPatternName]
  791.  
  792.         mpList = list(mp)
  793.         mpList[0] = pat
  794.         mpList[2] = False    # an updated meta pattern becomes a managed (non-user) meta pattern
  795.         
  796.         mps[metaPatternName] = tuple(mpList)
  797.  
  798.         # we need to reinstantiate patterns that used the changed meta pattern
  799.         for p in mp[-1].keys():
  800.             p.pattern = re.compile(mps.instantiate(p.origPattern))
  801.  
  802.         self.metaPatternsChanged = True
  803.  
  804.     def updateMetaPatternName_withUserFlag_ (self, metaPatternName, flag):
  805.         mps = globalObjects.metaPatterns
  806.         mp = mps[metaPatternName]
  807.         
  808.         mpList = list(mp)
  809.         mpList[2] = not bool(flag)
  810.         
  811.         mps[metaPatternName] = tuple(mpList)
  812.  
  813.         self.metaPatternsChanged = True
  814.  
  815.     def removeMetaPatternName_ (self, metaPatternName):
  816.         del globalObjects.metaPatterns[metaPatternName]
  817.         self.metaPatternsChanged = True
  818.  
  819.     # ----------------------------------------------------------------------
  820.     # for log
  821.     # ----------------------------------------------------------------------
  822.  
  823.     def loadLogToArray_ (self, logArray):
  824.         """Read in all new log entries and apppend logArray with them."""
  825.         try:
  826.             # log file can be missing
  827.             logFN = '%sjm.log' % CONF_PATH
  828.             logSize = os.path.getsize(logFN)
  829.             logF = open(logFN)
  830.         except:
  831.             del logArray[:]
  832.             return
  833.  
  834.         if logSize >= self.logReadPosition:
  835.             logF.seek(self.logReadPosition)
  836.         else:
  837.             # reset to the start
  838.             self.logReadPosition = 0
  839.             del logArray[:]
  840.             logF.seek(0)
  841.         
  842.         logStr = logF.read()
  843.  
  844.         properties = self.matcher.tests.properties
  845.         patterns = self.matcher.tests.patterns
  846.         
  847.         maxIdx = len(logStr)
  848.         if maxIdx != 0:
  849.             # re-open the emailDB cuz new entries have been added
  850.             globalObjects.emailDB.reOpen()
  851.         else:
  852.             if self.logReadPosition == 0:
  853.                 del logArray[:]
  854.             return
  855.  
  856.         # rIdx continues from where we left - BUT remember the latest entry comes
  857.         # at the top, so it's really an "reversed" index (realIdx = len(logArray) - rIdx - 1)
  858.         rIdx = len(logArray)
  859.         
  860.         sList = []
  861.         cList = []
  862.         idx = 0
  863.         while idx < maxIdx:
  864.             idx2 = logStr.find('\n', idx)
  865.             entryLen = int(logStr[idx:idx2])
  866.             idx2 += 1
  867.             idx = idx2 + entryLen + 1    # '+ 1' is for the trailing \n
  868.             d = dict(zip(logAttributeNames,
  869.                          cPickle.loads(logStr[idx2:idx - 1])))
  870.             d['receivedDate'] = NSDate.dateWithString_(u'%s +0000' % d['receivedDate'])
  871.             junkFlag = d['junkFlag']
  872.  
  873.             # produce details and comments from matchResult
  874.             if type(junkFlag) is bool:
  875.                 numPatterns = 0
  876.                 for m in filter(lambda m: m.isPositive, d['matchResult']):
  877.                     if hasattr(m, 'info'):
  878.                         if m.isProperty:
  879.                             name = properties[m.idStr].name
  880.                             if hasattr(m, 'info'):
  881.                                 try:
  882.                                     sList.append('- %s: %s.' % (name, m.info))
  883.                                 except:
  884.                                     sList.append('- %s.' % name)
  885.                             else:
  886.                                 sList.append('- %s.' % name)
  887.                             
  888.                             cList.append(name)
  889.                         else:
  890.                             p = patterns.get(m.idStr)
  891.                             if p is None:
  892.                                 sList.append('- Unknown pattern ("%s") matched in view "%s".' % (m.idStr, m.view))
  893.                             else:
  894.                                 sList.append('- Pattern "%s" matched in view "%s".' % (p.name, m.view))
  895.                             numPatterns += 1
  896.                     else:
  897.                         name = properties[m.idStr].name
  898.                         sList.append('- %s.' % name)
  899.                         cList.append(name)
  900.  
  901.                 d['details'] = (d['sender'], d['subject'], '\n'.join(sList), d['k'])
  902.                 if len(cList):
  903.                     if numPatterns:
  904.                         d['comments'] = '%s and %d pattern(s)' % (', '.join(cList), numPatterns)
  905.                     else:
  906.                         d['comments'] = ', '.join(cList)
  907.                 else:
  908.                     if numPatterns:
  909.                         d['comments'] = '%d pattern(s)' % numPatterns
  910.                         
  911.             else:
  912.                 # junkFlag is a string, which means the message is whitelisted
  913.                 d['details'] = (d['sender'], d['subject'], u'- Whitelisted using email address pattern "%s".' % junkFlag, d['k'])
  914.                 d['comments'] = u'Whitelisted'
  915.                 d['junkFlag'] = False
  916.  
  917.             d['cFlag'] = False
  918.             d['rIdx'] = rIdx
  919.             rIdx += 1
  920.             
  921.             del sList[:]
  922.             del cList[:]
  923.             
  924.             logArray.insert(0, d)
  925.  
  926.         self.logReadPosition += maxIdx
  927.  
  928.     def loadCorrections_intoLog_ (self, corrections, log):
  929.         """Load corrections (indices w.r.t. the log array/list).
  930.  
  931.         ASSUMPTION: ONLY call this after loadLogToArray_(), and you only need to call
  932.         this once (the first time the log is loaded)."""
  933.         # corrections is an NSMutableSet, log is an NSMutableArray
  934.         try:
  935.             f = open('%scorrections' % CONF_PATH)
  936.         except:
  937.             corrections.removeAllObjects()
  938.             return
  939.  
  940.         # remember the rIdx is "reversed" (see comment in loadLogToArray_())
  941.         logLen = len(log)
  942.         for rIdx in map(lambda l: int(l), filter(lambda l: len(l), f.read().split(' '))):
  943.             corrections.addObject_(rIdx)
  944.             entry = log[logLen - rIdx - 1]
  945.             entry['cFlag'] = True
  946.             entry['junkFlag'] = not entry['junkFlag']   # remember it's been corrected!
  947.  
  948.     def recycleLog (self):
  949.         if not _sendCommandToPlugin(JM_CMD_RECYCLE):
  950.             # server is not running - recycle the log ourselves
  951.             self.matcher.recycleLog()
  952.             
  953.         self.logReadPosition = 0
  954.  
  955.     def setCorrectionForEntry_withCorrectionsSet_ (self, entry, corrections):
  956.         """Correct (cFlag is True) or remove correction on a message with key 'k', so
  957.         that the target verdict equals to junkFlag, and dump the changed corrections into its file.
  958.  
  959.         ASSUMPTION: Corrections is already updated before this being called.
  960.         INVARIANCE: the statistics of properties/patterns, siteDB and whitelist of the GUI and the Plugin
  961.           (if it's running at the time) should be all synched after this call."""
  962.         open('%scorrections' % CONF_PATH, 'w').write(' '.join(map(str, corrections.allObjects())))
  963.  
  964.         k = entry.valueForKey_('k')
  965.         receivedDate = entry.valueForKey_('receivedDate')
  966.  
  967.         msg = Message('%s%s' % (encodeText(receivedDate.description()), globalObjects.emailDB.getEntry(k)))
  968.         if msg.m is None:
  969.             # message is malformed - nothing can be done
  970.             return
  971.  
  972.         cFlag = entry.valueForKey_('cFlag')
  973.         junkFlag = entry.valueForKey_('junkFlag')
  974.         matchResult = entry.valueForKey_('matchResult')
  975.  
  976.         properties = self.matcher.tests.properties
  977.         patterns = self.matcher.tests.patterns
  978.  
  979.         if cFlag:
  980.             # set correction
  981.             if junkFlag:
  982.                 # change to junk
  983.                 if msg.addSites():
  984.                     globalObjects.siteDB.writeToFile()
  985.                     _sendCommandToPlugin(JM_CMD_LOAD_SITEDB)
  986.             else:
  987.                 # change to clean
  988.                 if msg.senderEmail:
  989.                     # whitelist the sender - no check on possible duplicates here!
  990.                     globalObjects.whitelist.add(u'Automatically added %s' % NSCalendarDate.calendarDate(),
  991.                                                 '%s' % re.escape(msg.senderEmail), True)
  992.                     _sendCommandToPlugin(JM_CMD_WHITELIST)
  993.                     
  994.                 if msg.removeSites(False):
  995.                     globalObjects.siteDB.writeToFile()
  996.                     _sendCommandToPlugin(JM_CMD_LOAD_SITEDB)
  997.                     
  998.         else:
  999.             # remove correction
  1000.             if junkFlag:
  1001.                 # change to junk
  1002.                 if msg.senderEmail:
  1003.                     # try to remove the sender from the whitelist
  1004.                     # TO-DO: easy but slow implementation!
  1005.                     addr = msg.senderEmail
  1006.                     removeList = []
  1007.                     for idx, d in enumerate(globalObjects.whitelist.theList):
  1008.                         if d['managed']:
  1009.                             if re.search(d['pat'], addr):
  1010.                                 removeList.append(idx)
  1011.  
  1012.                     if removeList:
  1013.                         globalObjects.whitelist.removeMany(removeList)
  1014.                         _sendCommandToPlugin(JM_CMD_WHITELIST)
  1015.  
  1016.                 if msg.addSites():
  1017.                     globalObjects.siteDB.writeToFile()
  1018.                     _sendCommandToPlugin(JM_CMD_LOAD_SITEDB)
  1019.             else:
  1020.                 # change to clean
  1021.                 if msg.removeSites(False):
  1022.                     globalObjects.siteDB.writeToFile()
  1023.                     _sendCommandToPlugin(JM_CMD_LOAD_SITEDB)
  1024.  
  1025.         # update the statistics
  1026.         self.getStatsFromPlugin()
  1027.         for m in matchResult:
  1028.             if m.isPositive != junkFlag:
  1029.                 claimed = m.isPositive
  1030.                 if m.isProperty:
  1031.                     properties[m.idStr].testRecord.changeToFalse(claimed)
  1032.                 else:
  1033.                     p = patterns.get(m.idStr)  # patterns can be already gone by now
  1034.                     if p:
  1035.                         p.testRecords[m.view].changeToFalse(claimed)
  1036.             else:
  1037.                 if m.isProperty:
  1038.                     properties[m.idStr].testRecord.changeToTrue(junkFlag)
  1039.                 else:
  1040.                     p = patterns.get(m.idStr)  # patterns can be already gone by now
  1041.                     if p:
  1042.                         p.testRecords[m.view].changeToTrue(junkFlag)
  1043.  
  1044.         # send the updated stats to a live plugin, if any
  1045.         pluginAWOL = (_sendCommandToPlugin(JM_CMD_SET_STATS_PROPERTIES,
  1046.                                            properties.getStatsString()) is False)
  1047.         pluginAWOL &= (_sendCommandToPlugin(JM_CMD_SET_STATS_PATTERNS,
  1048.                                             patterns.getStatsString()) is False)
  1049.         if pluginAWOL:
  1050.             properties.writeToFile()
  1051.             patterns.writeToFile()
  1052.  
  1053.     def getRecycleDays (self):
  1054.         return globalObjects.prefs.recycleDays
  1055.  
  1056.     def setRecycleDays_ (self, numDays):
  1057.         """Returns True iff recycling did happen."""
  1058.         prefs = globalObjects.prefs
  1059.         if prefs.recycleDays != numDays:
  1060.             prefs.recycleDays = numDays
  1061.             prefs.writeToFile()
  1062.             
  1063.             reply = _sendCommandToPlugin(JM_CMD_PREFS_RECYCLEDAYS, str(numDays))
  1064.  
  1065.             # note if the plugin is not running we call recycleLogWhenItsDue() ourselves
  1066.             if (reply is False and self.matcher.recycleLogWhenItsDue()) or\
  1067.                    reply == 'RECYCLED':
  1068.                 # recycling did happen
  1069.                 self.logReadPosition = 0
  1070.                 return True
  1071.  
  1072.         return False
  1073.                 
  1074.     # ----------------------------------------------------------------------
  1075.     # for whitelist
  1076.     # ----------------------------------------------------------------------
  1077.  
  1078.     def loadWhitelist_ (self, patterns):
  1079.         del patterns[:]
  1080.         if self.whitelistInitialized:
  1081.             globalObjects.whitelist.load()
  1082.             patterns.extend(globalObjects.whitelist.theList)
  1083.         else:
  1084.             self.whitelistInitialized = True
  1085.             patterns.extend(globalObjects.whitelist.theList)
  1086.             
  1087.         for idx, d in enumerate(patterns):
  1088.             d['idx'] = idx
  1089.  
  1090.     def addPattern_toWhitelistWithName_ (self, pat, name):
  1091.         globalObjects.whitelist.add(name, pat)
  1092.         _sendCommandToPlugin(JM_CMD_WHITELIST)
  1093.  
  1094.     def updatePatternOfIndex_toWhitelistwithPattern_andName_ (self, idx, pat, name):
  1095.         globalObjects.whitelist.update(idx, name, pat)
  1096.         _sendCommandToPlugin(JM_CMD_WHITELIST)
  1097.  
  1098.     def removePatternInWhitelistWithIndex_ (self, idx):
  1099.         globalObjects.whitelist.remove(idx)
  1100.         _sendCommandToPlugin(JM_CMD_WHITELIST)
  1101.  
  1102.     # ----------------------------------------------------------------------
  1103.     # for recipient patterns (very similar to those for the whitelist)
  1104.     # ----------------------------------------------------------------------
  1105.  
  1106.     def loadRecipientPatterns (self):
  1107.         # ASSUMPTION: only called ONCE
  1108.         patterns = []
  1109.         patterns.extend(globalObjects.recipientPatterns.theList)
  1110.             
  1111.         for idx, d in enumerate(patterns):
  1112.             d['idx'] = idx
  1113.  
  1114.         return patterns
  1115.  
  1116.     def addPattern_toRecipientPatternsWithName_ (self, pat, name):
  1117.         globalObjects.recipientPatterns.add(name, pat)
  1118.         _sendCommandToPlugin(JM_CMD_RECIPIENTPATTERNS)
  1119.  
  1120.     def updatePatternOfIndex_toRecipientPatternswithPattern_andName_ (self, idx, pat, name):
  1121.         globalObjects.recipientPatterns.update(idx, name, pat)
  1122.         _sendCommandToPlugin(JM_CMD_RECIPIENTPATTERNS)
  1123.  
  1124.     def removePatternInRecipientPatternsWithIndex_ (self, idx):
  1125.         globalObjects.recipientPatterns.remove(idx)
  1126.         _sendCommandToPlugin(JM_CMD_RECIPIENTPATTERNS)
  1127.  
  1128.     def getMyEmailAddressPatterns (self):
  1129.         """Returns a list of dictionary: keys are names and values are regex of the user's email
  1130.         addresses"""
  1131.         myEmailPat = re.compile(r'myEmail(\d+)')
  1132.         ret = []
  1133.         for name, pat in __import__('MetaPatterns').getReservedMPs():
  1134.             mo = myEmailPat.match(name)
  1135.             if mo:
  1136.                 ret.append({'name': 'My Email %s' % mo.group(1), 'pat':pat, 'managed':True})
  1137.  
  1138.         globalObjects.recipientPatterns.addMany(ret)
  1139.         _sendCommandToPlugin(JM_CMD_RECIPIENTPATTERNS)
  1140.  
  1141.         return ret
  1142.  
  1143.     # ----------------------------------------------------------------------
  1144.     # for safe IPs (very similar to those for the recipient patterns)
  1145.     # ----------------------------------------------------------------------
  1146.  
  1147.     def loadSafeIPs (self):
  1148.         # ASSUMPTION: only called ONCE
  1149.         patterns = []
  1150.         patterns.extend(globalObjects.safeIPs.theList)
  1151.             
  1152.         for idx, d in enumerate(patterns):
  1153.             d['idx'] = idx
  1154.  
  1155.         return patterns
  1156.  
  1157.     def addPattern_toSafeIPsWithName_ (self, pat, name):
  1158.         globalObjects.safeIPs.add(name, pat)
  1159.         _sendCommandToPlugin(JM_CMD_SAFEIPS)
  1160.  
  1161.     def updatePatternOfIndex_toSafeIPswithPattern_andName_ (self, idx, pat, name):
  1162.         globalObjects.safeIPs.update(idx, name, pat)
  1163.         _sendCommandToPlugin(JM_CMD_SAFEIPS)
  1164.  
  1165.     def removePatternInSafeIPsWithIndex_ (self, idx):
  1166.         globalObjects.safeIPs.remove(idx)
  1167.         _sendCommandToPlugin(JM_CMD_SAFEIPS)
  1168.  
  1169.     # ----------------------------------------------------------------------
  1170.     # for sites
  1171.     # ----------------------------------------------------------------------
  1172.  
  1173.     def loadBadSites_ (self, badSites):
  1174.         """Load siteDB into badSites (list of dict)."""
  1175.         # get the latest collection of bad sites
  1176.         self._getSiteDBFromPlugin()
  1177.  
  1178.         globalObjects.siteDB.loadIntoList(badSites)
  1179.         for s in badSites:
  1180.             s['time'] = NSDate.dateWithString_(u'%s +0000' % s['time'])
  1181.  
  1182.     def getSiteDBLimit (self):
  1183.         return globalObjects.prefs.siteLimit
  1184.  
  1185.     def setSiteDBLimit_ (self, siteLimit):
  1186.         """Returns True iff siteDB gets pruned."""
  1187.         prefs = globalObjects.prefs
  1188.         if prefs.siteLimit != siteLimit:
  1189.             prefs.siteLimit = siteLimit
  1190.             prefs.writeToFile()
  1191.  
  1192.             reply = _sendCommandToPlugin(JM_CMD_PREFS_SITEDBLIMIT, str(siteLimit))
  1193.             if reply is False:
  1194.                 return globalObjects.siteDB.setSizeLimit(siteLimit)
  1195.             else:
  1196.                 # plugin is running!
  1197.                 self._getSiteDBFromPlugin()    # get the latest collection of bad sites
  1198.  
  1199.             return (reply == JM_REPLY_PRUNED)
  1200.  
  1201.     def canAddBadSite_ (self, site):
  1202.         mo = _ipPat.match(site)
  1203.         if mo and mo.end(0) == len(site):
  1204.             site = site.split('.')
  1205.             if len(site) != 4: return False  # numerical needs 4 components
  1206.         else:
  1207.             # don't add safe site!
  1208.             if globalObjects.safeSitesPattern.search(site): return False
  1209.                 
  1210.             site = site.split('.')
  1211.             if len(site) < 2: return False   # domainn names need at least 2 components
  1212.         
  1213.         l, n, b = globalObjects.siteDB.getOne(site)
  1214.         return l is not None and len(l)      # don't add existing or bogus sites!
  1215.  
  1216.     def addBadSite_withTime_ (self, badSite, timeSec):
  1217.         """Adds a bad site into the siteDB; returns 1 if a positive change happened, 0 for
  1218.         no change, and -1 for negative change (pruning happened)."""
  1219.         # get the latest collection of bad sites
  1220.         self._getSiteDBFromPlugin()
  1221.  
  1222.         siteDB = globalObjects.siteDB
  1223.         oldCount = siteDB.size()
  1224.         siteDB.addOne(badSite.split('.'), timeSec)
  1225.         newCount = siteDB.size()
  1226.  
  1227.         if oldCount < newCount:
  1228.             ret = 1
  1229.         elif oldCount == newCount:
  1230.             ret = 0
  1231.         else:
  1232.             ret = -1
  1233.             
  1234.         if ret != 0:
  1235.             if _sendCommandToPlugin(JM_CMD_SET_SITEDB, str(siteDB)) is False:
  1236.                 # plugin is not running: save the file ourselves
  1237.                 siteDB.writeToFile()
  1238.         
  1239.         return ret
  1240.  
  1241.     def removeBadSite_ (self, badSite):
  1242.         # get the latest collection of bad sites
  1243.         self._getSiteDBFromPlugin()
  1244.  
  1245.         siteDB = globalObjects.siteDB
  1246.         oldCount = siteDB.size()
  1247.         siteDB.removeOne(badSite.split('.'))
  1248.  
  1249.         changeHappened = (oldCount != siteDB.size())
  1250.         if changeHappened:
  1251.             if _sendCommandToPlugin(JM_CMD_SET_SITEDB, str(siteDB)) is False:
  1252.                 # plugin is not running: save the file ourselves
  1253.                 siteDB.writeToFile()
  1254.         
  1255.         return changeHappened
  1256.  
  1257.     def doWhoisOn_ (self, site):
  1258.         try:
  1259.             whoisInPipe, whoisOutPipe = os.popen4(encodeText('whois %s' % site))
  1260.             return decodeText(whoisOutPipe.read())
  1261.         except:
  1262.             return u'Error querying the Whois database (missing whois executable or Internet connection problem?).'
  1263.  
  1264.     def convertBadSiteToSafeSite_ (self, s):
  1265.         return u'(?:^|\.)%s$' % re.escape(s.strip())
  1266.     
  1267.     # ----------------------------------------------------------------------
  1268.     # for safe sites
  1269.     # ----------------------------------------------------------------------
  1270.  
  1271.     def loadSafeSites (self):
  1272.         """Returns a list of safe sites suitable for displaying in a NSTableView.
  1273.  
  1274.         ASSUMPTION: should be only called ONCE."""
  1275.         safeSiteLines = filter(lambda l: len(l), openFile('%ssafeSites' % CONF_PATH).read().split('\n'))
  1276.         safeSiteLines.sort()
  1277.         self.safeSites = sets.Set(safeSiteLines)   # for later removal operations
  1278.         return map(lambda l: {'pat':l}, safeSiteLines)
  1279.  
  1280.     def canAddSafeSite_ (self, safeSite):
  1281.         """Returns True iff safeSite doesn't already exist.
  1282.  
  1283.         ASSUMPTION: loadSafeSites() should've been called earlier than this."""
  1284.         return safeSite not in self.safeSites
  1285.  
  1286.     def addSafeSite_ (self, safeSite):
  1287.         self.safeSites.add(safeSite)
  1288.         openFile('%ssafeSites' % CONF_PATH, 'w').write('\n'.join(self.safeSites))
  1289.         _sendCommandToPlugin(JM_CMD_SAFESITES)
  1290.  
  1291.     def removeSafeSite_ (self, safeSite):
  1292.         self.safeSites.discard(safeSite)
  1293.         openFile('%ssafeSites' % CONF_PATH, 'w').write('\n'.join(self.safeSites))
  1294.         _sendCommandToPlugin(JM_CMD_SAFESITES)
  1295.  
  1296.     # ----------------------------------------------------------------------
  1297.     # for tags window
  1298.     # ----------------------------------------------------------------------
  1299.  
  1300.     def loadTags (self):
  1301.         htmlTags = globalObjects.htmlTags
  1302.         tags = htmlTags.keys()
  1303.         tags.sort()
  1304.         
  1305.         ret = []
  1306.         for tag in tags:
  1307.             attrs = htmlTags[tag]
  1308.             if attrs is None:
  1309.                 attrs = []
  1310.             elif attrs == '*':
  1311.                 attrs = ['*']
  1312.             else:
  1313.                 attrs = list(attrs)
  1314.                 attrs.sort()
  1315.             ret.append({'tag':tag,
  1316.                         'attrs':' '.join(attrs)})
  1317.  
  1318.         return ret
  1319.  
  1320.     def canAddTag_ (self, tag):
  1321.         return globalObjects.htmlTags.get(tag) is None
  1322.  
  1323.     def checkHTMLAttributes_ (self, attrs):
  1324.         attrs = attrs.strip().split(' ')
  1325.         
  1326.         try:
  1327.             attrs.index('*')
  1328.             return len(attrs) == 1
  1329.         except:
  1330.             return True
  1331.         
  1332.     def setTag_withAttributes_ (self, tag, attrs):
  1333.         htmlTags = globalObjects.htmlTags
  1334.         htmlTags.setTag(tag.lower(), attrs.lower())
  1335.         htmlTags.writeToFile()
  1336.         _sendCommandToPlugin(JM_CMD_HTMLTAGS)
  1337.  
  1338.     def removeTag_ (self, tag):
  1339.         htmlTags = globalObjects.htmlTags
  1340.         htmlTags.removeTag(tag.lower())
  1341.         htmlTags.writeToFile()
  1342.         _sendCommandToPlugin(JM_CMD_HTMLTAGS)
  1343.  
  1344.     # ----------------------------------------------------------------------
  1345.     # for preferences window
  1346.     # ----------------------------------------------------------------------
  1347.  
  1348.     def getMatchingModeAndArguments (self):
  1349.         prefs = globalObjects.prefs
  1350.         return (prefs.mode, prefs.modeArgs)
  1351.  
  1352.     def setMatchingMode_andArguments_ (self, mode, args):
  1353.         """Set the matching mode (int) and args (a list of string)."""
  1354.         prefs = globalObjects.prefs
  1355.         if prefs.mode != mode or prefs.modeArgs != args:            
  1356.             prefs.mode = mode
  1357.             prefs.modeArgs = args
  1358.             prefs.writeToFile()
  1359.  
  1360.             # NOTE: this has no effect to the matcher in the GUI it's only to set
  1361.             #       the matcher in the plugin
  1362.             _sendCommandToPlugin(JM_CMD_PREFS_MODE, '%s %s' % (mode, ' '.join(args)))
  1363.  
  1364.     def getActionJunkFlag (self):
  1365.         return globalObjects.prefs.junkMessage
  1366.  
  1367.     def setActionJunkFlag_ (self, junkFlag):
  1368.         prefs = globalObjects.prefs
  1369.         if prefs.junkMessage != junkFlag:
  1370.             prefs.junkMessage = junkFlag
  1371.             prefs.writeToFile()
  1372.             _sendCommandToPlugin(JM_CMD_PREFS_JUNK, str(int(junkFlag)))
  1373.