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

  1. #
  2. #  Message.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 email, datetime, time, string
  26. from email.Header import decode_header
  27.  
  28. from consts import *
  29. from utilities import *
  30. from emailAddress import *
  31. from parseURL import *
  32. from HTMLBody import *
  33.  
  34.  
  35. # the following headers are excluded from Message.headers since they are included
  36. # elsewhere in Message
  37. _ignoreHeaders = sets.Set(['from', 'sender', 'to', 'cc', 'subject'])
  38.  
  39. _receivedDatePat = re.compile(r'(\d+)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d) \+0000\@')
  40. _rawMIDPat = re.compile(r'<(.+\@.+)>')
  41. _ipPat = re.compile(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
  42. _asciiPat = re.compile(r'[\020-\0177]*')
  43. _anchorPat = re.compile(r'(?i)<a\s+[^>]*href\s*=\s*[\'"]?\s*(https?:/?/?[^"\'<> \t\n\r\f\v]+)[^>]*?>\s*(https?:/?/?[^"\'<> \t\n\r\f\v]+)\s*</a>')
  44.  
  45.  
  46. class Message (object):
  47.     """An email message
  48.        ----------------
  49.        WARNING: if a message is malformed, m is None and you should NOT call any method or
  50.        try to get any attribute except for msgSrc, receivedDate and m!
  51.  
  52.        The beginning of the input msgSrc may have an received date string in UTC. If not,
  53.        self.receivedDate is set to None.
  54.        
  55.        I. the following are set by __init__()
  56.  
  57.        (the following two can generate all the other data)
  58.        msgSrc: the raw source of the message
  59.        receivedDate: date/time (UTC) when the message was received; a datetime obj; could
  60.          be None if it is not indicated in the beginning of the input msgSrc.
  61.        containingNull: True iff msgSrc contains '\0' (which will be removed)
  62.        
  63.        m: Python email object (if the email is malformed, m is None and none of the
  64.                                following attributes exists)
  65.        mID: message ID (None if message ID is malformed/missing)
  66.        subject: the message subject (Unicode)
  67.        sender: the sender of the message, in the full form (Unicode)
  68.        senderEmail: the email address of the sender (None if no sender, False if malformed)
  69.        senderDomain: the email domain of the sender (None if no sender, False if malformed)
  70.        date: the date this email was sent - a string (None if missing)
  71.        headers: complete headers except for those specified in _ignoreHeaders (Unicode)
  72.  
  73.        II. the following are set by _decode()
  74.  
  75.        charsets: a newline-separated string of all character sets used in this email
  76.        charset: a representative charset (a random one from charsets); None if no charset is
  77.            available
  78.        body: the decoded message body; for plain text emails it's the text, for HTML mails it's
  79.            the cleaned HTML code (Unicode)
  80.        htmlBody: for HTML emails only - an HTMLBody object (Unicode)
  81.        isHTML: True iff body is in HTML
  82.        filenames:  a newline-separated string of all attachment filenames
  83.        urlDict: a dictionary of URLs used in the email (Unicode and in lowercase; URLs can
  84.            contain weird encodings!)
  85.  
  86.        III. the following are set by other methods
  87.  
  88.        dateObj: set by _setDateObj(); False if date is malformed, None if date is missing,
  89.            otherwise it's a datetime obj converted from self.date (UTC).
  90.        timeDelta: set by _setTimeDelta(); None if self.dateObj is malformed/missing, otherwise
  91.            it's a timedelta object = self.dateObj - self.receivedDate.
  92.            
  93.        recipients: set by _setRecipients(); a list of tuples (text, email address, domain) when
  94.            none of the recipients is malformed (note text is *not* decoded - see emailAddress.py);
  95.            otherwise it's the part of addresses that are near the error point.
  96.        numRecipients: set by _setRecipients(): length of self.recipients; -1 if error occurs.
  97.        decodedRecipients: set by _setDecodedRecipients(): a list of decoded recipients, in
  98.            full form and Unicode.
  99.        
  100.        badSite: set by _setSites(); the first bad site found against globalObjects.siteDB;
  101.            None if no bad site is found.
  102.  
  103.        phishingURL: set by _setPhishingURL(); it's a tuple ((s1, e1), (s2, e2),
  104.            (real site, claimed site)) - the first sub-tuple is the span of the *real* URL,
  105.            the 2nd sub-tuple is the span of the *claimed* URL, and the 3rd sub-tuple is
  106.            self-explanatory (they are lowercase strings of the domain names); if no phishing
  107.            URL is found, it's None.
  108.  
  109.        rendering: set by HTMLBody.setRendering().
  110.  
  111.        headerIPs: set by _setHeaderIPs().
  112.  
  113.  
  114.        IMPORTANT ASSUMPTION: One Message instance per thread!
  115.     """
  116.     # improving performance by not having __dict__
  117.     __slots__ = ('msgSrc', 'receivedDate', 'containingNull', 'm', 'mID', 'subject', 'sender', 'senderEmail', 'senderDomain', 'date', 'headers',
  118.                  '_charsetSet', '_charsetList', '_fnList', 'charsets', 'charset', 'body', 'htmlBody', 'isHTML', 'filenames', 'urlDict',
  119.                  '_sites', 'dateObj', 'timeDelta', 'recipients', 'numRecipients', 'decodedRecipients', 'badSite', 'phishingURL', 'rendering',
  120.                  'headerIPs')
  121.  
  122.     identityTable = __import__('string').maketrans('', '')
  123.     
  124.     def __init__ (self, msgSrc):
  125.         # The beginning of msgSrc might have a received date string
  126.         # e.g., "2005-02-06 15:57:37 +0000@" (NOTE the '@' delimiter)
  127.         # the date is always in UTC so the time zone is always +0000
  128.         mo = _receivedDatePat.match(msgSrc)
  129.         if mo:
  130.             msgSrc = msgSrc[mo.end(0):]
  131.             self.receivedDate = datetime.datetime(int(mo.group(1)), int(mo.group(2)), int(mo.group(3)),
  132.                                                   int(mo.group(4)), int(mo.group(5)), int(mo.group(6)))
  133.         else:            
  134.             self.receivedDate = None
  135.  
  136.         self.msgSrc = msgSrc
  137.  
  138.         # check and clean out possible null chars
  139.         oldLength = len(msgSrc)
  140.         msgSrc = msgSrc.translate(Message.identityTable, '\0')
  141.         self.containingNull = (oldLength != len(msgSrc))
  142.         
  143.         try:            
  144.             m = email.message_from_string(msgSrc)
  145.             self.m = m
  146.         except:
  147.             # malformed emails
  148.             self.m = None
  149.             return
  150.  
  151.         mID = m['Message-Id']
  152.         if mID is not None:
  153.             mo = _rawMIDPat.search(mID)
  154.             if mo is None: self.mID = None
  155.             else: self.mID = mo.group(1)
  156.         else:
  157.             self.mID = None
  158.  
  159.         self._charsetSet = sets.Set()
  160.  
  161.         # set self.subject - self._charsetSet will be updated
  162.         subj = m['Subject']
  163.         if subj:
  164.             try:
  165.                 self.subject = decodeTextList(decode_header(subj), self._charsetSet).strip()
  166.             except:
  167.                 # error occurred in decode_header()
  168.                 self.subject = u''
  169.         else:
  170.             self.subject = u''        
  171.  
  172.         # set self.sender, self.senderEmail and self.senderDomain -
  173.         # self._charsetSet will be updated
  174.         sender = m['From']
  175.         if sender:
  176.             try:
  177.                 self.sender = decodeTextList(decode_header(sender), self._charsetSet).strip()
  178.                 self.senderEmail, self.senderDomain = validateEmailAddress(sender)
  179.             except:
  180.                 # error occurred in decode_header()
  181.                 self.sender = u''
  182.                 self.senderEmail = self.senderDomain = None
  183.         else:
  184.             self.sender = u''
  185.             self.senderEmail = self.senderDomain = None
  186.  
  187.         self.date = m['Date']
  188.  
  189.         # set up defaultEncoding
  190.         if len(self._charsetSet):
  191.             defaultEncoding = self._charsetSet.pop()
  192.             self._charsetSet.add(defaultEncoding)
  193.         else:
  194.             defaultEncoding = None
  195.             
  196.         # set up _charsetList and correct common mispellings
  197.         self._charsetList = []            
  198.         for c in m.get_charsets():
  199.             if c:
  200.                 mispelling = charsetMispellings.get(c)
  201.                 if mispelling: c = mispelling
  202.                 self._charsetSet.add(c)
  203.             else:
  204.                 # encoding is None; fall back to the defaultEncoding
  205.                 c = defaultEncoding
  206.                 if c: self._charsetSet.add(c)
  207.  
  208.             self._charsetList.append(c)
  209.  
  210.         # set up headers
  211.         self.headers = decodeText('\n'.join([': '.join((hk, hv))
  212.                                              for hk, hv in filter(lambda i:i[0].lower() not in _ignoreHeaders,
  213.                                                                   self.m.items())]))
  214.  
  215.     def __getattr__ (self, name):
  216.         if name == 'charsets':
  217.             self._setDecodedRecipients()
  218.             return self.charsets
  219.         elif name == 'charset':
  220.             self._setDecodedRecipients()
  221.             return self.charset
  222.         elif name == 'body':
  223.             self._decode()
  224.             return self.body
  225.         elif name == 'htmlBody':
  226.             self._decode()
  227.             return self.htmlBody
  228.         elif name == 'isHTML':
  229.             self._decode()
  230.             return self.isHTML
  231.         elif name == 'filenames':
  232.             self._decode()
  233.             return self.filenames
  234.         elif name == 'urlDict':
  235.             self._decode()
  236.             return self.urlDict
  237.         elif name == 'recipients':
  238.             self._setRecipients()
  239.             return self.recipients
  240.         elif name == 'numRecipients':
  241.             self._setRecipients()
  242.             return self.numRecipients
  243.         elif name == 'decodedRecipients':
  244.             self._setDecodedRecipients()
  245.             return self.decodedRecipients
  246.         elif name == '_sites':
  247.             self._setSites()
  248.             return self._sites
  249.         elif name == 'badSite':
  250.             self._setSites()
  251.             return self.badSite
  252.         elif name == 'phishingURL':
  253.             self._setPhishingURL()
  254.             return self.phishingURL
  255.         elif name == 'headerIPs':
  256.             self._setHeaderIPs()
  257.             return self.headerIPs
  258.         elif name == 'dateObj':
  259.             self._setDateObj()
  260.             return self.dateObj
  261.         elif name == 'timeDelta':
  262.             self._setTimeDelta()
  263.             return self.timeDelta
  264.         elif name == 'rendering':
  265.             # for HTML messages the rendering is different from the message body
  266.             # (using elinks to provide the HTML rendering); otherwise it's the body
  267.             if self.isHTML:
  268.                 self.htmlBody.setRendering()
  269.                 self.rendering = self.htmlBody.rendering
  270.             else:
  271.                 self.rendering = self.body
  272.             return self.rendering
  273.         else:
  274.             raise AttributeError('No attribute %s in this %s instance.' % (name, self.__class__.__name__))
  275.  
  276.     def isMultipart (self):
  277.         return self.m.is_multipart()
  278.  
  279.     def __decodePartOfEmail (self, part, charset):
  280.         """Returns unicode object or HTMLBody object (if part is in HTML) if payload exists,
  281.         otherwise returns None; updates self.isHTML and self._charsetSet accordingly."""
  282.         ret = None
  283.         
  284.         if part.get_content_maintype() == 'text':
  285.             payload = part.get_payload(decode = True)
  286.             if payload:
  287.                 # Invariance: payload is not decoded; but ret is
  288.                 if part.get_content_subtype() == 'html':
  289.                     self.isHTML = True
  290.                     ret = HTMLBody(payload, charset)
  291.                     if ret.encoding:
  292.                         self._charsetSet.add(ret.encoding)  # spelling should have been checked in HTMLBody
  293.                 else:
  294.                     ret = decodeText(payload, charset).strip()
  295.                     
  296.         fn = part.get_filename()
  297.         if fn is None: fn = part.get_param('name')
  298.         if fn: self._fnList.append(fn)
  299.  
  300.         return ret
  301.  
  302.     def _decode (self):
  303.         """Decode for content from self.m (see the attributes set by this method in
  304.         the docstring of self); returns nothing."""
  305.         self.isHTML = False
  306.         self._fnList = []
  307.         charsetIndex = 0
  308.         htmlBodyNotFound = True
  309.  
  310.         if self.m.is_multipart():
  311.             # multi-part email
  312.             contentList = []
  313.             for part in self.m.walk():
  314.                 if htmlBodyNotFound:
  315.                     payload = self.__decodePartOfEmail(part, self._charsetList[charsetIndex])
  316.  
  317.                     if self.isHTML:
  318.                         # when self.isHTML is True, payload is guaranteed to not be None
  319.                         self.htmlBody = payload
  320.                         self.body = payload.content
  321.                         htmlBodyNotFound = False
  322.                     else:
  323.                         if payload:
  324.                             contentList.append(payload)
  325.  
  326.                 else:
  327.                     # we're only interested in collecting filenames if HTML body is already found
  328.                     fn = part.get_filename()
  329.                     if fn is None: fn = part.get_param('name')
  330.                     if fn: self._fnList.append(fn)
  331.                 
  332.                 charsetIndex += 1
  333.                         
  334.             if htmlBodyNotFound:
  335.                 # it's not an HTML body
  336.                 self.body = ' '.join(contentList).strip()
  337.  
  338.         else:
  339.             # single-part email
  340.             payload = self.__decodePartOfEmail(self.m, self._charsetList[charsetIndex])
  341.             if payload:
  342.                 if self.isHTML:
  343.                     self.htmlBody = payload
  344.                     self.body = payload.content
  345.                 else:
  346.                     self.body = payload
  347.             else: self.body = u''
  348.  
  349.         # set urlDict
  350.         if self.isHTML:
  351.             self.urlDict = self.htmlBody.urlDict
  352.         else:
  353.             self.urlDict = {}
  354.             d = self.urlDict
  355.             for url in map(string.lower, filter(lambda s:s,
  356.                                                 [m.group(0) for m in httpPat.finditer(self.body)])):
  357.                 d[url] = d.get(url, 0) + 1
  358.         
  359.         self.filenames = '\n'.join(self._fnList)
  360.  
  361.     def _setDateObj (self):
  362.         """Sets self.dateObj; NO return value."""
  363.         if self.date is not None:
  364.             try:
  365.                 # TO-DO: according to Python's doc, mktime_tz() might introduce minor inaccuracies during DST changes.
  366.                 self.dateObj = datetime.datetime.utcfromtimestamp(email.Utils.mktime_tz(email.Utils.parsedate_tz(self.date)))
  367.             except:
  368.                 self.dateObj = False
  369.         else:
  370.             # missing date counts as *not* malformed!
  371.             self.dateObj = None
  372.         
  373.     def _setTimeDelta (self):
  374.         """Sets self.timeDelta; NO return value."""
  375.         if self.dateObj and self.receivedDate:
  376.             self.timeDelta = self.dateObj - self.receivedDate  # normally timeDelta should be < 0
  377.         else:
  378.             self.timeDelta = None
  379.  
  380.     def _setRecipients (self):
  381.         """Parses the To and CC header fields for recipient email addresses:
  382.         
  383.         1. It sets self.recipients - if successful it's a list of tuples
  384.            (text, email address, domain); if not it's a string (see emailAddress.py);
  385.         2. It also sets self.numRecipients: -1 if error occurs;
  386.         2. NO return value
  387.         """        
  388.         recp = self.m.get_all('to')
  389.         if recp is None:
  390.             recp = self.m.get_all('cc')
  391.         else:
  392.             recp2 = self.m.get_all('cc')
  393.             if recp2: recp.extend(recp2)
  394.  
  395.         if recp is None:
  396.             self.recipients = []
  397.             self.numRecipients = 0
  398.         else:
  399.             # if any email address is malformed, self.recipients becomes a string
  400.             # (the text that's skipped in parsing the email addresses)
  401.             self.recipients = extractEmailAddresses(', '.join(recp))
  402.             if type(self.recipients) is list:
  403.                 self.numRecipients = len(self.recipients)
  404.             else: 
  405.                 self.numRecipients = -1
  406.  
  407.     def _setDecodedRecipients (self):
  408.         """Decode the recipient address(es) based on the embedded encoding(s); sets
  409.         self.decodedRecipients; NO return value"""
  410.         if type(self.recipients) is list:
  411.             self.decodedRecipients = []
  412.             try:    # decode as much as possible
  413.                 self.decodedRecipients.extend(map(lambda r: decodeTextList(decode_header(r[0]),
  414.                                                                            self._charsetSet),
  415.                                                   self.recipients))                
  416.             except:
  417.                 pass
  418.  
  419.         else:
  420.             self.decodedRecipients = []
  421.  
  422.         self.charsets = '\n'.join(self._charsetSet)
  423.         if len(self._charsetSet):
  424.             self.charset = self._charsetSet.pop()
  425.         else:
  426.             self.charset = None
  427.  
  428.     def _setSites (self):
  429.         """ Returns nothing."""
  430.         # stores only sites from parseURL that is not a safe site, and contains only ASCII
  431.         # CAUTION: parseURL() might return None
  432.         safeSitesPattern = globalObjects.safeSitesPattern
  433.         sites = sets.Set(filter(lambda url: _asciiPat.match(url),
  434.                                 filter(lambda url: safeSitesPattern.search(url) is None,
  435.                                        map(lambda l:'.'.join(l),
  436.                                            filter(lambda l: l, map(parseURL, self.urlDict.keys()))))))
  437.  
  438.         # _sites is a list of (nameComponents, node, b) where nameComponents is
  439.         # a sequence of name components, node is a SiteDB node, and b is a boolean
  440.         # (see getOne() in SiteDB.py); this is for calling addSites() later (see getOne(),
  441.         # addOne() and addOneToNode() in SiteDB.py)
  442.         self._sites = []
  443.         self.badSite = None
  444.  
  445.         if sites:
  446.             siteDB = globalObjects.siteDB
  447.             for site in sites:
  448.                 # this is to weed out the ill-encoded site from being added
  449.                 result = siteDB.getOne(site.split('.'))
  450.                 l = result[0]
  451.                 if l is not None:  # otherwise the site is bogus
  452.                     self._sites.append(result)
  453.                     if self.badSite is None and len(l) == 0:
  454.                         self.badSite = site
  455.  
  456.     def _setPhishingURL (self):
  457.         if self.isHTML:
  458.             for moIter in _anchorPat.finditer(self.body):
  459.                 # don't bother using urllib.unquote() cuz we're after the inequality!
  460.                 realSite = parseURL(moIter.group(1).lower())
  461.                 claimedSite = parseURL(moIter.group(2).lower())
  462.                 if realSite != claimedSite:
  463.                     self.phishingURL = (moIter.span(1), moIter.span(2),
  464.                                         ('.'.join(realSite), '.'.join(claimedSite)))
  465.                     return
  466.                 
  467.         self.phishingURL = None
  468.  
  469.     def addSites (self):
  470.         """Add the collected sites into globalObjects.siteDB; returns True if at least one site is added."""
  471.         if self._sites:
  472.             siteDB = globalObjects.siteDB
  473.             for l, n, b in self._sites:
  474.                 if n is None:
  475.                     siteDB.addOne(l, time.time())
  476.                 else:
  477.                     siteDB.addOneToNode(l, n, b, time.time())
  478.             return True
  479.         return False
  480.  
  481.     def removeSites (self, countMatters = True):
  482.         """Remove the collected sites from globalObjects.siteDB; returns True iff at least one site is removed."""
  483.         safeSitesPattern = globalObjects.safeSitesPattern
  484.         sites = sets.Set(filter(lambda url: safeSitesPattern.search(url) is None,
  485.                                 map(lambda l:'.'.join(l),
  486.                                     filter(lambda l: l, map(parseURL, self.urlDict.keys())))))
  487.  
  488.         if sites:
  489.             siteDB = globalObjects.siteDB
  490.             ret = False
  491.             for site in sites:
  492.                 if ret:
  493.                     siteDB.removeOne(site.split('.'), countMatters)
  494.                 else:
  495.                     ret = siteDB.removeOne(site.split('.'), countMatters)
  496.  
  497.             return ret
  498.         return False
  499.  
  500.     def _setHeaderIPs (self):
  501.         """Collects all the IPs mentioned in the headers in headerIPs, in
  502.         reversed chronological order."""
  503.         rList = self.m.get_all('Received')
  504.         ipSet = sets.Set()
  505.         self.headerIPs = []
  506.  
  507.         if rList:
  508.             for line in rList:
  509.                 for moIter in _ipPat.finditer(line):
  510.                     ip = moIter.group(0)
  511.                     if not ip in ipSet:
  512.                         ipSet.add(ip)
  513.                         self.headerIPs.insert(0, moIter.group(0))
  514.  
  515.     def show (self):
  516.         """Just a way to dump all relevant info on screen."""
  517.         print 'This message is multi-part:', self.isMultipart()
  518.         if (self.receivedDate):
  519.             print 'Received date (UTC):', self.receivedDate
  520.         print 'Subject:', encodeText(self.subject)
  521.         print 'Sender:', encodeText(self.sender)
  522.         print 'Sender email:', self.senderEmail
  523.         print 'Sender domain:', self.senderDomain
  524.         print 'Is HTML:', self.isHTML
  525.         if len(self.charsets):
  526.             print 'Charsets:', ', '.join(self.charsets.split('\n'))
  527.         if len(self.filenames):
  528.             print 'Filenames:', ', '.join(self.filenames.split('\n'))
  529.         if self.numRecipients > 0:
  530.             print 'Decoded Recipients:', encodeText(u', '.join(self.decodedRecipients))
  531.             print 'Recipient Emails:', ', '.join([r[1] for r in self.recipients])
  532.         print 'Bad site:', self.badSite
  533.         if len(self.headerIPs):
  534.             print 'Header IPs:', ', '.join(self.headerIPs)
  535.         print 'Date Obj:', self.dateObj
  536.         print 'Time delta:', self.timeDelta
  537.         print
  538.         print '-------------------- Headers --------------------'
  539.         print self.headers
  540.         print
  541.         print '-------------------- Body --------------------'
  542.         print encodeText(self.body)
  543.         print '-------------------- Rendering --------------------'
  544.         print encodeText(self.rendering)
  545.     
  546.         if self.isHTML:
  547.             numHiddenURLs = len(self.htmlBody.hiddenURLList)
  548.             if numHiddenURLs:
  549.                 print '-------------------- %d Hidden URL(s) --------------------'%numHiddenURLs
  550.                 for idx, (start, end) in enumerate(self.htmlBody.hiddenURLList):
  551.                     print idx, encodeText(self.htmlBody.contentWithoutEntities[start:end])
  552.  
  553.             numBadTags = len(self.htmlBody.badTagList)
  554.             if numBadTags:
  555.                 print '-------------------- %d Bad tag(s) --------------------'%numBadTags
  556.                 for idx, (start, end) in enumerate(self.htmlBody.badTagList):
  557.                     print idx, encodeText(self.htmlBody.contentWithoutEntities[start:end])
  558.                 
  559.             numVacuousTags = len(self.htmlBody.vacuousTagList)
  560.             if numVacuousTags:
  561.                 print '-------------------- %d Vacuous tag(s) --------------------'%numVacuousTags
  562.                 for idx, (start, end) in enumerate(self.htmlBody.vacuousTagList):
  563.                     print idx, encodeText(self.htmlBody.contentWithoutBadTags[start:end])
  564.  
  565.         numURLs = len(self.urlDict)
  566.         if numURLs:
  567.             print '-------------------- %d URL(s) --------------------'%numURLs
  568.             for idx, (url, count) in enumerate(self.urlDict.items()):
  569.                 print '%d %s (%d)' % (idx, encodeText(url), count)
  570.  
  571.  
  572. if __name__ == '__main__':
  573.     import sys
  574.  
  575.     if len(sys.argv) == 1:
  576.         print 'Usage: ./Message.py <filename>'
  577.         print '   * filename is the name of the file containing email raw source.'
  578.         sys.exit(1)
  579.  
  580.     oldSiteDBSize = globalObjects.siteDB.size()
  581.  
  582.     msg = Message(open(sys.argv[1]).read())
  583.     msg.show()
  584.     msg.addSites()
  585.  
  586.     print
  587.     print '* SiteDB count change:', globalObjects.siteDB.size() - oldSiteDBSize
  588.