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

  1. #
  2. #  Property.py
  3. #  JunkMatcher
  4. #
  5. #  Created by Benjamin Han on 2/1/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. #!/usr/bin/env python
  24.  
  25. import datetime
  26. from time import time as now
  27.  
  28. from consts import *
  29. from utilities import *
  30. from emailAddress import *
  31. from checkIP import *
  32. from GlobalObjects import *
  33. from TestRecord import *
  34.  
  35. import DNS
  36.  
  37.  
  38. class Property (object):
  39.     """Base class of all property tests
  40.        --------------------------------
  41.        name: the name of this property test (can contain spaces, in Unicode)
  42.        testRecord: a TestRecord object
  43.        """
  44.     # improving performance by not having __dict__
  45.     __slots__ = ('_rPat', 'name', 'testRecord', 'recipientPattern')
  46.     
  47.     def __init__ (self, name, testRecord, recipientPattern):
  48.         self.name = name
  49.         self.testRecord = testRecord   # thread-safe
  50.         self._rPat = recipientPattern
  51.  
  52.     def __getattr__ (self, name):
  53.         # lazy initialization
  54.         if name == 'recipientPattern':
  55.             if self._rPat:
  56.                 self.recipientPattern = re.compile(self._rPat)
  57.             else:
  58.                 self.recipientPattern = None
  59.             return self.recipientPattern        
  60.         else:
  61.             raise AttributeError('No attribute %s in this %s instance.' % (name, self.__class__.__name__))
  62.  
  63.     def changeRecipientPattern (self, newPattern):
  64.         # CAUTION: self._rPat is left unchanged
  65.         self.recipientPattern = re.compile(newPattern)
  66.         
  67.     def run (self, msg):
  68.         """Run a property test on the msg; returns tuple (result, float) where the result
  69.         can be a bool (True = positive, False = negative) or a string (if the property is
  70.         positive and has special info), and the float is the CPU time spent on this
  71.         property checking (in usec)."""
  72.         start = now()
  73.         result = self._run(msg)
  74.         finish = now()
  75.  
  76.         return result, (finish - start) * 1000000.0
  77.  
  78.     def contentString (self):
  79.         return u''
  80.  
  81.     def getAttribute_ (self, name):
  82.         """This is basically for Obj-C side of PyObjC bridge so we can get at the instance variables"""
  83.         return getattr(self, name)
  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.  
  90. class PropertyMessageMalformed (Property):
  91.     """Positive iff the message is malformed.
  92.     IMPORTANT: STOP DOING MORE TESTS IF THIS ONE IS POSITIVE!"""
  93.     __slots__ = ()
  94.     
  95.     def __init__ (self, testRecord, recipientPattern):
  96.         Property.__init__(self, u'Message is malformed/containing \'\\0\'', testRecord, recipientPattern)
  97.     
  98.     def _run (self, msg):
  99.         return msg.m is None or msg.containingNull
  100.  
  101.  
  102. class PropertyMessageIDMalformed (Property):
  103.     """Positive iff the message ID is malformed or missing."""
  104.     __slots__ = ()
  105.     
  106.     def __init__ (self, testRecord, recipientPattern):
  107.         Property.__init__(self, u'Message ID is malformed/missing', testRecord, recipientPattern)
  108.     
  109.     def _run (self, msg):
  110.         return msg.mID is None
  111.  
  112.  
  113. class PropertySenderMalformed (Property):
  114.     """Positive iff the address is malformed."""
  115.     __slots__ = ()
  116.     
  117.     def __init__ (self, testRecord, recipientPattern):
  118.         Property.__init__(self, u'Sender is malformed', testRecord, recipientPattern)
  119.     
  120.     def _run (self, msg):
  121.         if msg.senderEmail is False:
  122.             return msg.sender
  123.         
  124.         return False
  125.  
  126.  
  127. class PropertySubjectMissing (Property):
  128.     """Positive iff the subject is missing."""
  129.     __slots__ = ()
  130.     
  131.     def __init__ (self, testRecord, recipientPattern):
  132.         Property.__init__(self, u'Subject is missing', testRecord, recipientPattern)
  133.     
  134.     def _run (self, msg):
  135.         return len(msg.subject) == 0
  136.  
  137.  
  138. class PropertyDateMissing (Property):
  139.     """Positive iff the date is missing."""
  140.     __slots__ = ()
  141.     
  142.     def __init__ (self, testRecord, recipientPattern):
  143.         Property.__init__(self, u'Date is missing', testRecord, recipientPattern)
  144.     
  145.     def _run (self, msg):
  146.         return msg.date is None
  147.  
  148.  
  149. class PropertyDateMalformed (Property):
  150.     """Positive iff the date is malformed; after this test
  151.     an attribute 'dateObj' in the Message obj will be added - it's None for malformed
  152.     date, or a datetime object if otherwise."""
  153.     __slots__ = ()
  154.     
  155.     def __init__ (self, testRecord, recipientPattern):
  156.         Property.__init__(self, u'Date is malformed', testRecord, recipientPattern)
  157.     
  158.     def _run (self, msg):
  159.         return msg.dateObj is False
  160.  
  161.  
  162. class PropertyDateInFuture (Property):
  163.     """Positive iff the date is in future."""
  164.     # improving performance by not having __dict__
  165.     __slots__ = ('numMinutes', 'timeDelta')
  166.  
  167.     def __init__ (self, testRecord, recipientPattern, numMinutes):
  168.         Property.__init__(self, u'Date is in future', testRecord, recipientPattern)
  169.         self.numMinutes = numMinutes
  170.         self.timeDelta = datetime.timedelta(minutes = numMinutes)
  171.     
  172.     def _run (self, msg):
  173.         if msg.timeDelta is not None and msg.timeDelta >= self.timeDelta:
  174.             return 'Ahead %s' % msg.timeDelta
  175.  
  176.         return False
  177.     
  178.     def contentString (self):
  179.         return u'Ahead: %d minute(s)' % self.numMinutes
  180.  
  181.     def setNumMinutes_ (self, numMinutes):
  182.         self.numMinutes = numMinutes
  183.         self.timeDelta = datetime.timedelta(minutes = numMinutes)
  184.  
  185.  
  186. class PropertyDateInThePast (Property):
  187.     """Positive iff the date is in the past."""
  188.     # improving performance by not having __dict__
  189.     __slots__ = ('numDays', 'timeDelta')
  190.     
  191.     def __init__ (self, testRecord, recipientPattern, numDays):
  192.         Property.__init__(self, u'Date is in the past', testRecord, recipientPattern)
  193.         self.numDays = numDays
  194.         self.timeDelta = datetime.timedelta(days = -numDays)
  195.     
  196.     def _run (self, msg):
  197.         if msg.timeDelta is not None and msg.timeDelta <= self.timeDelta:
  198.             return 'Behind %s' % -msg.timeDelta
  199.  
  200.         return False
  201.  
  202.     def contentString (self):
  203.         return u'Behind: %d day(s)' % self.numDays
  204.  
  205.     def setNumDays_ (self, numDays):
  206.         self.numDays = numDays
  207.         self.timeDelta = datetime.timedelta(days = -numDays)
  208.  
  209.  
  210. class PropertyRecipientMissing (Property):
  211.     """Positive iff the recipient is missing."""
  212.     __slots__ = ()
  213.     
  214.     def __init__ (self, testRecord, recipientPattern):
  215.         Property.__init__(self, u'Recipient is missing', testRecord, recipientPattern)
  216.     
  217.     def _run (self, msg):
  218.         return msg.numRecipients == 0
  219.  
  220.  
  221. class PropertyRecipientsTooMany (Property):
  222.     """Positive iff there are too many recipients."""
  223.     __slots__ = 'intArg'
  224.     
  225.     def __init__ (self, testRecord, recipientPattern, recipientsLimit):
  226.         Property.__init__(self, u'Too many recipients', testRecord, recipientPattern)
  227.         self.intArg = recipientsLimit
  228.     
  229.     def _run (self, msg):
  230.         if msg.numRecipients > self.intArg:
  231.             return '%d recipient(s)' % msg.numRecipients
  232.  
  233.         return False
  234.  
  235.     def contentString (self):
  236.         return u'> %d recipients' % self.intArg
  237.  
  238.  
  239. class PropertyRecipientsMismatch (Property):
  240.     """Positive iff no match is possible."""
  241.     __slots__ = ()
  242.     
  243.     def __init__ (self, testRecord, recipientPattern):
  244.         Property.__init__(self, u'Recipient(s) mismatch', testRecord, recipientPattern)
  245.     
  246.     def _run (self, msg):
  247.         recipientPatterns = globalObjects.recipientPatterns
  248.         if msg.numRecipients > 0 and recipientPatterns is not None:
  249.             # NOTE: if recipientPatterns has not been configured, we won't report a positive here
  250.             #       (see SimplePatterns.match())
  251.             return not filter(lambda r:recipientPatterns.match(r) is not False, msg.decodedRecipients)
  252.         return False
  253.  
  254.  
  255. class PropertyRecipientMalformed (Property):
  256.     """Positive iff at least one recipient is malformed."""
  257.     __slots__ = ()
  258.     
  259.     def __init__ (self, testRecord, recipientPattern):
  260.         Property.__init__(self, u'Recipient is malformed', testRecord, recipientPattern)
  261.     
  262.     def _run (self, msg):
  263.         if msg.numRecipients == -1:
  264.             return msg.recipients   # it's a string when some recipient address is malformed
  265.                 
  266.         return False
  267.  
  268.  
  269. class PropertyTooFewKnownRecipients (Property):
  270.     """Positive iff too few recipients are on user's address book."""
  271.     # improving performance by not having __dict__
  272.     __slots__ = 'intArg'
  273.     
  274.     def __init__ (self, testRecord, recipientPattern, acquaintantLimit):
  275.         Property.__init__(self, u'Too few known recipients', testRecord, recipientPattern)
  276.         self.intArg = acquaintantLimit
  277.     
  278.     def _run (self, msg):
  279.         if msg.numRecipients > 1:
  280.             numAcquaintants = len(filter(lambda r: r[1] in globalObjects.addressSet,
  281.                                          msg.recipients))
  282.             if numAcquaintants < self.intArg:
  283.                 return '%d acquaintant(s)' % numAcquaintants
  284.  
  285.         return False
  286.  
  287.  
  288. class PropertyHTMLAttachment (Property):
  289.     """Positive iff an HTML attachment exists."""
  290.     __slots__ = ()
  291.     
  292.     def __init__ (self, testRecord, recipientPattern):
  293.         Property.__init__(self, u'HTML attachment', testRecord, recipientPattern)
  294.     
  295.     def _run (self, msg):
  296.         return msg.isHTML
  297.     
  298.  
  299. class PropertyHTMLBadTags (Property):
  300.     """Positive iff an HTML attachment has too many bad tags."""
  301.     # improving performance by not having __dict__
  302.     __slots__ = 'intArg'
  303.  
  304.     def __init__ (self, testRecord, recipientPattern, badTagLimit):
  305.         Property.__init__(self, u'HTML has too many bad tags', testRecord, recipientPattern)
  306.         self.intArg = badTagLimit
  307.     
  308.     def _run (self, msg):
  309.         if msg.isHTML:
  310.             numBadTags = len(msg.htmlBody.badTagList)
  311.             if numBadTags >= self.intArg:
  312.                 return '%d bad tag(s)' % numBadTags
  313.  
  314.         return False
  315.  
  316.     def contentString (self):
  317.         return u'>= %d bad tag(s)' % self.intArg
  318.  
  319.  
  320. class PropertyHTMLHiddenURLs (Property):
  321.     """Positive iff an HTML attachment has too many hidden URLs."""
  322.     # improving performance by not having __dict__
  323.     __slots__ = 'intArg'
  324.  
  325.     def __init__ (self, testRecord, recipientPattern, hiddenURLLimit):
  326.         Property.__init__(self, u'HTML has too many hidden URLs', testRecord, recipientPattern)
  327.         self.intArg = hiddenURLLimit
  328.     
  329.     def _run (self, msg):
  330.         if msg.isHTML:
  331.             numHiddenURLs = len(msg.htmlBody.hiddenURLList)
  332.             if numHiddenURLs >= self.intArg:
  333.                 return '%d hidden URL(s)' % numHiddenURLs
  334.  
  335.         return False
  336.  
  337.     def contentString (self):
  338.         return u'>= %d hidden URL(s)' % self.intArg
  339.  
  340.  
  341. class PropertyHTMLVacuousTags (Property):
  342.     """Positive iff an HTML attachment has too many vacuous tags."""
  343.     # improving performance by not having __dict__
  344.     __slots__ = 'intArg'
  345.  
  346.     def __init__ (self, testRecord, recipientPattern, vacuousTagLimit):
  347.         Property.__init__(self, u'HTML has too many vacuous tags', testRecord, recipientPattern)
  348.         self.intArg = vacuousTagLimit
  349.     
  350.     def _run (self, msg):
  351.         if msg.isHTML:
  352.             numVacuousTags = len(msg.htmlBody.vacuousTagList)
  353.             if numVacuousTags >= self.intArg:
  354.                 return '%d vacuous tag(s)' % numVacuousTags
  355.  
  356.         return False
  357.  
  358.     def contentString (self):
  359.         return u'>= %d vacuous tag(s)' % self.intArg
  360.  
  361.  
  362. class PropertyBlankRendering (Property):
  363.     """Positive iff an email renders nothing (either text or HTML)."""
  364.     __slots__ = ()
  365.     
  366.     def __init__ (self, testRecord, recipientPattern):
  367.         Property.__init__(self, u'Blank rendering', testRecord, recipientPattern)
  368.     
  369.     def _run (self, msg):
  370.         if msg.isHTML:
  371.             return len(msg.rendering) == 0
  372.         else:
  373.             return len(msg.body) == 0
  374.  
  375.  
  376. class PropertyHasBadSites (Property):
  377.     """Positive iff an email refers to a bad site."""
  378.     __slots__ = ()
  379.     
  380.     def __init__ (self, testRecord, recipientPattern):
  381.         Property.__init__(self, u'Has bad site(s)', testRecord, recipientPattern)
  382.     
  383.     def _run (self, msg):
  384.         if msg.badSite is not None:
  385.             return msg.badSite
  386.         
  387.         return False
  388.  
  389.  
  390. class PropertyOpenRelay (Property):
  391.     """Positive iff an any IP in the headers, except for the safe IPs, is blacklisted."""
  392.     # improving performance by not having __dict__
  393.     __slots__ = ('timeout', 'numLastIPs', 'blackLists')
  394.  
  395.     def __init__ (self, testRecord, recipientPattern, timeout, numLastIPs, blackLists):
  396.         Property.__init__(self, u'Open relay', testRecord, recipientPattern)
  397.         self.timeout = timeout
  398.         self.numLastIPs = numLastIPs
  399.         self.blackLists = blackLists        
  400.     
  401.     def _run (self, msg):
  402.         if len(msg.headerIPs):
  403.             # NOTE: if safeIPs has not been configured, we check every IP
  404.             #       (see SimplePatterns.match())
  405.             safeIPs = globalObjects.safeIPs
  406.             result = checkIPList(filter(lambda ip:safeIPs.match(ip) is False,
  407.                                         msg.headerIPs)[-self.numLastIPs:],
  408.                                  self.timeout, self.blackLists)
  409.  
  410.             if result is not None:
  411.                 return '%s (%s)' % (result[0], result[1])
  412.         
  413.         return False
  414.  
  415.     def contentString (self):
  416.         return u'Check last %d IPs with timeout %.1f sec' % (self.numLastIPs, self.timeout)
  417.  
  418.  
  419. class PropertyDomainNoMX (Property):
  420.     """Positive iff the domain of the sender's email address has no MX record."""
  421.     __slots__ = ()
  422.     
  423.     def __init__ (self, testRecord, recipientPattern):
  424.         Property.__init__(self, u'Domain has no MX record or bogus', testRecord, recipientPattern)
  425.     
  426.     def _run (self, msg):
  427.         if msg.senderDomain:
  428.             try:
  429.             
  430.                 try:
  431.                     # TO-DO: is PyDNS thread-safe?
  432.                     DNS.DiscoverNameServers()
  433.                     if len(map(lambda x:x['data'],
  434.                                DNS.DnsRequest(msg.senderDomain, qtype = 'mx',
  435.                                               timeout = DEFAULT_MX_TIMEOUT).req().answers)) == 0:
  436.                         return msg.senderDomain
  437.             
  438.                 except DNS.DNSError, e:
  439.                     # if there's no DNS hosts for lookup, assume domain has MX
  440.                     # but timeout is NOT ok
  441.                     if str(e) != 'no working nameservers found':
  442.                         return msg.senderDomain
  443.             
  444.             except:
  445.                 # for unknown reasons the DNS query fails (maybe due to bad setup)
  446.                 pass
  447.  
  448.         return False
  449.  
  450. class PropertyPhishingURL (Property):
  451.     """Positive iff an HTML email contains at least a phishing URL."""
  452.     # improving performance by not having __dict__
  453.     __slots__ = 'checkWhitelistedEmail'
  454.  
  455.     def __init__ (self, testRecord, recipientPattern, checkWhitelistedEmail):
  456.         Property.__init__(self, u'Has a phishing URL', testRecord, recipientPattern)
  457.         self.checkWhitelistedEmail = checkWhitelistedEmail
  458.     
  459.     def _run (self, msg):
  460.         t = msg.phishingURL
  461.         if t:
  462.             return u'%s claimed to be %s' % (t[-1][0], t[-1][1])
  463.         else:
  464.             return False
  465.  
  466. if __name__ == '__main__':
  467.     import sys
  468.  
  469.     if len(sys.argv) == 1:
  470.         print 'Usage: ./Property.py <filename>'
  471.         print '   * filename is the name of the file containing email raw source.'
  472.         sys.exit(1)
  473.  
  474.     from Message import *
  475.  
  476.     oldSiteDBSize = globalObjects.siteDB.size()
  477.  
  478.     msg = Message(open(sys.argv[1]).read())
  479.  
  480.     # PropertyMessageMalformed must be the *first* thing to test
  481.     # all the other tests can proceed only if PropertyMessageMalformed is negative
  482.     p = PropertyMessageMalformed(TestRecord(), None)
  483.     result, cpuTime = p.run(msg)
  484.     if result:
  485.         print '* %s'%p.name
  486.         
  487.     else:
  488.         properties = [PropertyMessageIDMalformed(TestRecord(), None),
  489.                       PropertySenderMalformed(TestRecord(), None),
  490.                       PropertySubjectMissing(TestRecord(), None),
  491.                       PropertyDateMissing(TestRecord(), None),
  492.                       PropertyDateMalformed(TestRecord(), None),
  493.                       PropertyDateInFuture(TestRecord(), None, 120),
  494.                       PropertyDateInThePast(TestRecord(), None, 2),
  495.                       PropertyRecipientMissing(TestRecord(), None),
  496.                       PropertyRecipientsTooMany(TestRecord(), None, 5),
  497.                       PropertyRecipientsMismatch(TestRecord(), None),
  498.                       PropertyRecipientMalformed(TestRecord(), None),
  499.                       PropertyTooFewKnownRecipients(TestRecord(), None, 1),
  500.                       PropertyHTMLAttachment(TestRecord(), None),
  501.                       PropertyHTMLBadTags(TestRecord(), None, 5),
  502.                       PropertyHTMLHiddenURLs(TestRecord(), None, 1),
  503.                       PropertyHTMLVacuousTags(TestRecord(), None, 3),
  504.                       PropertyBlankRendering(TestRecord(), None),
  505.                       PropertyHasBadSites(TestRecord(), None),
  506.                       PropertyOpenRelay(TestRecord(), None, 0.5, 3, ['bl.spamcop.net']),
  507.                       PropertyDomainNoMX(TestRecord(), None)]
  508.  
  509.         for r in properties:
  510.             result, cpuTime = r.run(msg)
  511.             if result is not False:
  512.                 if result is True:
  513.                     s = '* %s' % r.name
  514.                 else:
  515.                     s = encodeText('* %s (%s)' % (r.name, result))
  516.                     
  517.                 print '%s: %f usec(s)' % (s, cpuTime)
  518.  
  519.         msg.addSites()
  520.         print '* SiteDB count change:', globalObjects.siteDB.size() - oldSiteDBSize
  521.