Changeset 507


Ignore:
Timestamp:
01/11/11 14:30:31 (12 years ago)
Author:
bas
Message:

converted tabs to spaces

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/email2trac.py.in

    r506 r507  
    5050 * Commandline opions:
    5151                -h,--help
    52                                 -d, --debug
     52                -d, --debug
    5353                -f,--file  <configuration file>
    5454                -n,--dry-run
     
    8989
    9090class SaraDict(UserDict.UserDict):
    91         def __init__(self, dictin = None):
    92                 UserDict.UserDict.__init__(self)
    93                 self.name = None
    94                
    95                 if dictin:
    96                         if dictin.has_key('name'):
    97                                 self.name = dictin['name']
    98                                 del dictin['name']
    99                         self.data = dictin
    100                        
    101         def get_value(self, name):
    102                 if self.has_key(name):
    103                         return self[name]
    104                 else:
    105                         return None
    106                                
    107         def __repr__(self):
    108                 return repr(self.data)
    109 
    110         def __str__(self):
    111                 return str(self.data)
    112                        
    113         def __getattr__(self, name):
    114                 """
    115                 override the class attribute get method. Return the value
    116                 from the dictionary
    117                 """
    118                 if self.data.has_key(name):
    119                         return self.data[name]
    120                 else:
    121                         return None
    122                        
    123         def __setattr__(self, name, value):
    124                 """
    125                 override the class attribute set method only when the UserDict
    126                 has set its class attribute
    127                 """
    128                 if self.__dict__.has_key('data'):
    129                         self.data[name] = value
    130                 else:
    131                         self.__dict__[name] = value
    132 
    133         def __iter__(self):
    134                 return iter(self.data.keys())
     91    def __init__(self, dictin = None):
     92        UserDict.UserDict.__init__(self)
     93        self.name = None
     94       
     95        if dictin:
     96            if dictin.has_key('name'):
     97                self.name = dictin['name']
     98                del dictin['name']
     99            self.data = dictin
     100           
     101    def get_value(self, name):
     102        if self.has_key(name):
     103            return self[name]
     104        else:
     105            return None
     106               
     107    def __repr__(self):
     108        return repr(self.data)
     109
     110    def __str__(self):
     111        return str(self.data)
     112           
     113    def __getattr__(self, name):
     114        """
     115        override the class attribute get method. Return the value
     116        from the dictionary
     117        """
     118        if self.data.has_key(name):
     119            return self.data[name]
     120        else:
     121            return None
     122           
     123    def __setattr__(self, name, value):
     124        """
     125        override the class attribute set method only when the UserDict
     126        has set its class attribute
     127        """
     128        if self.__dict__.has_key('data'):
     129            self.data[name] = value
     130        else:
     131            self.__dict__[name] = value
     132
     133    def __iter__(self):
     134        return iter(self.data.keys())
    135135
    136136class TicketEmailParser(object):
    137         env = None
    138         comment = '> '
    139 
    140         def __init__(self, env, parameters, logger, version):
    141                 self.env = env
    142 
    143                 # Database connection
    144                 #
    145                 self.db = None
    146 
    147                 # Save parameters
    148                 #
    149                 self.parameters = parameters
    150                 self.logger = logger
    151 
    152                 # Some useful mail constants
    153                 #
    154                 self.email_name = None
    155                 self.email_addr = None
    156                 self.email_from = None
    157                 self.author     = None
    158                 self.id         = None
    159                
    160                 self.STRIP_CONTENT_TYPES = list()
    161 
    162                 ## fields properties via body_text
    163                 #
    164                 self.properties = dict()
    165 
    166                 self.VERSION = version
    167 
    168                 self.get_config = self.env.config.get
    169 
    170                 ## init function ##
    171                 #
    172                 self.setup_parameters()
    173 
    174         def setup_parameters(self):
    175                 if self.parameters.umask:
    176                         os.umask(self.parameters.umask)
    177 
    178                 if not self.parameters.spam_level:
    179                         self.parameters.spam_level = 0
    180 
    181                 if not self.parameters.spam_header:
    182                         self.parameters.spam_header = 'X-Spam-Score'
    183 
    184                 if not self.parameters.email_quote:
    185                         self.parameters.email_quote = '> '
    186 
    187                 if not self.parameters.ticket_update_by_subject_lookback:
    188                         self.parameters.ticket_update_by_subject_lookback = 30
    189 
    190                 if self.parameters.verbatim_format == None:
    191                         self.parameters.verbatim_format = 1
    192 
    193                 if self.parameters.reflow == None:
    194                         self.parameters.reflow = 1
    195 
    196                 if self.parameters.binhex:
    197                         self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
    198 
    199                 if self.parameters.applesingle:
    200                         self.STRIP_CONTENT_TYPES.append('application/applefile')
    201 
    202                 if self.parameters.appledouble:
    203                         self.STRIP_CONTENT_TYPES.append('application/applefile')
    204 
    205                 if self.parameters.strip_content_types:
    206                         items = self.parameters.strip_content_types.split(',')
    207                         for item in items:
    208                                 self.STRIP_CONTENT_TYPES.append(item.strip())
    209 
    210                 if self.parameters.tmpdir:
    211                         self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
    212                 else:
    213                         self.parameters.tmpdir = os.path.normcase('/tmp')
    214 
    215                 if self.parameters.email_triggers_workflow == None:
    216                         self.parameters.email_triggers_workflow = 1
    217 
    218                 if not self.parameters.subject_field_separator:
    219                         self.parameters.subject_field_separator = '&'
    220                 else:
    221                         self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
    222 
    223                 self.trac_smtp_from = self.get_config('notification', 'smtp_from')
    224                 self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
    225 
    226 
    227                 self.system = None
     137    env = None
     138    comment = '> '
     139
     140    def __init__(self, env, parameters, logger, version):
     141        self.env = env
     142
     143        # Database connection
     144        #
     145        self.db = None
     146
     147        # Save parameters
     148        #
     149        self.parameters = parameters
     150        self.logger = logger
     151
     152        # Some useful mail constants
     153        #
     154        self.email_name = None
     155        self.email_addr = None
     156        self.email_from = None
     157        self.author     = None
     158        self.id         = None
     159       
     160        self.STRIP_CONTENT_TYPES = list()
     161
     162        ## fields properties via body_text
     163        #
     164        self.properties = dict()
     165
     166        self.VERSION = version
     167
     168        self.get_config = self.env.config.get
     169
     170        ## init function ##
     171        #
     172        self.setup_parameters()
     173
     174    def setup_parameters(self):
     175        if self.parameters.umask:
     176            os.umask(self.parameters.umask)
     177
     178        if not self.parameters.spam_level:
     179            self.parameters.spam_level = 0
     180
     181        if not self.parameters.spam_header:
     182            self.parameters.spam_header = 'X-Spam-Score'
     183
     184        if not self.parameters.email_quote:
     185            self.parameters.email_quote = '> '
     186
     187        if not self.parameters.ticket_update_by_subject_lookback:
     188            self.parameters.ticket_update_by_subject_lookback = 30
     189
     190        if self.parameters.verbatim_format == None:
     191            self.parameters.verbatim_format = 1
     192
     193        if self.parameters.reflow == None:
     194            self.parameters.reflow = 1
     195
     196        if self.parameters.binhex:
     197            self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
     198
     199        if self.parameters.applesingle:
     200            self.STRIP_CONTENT_TYPES.append('application/applefile')
     201
     202        if self.parameters.appledouble:
     203            self.STRIP_CONTENT_TYPES.append('application/applefile')
     204
     205        if self.parameters.strip_content_types:
     206            items = self.parameters.strip_content_types.split(',')
     207            for item in items:
     208                self.STRIP_CONTENT_TYPES.append(item.strip())
     209
     210        if self.parameters.tmpdir:
     211            self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
     212        else:
     213            self.parameters.tmpdir = os.path.normcase('/tmp')
     214
     215        if self.parameters.email_triggers_workflow == None:
     216            self.parameters.email_triggers_workflow = 1
     217
     218        if not self.parameters.subject_field_separator:
     219            self.parameters.subject_field_separator = '&'
     220        else:
     221            self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
     222
     223        self.trac_smtp_from = self.get_config('notification', 'smtp_from')
     224        self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
     225
     226
     227        self.system = None
    228228
    229229########## Email Header Functions ###########################################################
    230230
    231         def spam(self, message):
    232                 """
    233                 # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
    234                 # Note if Spam_level then '*' are included
    235                 """
    236                 spam = False
    237                 if message.has_key(self.parameters.spam_header):
    238                         spam_l = string.split(message[self.parameters.spam_header])
    239 
    240                         try:
    241                                 number = spam_l[0].count('*')
    242                         except IndexError, detail:
    243                                 number = 0
    244                                
    245                         if number >= self.parameters.spam_level:
    246                                 spam = True
    247                                
    248                 # treat virus mails as spam
    249                 #
    250                 elif message.has_key('X-Virus-found'):                 
    251                         spam = True
    252 
    253                 # How to handle SPAM messages
    254                 #
    255                 if self.parameters.drop_spam and spam:
    256 
    257                         self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
    258                         return 'drop'   
    259 
    260                 elif spam:
    261 
    262                         return 'Spam'   
    263                 else:
    264 
    265                         return False
    266 
    267         def email_header_acl(self, keyword, header_field, default):
    268                 """
    269                 This function wil check if the email address is allowed or denied
    270                 to send mail to the ticket list
    271             """
    272                 self.logger.debug('function email_header_acl: %s' %keyword)
    273 
    274                 try:
    275                         mail_addresses = self.parameters[keyword]
    276 
    277                         # Check if we have an empty string
    278                         #
    279                         if not mail_addresses:
    280                                 return default
    281 
    282                 except KeyError, detail:
    283                         self.logger.debug('%s not defined, all messages are allowed.' %(keyword))
    284 
    285                         return default
    286 
    287                 mail_addresses = string.split(mail_addresses, ',')
    288 
    289                 for entry in mail_addresses:
    290                         entry = entry.strip()
    291                         TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
    292                         result =  TO_RE.search(header_field)
    293                         if result:
    294                                 return True
    295 
    296                 return False
    297 
    298         def email_header_txt(self, m):
    299                 """
    300                 Display To and CC addresses in description field
    301                 """
    302                 s = ''
    303 
    304                 if m['To'] and len(m['To']) > 0:
    305                         s = "'''To:''' %s\r\n" %(m['To'])
    306                 if m['Cc'] and len(m['Cc']) > 0:
    307                         s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
    308 
    309                 return  self.email_to_unicode(s)
    310 
    311 
    312         def get_sender_info(self, message):
    313                 """
    314                 Get the default author name and email address from the message
    315                 """
    316 
    317                 self.email_to = self.email_to_unicode(message['to'])
    318                 self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
    319 
    320                 self.email_from = self.email_to_unicode(message['from'])
    321                 self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
    322 
    323                 ## Trac can not handle author's name that contains spaces
    324                 #
    325                 if self.email_addr == self.trac_smtp_from:
    326                         if self.email_name:
    327                                 self.author = self.email_name
    328                         else:
    329                                 self.author = "email2trac"
    330                 else:
    331                         self.author = self.email_addr
    332 
    333                 if self.parameters.ignore_trac_user_settings:
    334                         return
    335 
    336                 # Is this a registered user, use email address as search key:
    337                 # result:
    338                 #   u : login name
    339                 #   n : Name that the user has set in the settings tab
    340                 #   e : email address that the user has set in the settings tab
    341                 #
    342                 users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
    343                         if (
    344                                 (e and (e.lower() == self.email_addr.lower())) or
    345                             (u + '@' + self.smtp_default_domain.lower() == self.email_addr.lower())
    346                         )
    347                         ]
    348 
    349                 if len(users) >= 1:
    350                         self.email_from = users[0][0]
    351                         self.author = users[0][0]
    352 
    353         def set_reply_fields(self, ticket, message):
    354                 """
    355                 Set all the right fields for a new ticket
    356                 """
    357                 self.logger.debug('function set_reply_fields')
    358 
    359                 ## Only use name or email adress
    360                 #ticket['reporter'] = self.email_from
    361                 ticket['reporter'] = self.author
    362 
    363 
    364                 # Put all CC-addresses in ticket CC field
    365                 #
    366                 if self.parameters.reply_all:
    367 
    368                         email_cc = ''
    369 
    370                         cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
    371 
    372                         if not cc_addrs:
    373                                 return
    374 
    375                         ## Build a list of forbidden CC addresses
    376                         #
    377                         #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
    378                         #to_list = list()
    379                         #for n,e in to_addrs:
    380                         #       to_list.append(e)
    381                                
    382                         # Always Remove reporter email address from cc-list
    383                         #
    384                         try:
    385                                 cc_addrs.remove((self.author, self.email_addr))
    386                         except ValueError, detail:
    387                                 pass
    388 
    389                         for name,addr in cc_addrs:
    390                
    391                                 ## Prevent mail loop
    392                                 #
    393                                 #if addr in to_list:
    394 
    395                                 if addr == self.trac_smtp_from:
    396                                         self.logger.debug("Skipping %s mail address for CC-field" %(addr))
    397                                         continue
    398 
    399                                 if email_cc:
    400                                         email_cc = '%s, %s' %(email_cc, addr)
    401                                 else:
    402                                         email_cc = addr
    403 
    404                         if email_cc:
    405                                 self.logger.debug('set_reply_fields: %s' %email_cc)
    406 
    407                                 ticket['cc'] = self.email_to_unicode(email_cc)
     231    def spam(self, message):
     232        """
     233        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
     234        # Note if Spam_level then '*' are included
     235        """
     236        spam = False
     237        if message.has_key(self.parameters.spam_header):
     238            spam_l = string.split(message[self.parameters.spam_header])
     239
     240            try:
     241                number = spam_l[0].count('*')
     242            except IndexError, detail:
     243                number = 0
     244               
     245            if number >= self.parameters.spam_level:
     246                spam = True
     247               
     248        # treat virus mails as spam
     249        #
     250        elif message.has_key('X-Virus-found'):         
     251            spam = True
     252
     253        # How to handle SPAM messages
     254        #
     255        if self.parameters.drop_spam and spam:
     256
     257            self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
     258            return 'drop'   
     259
     260        elif spam:
     261
     262            return 'Spam'   
     263        else:
     264
     265            return False
     266
     267    def email_header_acl(self, keyword, header_field, default):
     268        """
     269        This function wil check if the email address is allowed or denied
     270        to send mail to the ticket list
     271        """
     272        self.logger.debug('function email_header_acl: %s' %keyword)
     273
     274        try:
     275            mail_addresses = self.parameters[keyword]
     276
     277            # Check if we have an empty string
     278            #
     279            if not mail_addresses:
     280                return default
     281
     282        except KeyError, detail:
     283            self.logger.debug('%s not defined, all messages are allowed.' %(keyword))
     284
     285            return default
     286
     287        mail_addresses = string.split(mail_addresses, ',')
     288
     289        for entry in mail_addresses:
     290            entry = entry.strip()
     291            TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
     292            result =  TO_RE.search(header_field)
     293            if result:
     294                return True
     295
     296        return False
     297
     298    def email_header_txt(self, m):
     299        """
     300        Display To and CC addresses in description field
     301        """
     302        s = ''
     303
     304        if m['To'] and len(m['To']) > 0:
     305            s = "'''To:''' %s\r\n" %(m['To'])
     306        if m['Cc'] and len(m['Cc']) > 0:
     307            s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
     308
     309        return  self.email_to_unicode(s)
     310
     311
     312    def get_sender_info(self, message):
     313        """
     314        Get the default author name and email address from the message
     315        """
     316
     317        self.email_to = self.email_to_unicode(message['to'])
     318        self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
     319
     320        self.email_from = self.email_to_unicode(message['from'])
     321        self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
     322
     323        ## Trac can not handle author's name that contains spaces
     324        #
     325        if self.email_addr == self.trac_smtp_from:
     326            if self.email_name:
     327                self.author = self.email_name
     328            else:
     329                self.author = "email2trac"
     330        else:
     331            self.author = self.email_addr
     332
     333        if self.parameters.ignore_trac_user_settings:
     334            return
     335
     336        # Is this a registered user, use email address as search key:
     337        # result:
     338        #   u : login name
     339        #   n : Name that the user has set in the settings tab
     340        #   e : email address that the user has set in the settings tab
     341        #
     342        users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
     343            if (
     344                (e and (e.lower() == self.email_addr.lower())) or
     345                (u + '@' + self.smtp_default_domain.lower() == self.email_addr.lower())
     346            )
     347            ]
     348
     349        if len(users) >= 1:
     350            self.email_from = users[0][0]
     351            self.author = users[0][0]
     352
     353    def set_reply_fields(self, ticket, message):
     354        """
     355        Set all the right fields for a new ticket
     356        """
     357        self.logger.debug('function set_reply_fields')
     358
     359        ## Only use name or email adress
     360        #ticket['reporter'] = self.email_from
     361        ticket['reporter'] = self.author
     362
     363
     364        # Put all CC-addresses in ticket CC field
     365        #
     366        if self.parameters.reply_all:
     367
     368            email_cc = ''
     369
     370            cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
     371
     372            if not cc_addrs:
     373                return
     374
     375            ## Build a list of forbidden CC addresses
     376            #
     377            #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
     378            #to_list = list()
     379            #for n,e in to_addrs:
     380            #   to_list.append(e)
     381               
     382            # Always Remove reporter email address from cc-list
     383            #
     384            try:
     385                cc_addrs.remove((self.author, self.email_addr))
     386            except ValueError, detail:
     387                pass
     388
     389            for name,addr in cc_addrs:
     390       
     391                ## Prevent mail loop
     392                #
     393                #if addr in to_list:
     394
     395                if addr == self.trac_smtp_from:
     396                    self.logger.debug("Skipping %s mail address for CC-field" %(addr))
     397                    continue
     398
     399                if email_cc:
     400                    email_cc = '%s, %s' %(email_cc, addr)
     401                else:
     402                    email_cc = addr
     403
     404            if email_cc:
     405                self.logger.debug('set_reply_fields: %s' %email_cc)
     406
     407                ticket['cc'] = self.email_to_unicode(email_cc)
    408408
    409409
    410410########## DEBUG functions  ###########################################################
    411411
    412         def debug_body(self, message_body, temporary_file=False):
    413                 if temporary_file:
    414                         body_file = tempfile.mktemp('.email2trac')
    415                 else:
    416                         body_file = os.path.join(self.parameters.tmpdir, 'body.txt')
    417 
    418                 if self.parameters.dry_run:
    419                         print 'DRY-RUN: not saving body to %s' %(body_file)
    420                         return
    421 
    422                 print 'writing body to %s' %(body_file)
    423                 fx = open(body_file, 'wb')
    424                 if not message_body:
    425                                 message_body = '(None)'
    426 
    427                 message_body = message_body.encode('utf-8')
    428                 #message_body = unicode(message_body, 'iso-8859-15')
    429 
    430                 fx.write(message_body)
    431                 fx.close()
    432                 try:
    433                         os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
    434                 except OSError:
    435                         pass
    436 
    437         def debug_attachments(self, message_parts):
    438                 """
    439                 """
    440                 self.logger.debug('function debug_attachments')
    441                
    442                 n = 0
    443                 for item in message_parts:
    444                         # Skip inline text parts
    445                         if not isinstance(item, tuple):
    446                                 continue
    447                                
    448                         (original, filename, part) = item
    449 
    450                         n = n + 1
    451                         print 'part%d: Content-Type: %s' % (n, part.get_content_type())
    452                
    453                         s = 'part%d: filename: %s' %(n, filename)
    454                         self.print_unicode(s)
    455        
    456                         ## Forbidden chars
    457                         #
    458                         filename = filename.replace('\\', '_')
    459                         filename = filename.replace('/', '_')
    460        
    461 
    462                         part_file = os.path.join(self.parameters.tmpdir, filename)
    463                         s = 'writing part%d (%s)' % (n,part_file)
    464                         self.print_unicode(s)
    465 
    466                         if self.parameters.dry_run:
    467                                 print 'DRY_RUN: NOT saving attachments'
    468                                 continue
    469 
    470                         part_file = util.text.unicode_quote(part_file)
    471 
    472                         fx = open(part_file, 'wb')
    473                         text = part.get_payload(decode=1)
    474 
    475                         if not text:
    476                                 text = '(None)'
    477 
    478                         fx.write(text)
    479                         fx.close()
    480 
    481                         try:
    482                                 os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
    483                         except OSError:
    484                                 pass
    485 
    486         def save_email_for_debug(self, message, create_tempfile=False):
    487 
    488                 if create_tempfile:
    489                         msg_file = tempfile.mktemp('.email2trac')
    490                 else:
    491                         #msg_file = '/var/tmp/msg.txt'
    492                         msg_file = os.path.join(self.parameters.tmpdir, 'msg.txt')
    493 
    494                 if self.parameters.dry_run:
    495                         print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
    496                 else:
    497                         print 'saving email to %s' %(msg_file)
    498 
    499                         fx = open(msg_file, 'wb')
    500                         fx.write('%s' % message)
    501                         fx.close()
    502                        
    503                         try:
    504                                 os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
    505                         except OSError:
    506                                 pass
    507 
    508                 message_parts = self.get_message_parts(message)
    509                 message_parts = self.unique_attachment_names(message_parts)
    510                 body_text = self.get_body_text(message_parts)
    511                 self.debug_body(body_text, True)
    512                 self.debug_attachments(message_parts)
     412    def debug_body(self, message_body, temporary_file=False):
     413        if temporary_file:
     414            body_file = tempfile.mktemp('.email2trac')
     415        else:
     416            body_file = os.path.join(self.parameters.tmpdir, 'body.txt')
     417
     418        if self.parameters.dry_run:
     419            print 'DRY-RUN: not saving body to %s' %(body_file)
     420            return
     421
     422        print 'writing body to %s' %(body_file)
     423        fx = open(body_file, 'wb')
     424        if not message_body:
     425                message_body = '(None)'
     426
     427        message_body = message_body.encode('utf-8')
     428        #message_body = unicode(message_body, 'iso-8859-15')
     429
     430        fx.write(message_body)
     431        fx.close()
     432        try:
     433            os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
     434        except OSError:
     435            pass
     436
     437    def debug_attachments(self, message_parts):
     438        """
     439        """
     440        self.logger.debug('function debug_attachments')
     441       
     442        n = 0
     443        for item in message_parts:
     444            # Skip inline text parts
     445            if not isinstance(item, tuple):
     446                continue
     447               
     448            (original, filename, part) = item
     449
     450            n = n + 1
     451            print 'part%d: Content-Type: %s' % (n, part.get_content_type())
     452       
     453            s = 'part%d: filename: %s' %(n, filename)
     454            self.print_unicode(s)
     455   
     456            ## Forbidden chars
     457            #
     458            filename = filename.replace('\\', '_')
     459            filename = filename.replace('/', '_')
     460   
     461
     462            part_file = os.path.join(self.parameters.tmpdir, filename)
     463            s = 'writing part%d (%s)' % (n,part_file)
     464            self.print_unicode(s)
     465
     466            if self.parameters.dry_run:
     467                print 'DRY_RUN: NOT saving attachments'
     468                continue
     469
     470            part_file = util.text.unicode_quote(part_file)
     471
     472            fx = open(part_file, 'wb')
     473            text = part.get_payload(decode=1)
     474
     475            if not text:
     476                text = '(None)'
     477
     478            fx.write(text)
     479            fx.close()
     480
     481            try:
     482                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
     483            except OSError:
     484                pass
     485
     486    def save_email_for_debug(self, message, create_tempfile=False):
     487
     488        if create_tempfile:
     489            msg_file = tempfile.mktemp('.email2trac')
     490        else:
     491            #msg_file = '/var/tmp/msg.txt'
     492            msg_file = os.path.join(self.parameters.tmpdir, 'msg.txt')
     493
     494        if self.parameters.dry_run:
     495            print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
     496        else:
     497            print 'saving email to %s' %(msg_file)
     498
     499            fx = open(msg_file, 'wb')
     500            fx.write('%s' % message)
     501            fx.close()
     502           
     503            try:
     504                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
     505            except OSError:
     506                pass
     507
     508        message_parts = self.get_message_parts(message)
     509        message_parts = self.unique_attachment_names(message_parts)
     510        body_text = self.get_body_text(message_parts)
     511        self.debug_body(body_text, True)
     512        self.debug_attachments(message_parts)
    513513
    514514########## Conversion functions  ###########################################################
    515515
    516         def email_to_unicode(self, message_str):
    517                 """
    518                 Email has 7 bit ASCII code, convert it to unicode with the charset
    519                 that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
    520                 understands it.
    521                 """
    522                 self.logger.debug("function email_to_unicode")
    523 
    524                 results =  email.Header.decode_header(message_str)
    525 
    526                 s = None
    527                 for text,format in results:
    528                         if format:
    529                                 try:
    530                                         temp = unicode(text, format)
    531                                 except UnicodeError, detail:
    532                                         # This always works
    533                                         #
    534                                         temp = unicode(text, 'iso-8859-15')
    535                                 except LookupError, detail:
    536                                         #text = 'ERROR: Could not find charset: %s, please install' %format
    537                                         #temp = unicode(text, 'iso-8859-15')
    538                                         temp = message_str
    539                                        
    540                         else:
    541                                 temp = string.strip(text)
    542                                 temp = unicode(text, 'iso-8859-15')
    543 
    544                         if s:
    545                                 s = '%s %s' %(s, temp)
    546                         else:
    547                                 s = '%s' %temp
    548 
    549                 #s = s.encode('utf-8')
    550                 return s
    551 
    552         def str_to_dict(self, s):
    553                 """
    554                 Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
    555                 """
    556                 self.logger.debug("function str_to_dict")
    557 
    558                 fields = string.split(s, self.parameters.subject_field_separator)
    559 
    560                 result = dict()
    561                 for field in fields:
    562                         try:
    563                                 index, value = string.split(field, '=')
    564 
    565                                 # We can not change the description of a ticket via the subject
    566                                 # line. The description is the body of the email
    567                                 #
    568                                 if index.lower() in ['description']:
    569                                         continue
    570 
    571                                 if value:
    572                                         result[index.lower()] = value
    573 
    574                         except ValueError:
    575                                 pass
    576                 return result
    577 
    578         def print_unicode(self,s):
    579                 """
    580                 This function prints unicode strings if possible else it will quote it
    581                 """
    582                 try:
    583                         self.logger.debug(s)
    584                 except UnicodeEncodeError, detail:
    585                         self.logger.debug(util.text.unicode_quote(s))
    586 
    587 
    588         def html_2_txt(self, data):
    589                 """
    590                 Various routines to convert html syntax to valid trac wiki syntax
    591                 """
    592                 self.logger.debug('function html_2_txt')
    593 
    594                 ## This routine make an safe html that can be include
    595                 #  in trac, but no further text processing can be done
    596                 #
    597 #               try:
    598 #                       from lxml.html.clean import Cleaner
    599 #                       tags_rm = list()
    600 #                       tags_rm.append('body')
     516    def email_to_unicode(self, message_str):
     517        """
     518        Email has 7 bit ASCII code, convert it to unicode with the charset
     519        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
     520        understands it.
     521        """
     522        self.logger.debug("function email_to_unicode")
     523
     524        results =  email.Header.decode_header(message_str)
     525
     526        s = None
     527        for text,format in results:
     528            if format:
     529                try:
     530                    temp = unicode(text, format)
     531                except UnicodeError, detail:
     532                    # This always works
     533                    #
     534                    temp = unicode(text, 'iso-8859-15')
     535                except LookupError, detail:
     536                    #text = 'ERROR: Could not find charset: %s, please install' %format
     537                    #temp = unicode(text, 'iso-8859-15')
     538                    temp = message_str
     539                       
     540            else:
     541                temp = string.strip(text)
     542                temp = unicode(text, 'iso-8859-15')
     543
     544            if s:
     545                s = '%s %s' %(s, temp)
     546            else:
     547                s = '%s' %temp
     548
     549        #s = s.encode('utf-8')
     550        return s
     551
     552    def str_to_dict(self, s):
     553        """
     554        Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
     555        """
     556        self.logger.debug("function str_to_dict")
     557
     558        fields = string.split(s, self.parameters.subject_field_separator)
     559
     560        result = dict()
     561        for field in fields:
     562            try:
     563                index, value = string.split(field, '=')
     564
     565                # We can not change the description of a ticket via the subject
     566                # line. The description is the body of the email
     567                #
     568                if index.lower() in ['description']:
     569                    continue
     570
     571                if value:
     572                    result[index.lower()] = value
     573
     574            except ValueError:
     575                pass
     576        return result
     577
     578    def print_unicode(self,s):
     579        """
     580        This function prints unicode strings if possible else it will quote it
     581        """
     582        try:
     583            self.logger.debug(s)
     584        except UnicodeEncodeError, detail:
     585            self.logger.debug(util.text.unicode_quote(s))
     586
     587
     588    def html_2_txt(self, data):
     589        """
     590        Various routines to convert html syntax to valid trac wiki syntax
     591        """
     592        self.logger.debug('function html_2_txt')
     593
     594        ## This routine make an safe html that can be include
     595        #  in trac, but no further text processing can be done
     596        #
     597#       try:
     598#           from lxml.html.clean import Cleaner
     599#           tags_rm = list()
     600#           tags_rm.append('body')
    601601#
    602 #                       cleaner = Cleaner(remove_tags=tags_rm )
    603 #                       parsed_data = cleaner.clean_html(data)
    604 #                       parsed_data = '\n{{{\n#!html\n' + parsed_data + '\n}}}\n'
     602#           cleaner = Cleaner(remove_tags=tags_rm )
     603#           parsed_data = cleaner.clean_html(data)
     604#           parsed_data = '\n{{{\n#!html\n' + parsed_data + '\n}}}\n'
    605605#
    606 #                       return parsed_data
    607 #                       
    608 #               except ImportError::
    609 #                       pass
    610 
    611                 parsed_data = None
    612                 if self.parameters.html2text_cmd:
    613                         tmp_file = tempfile.mktemp('email2trac.html')
    614                         cmd = '%s %s' %(self.parameters.html2text_cmd, tmp_file)
    615                         self.logger.debug('\t html2text conversion %s'%(cmd))
    616        
    617                         if self.parameters.dry_run:
    618                                 print 'DRY_RUN: html2text conversion command: %s\n' %(cmd)
    619 
    620                         else:
    621                                 f = open(tmp_file, "w+")
    622                                 f.write(data)
    623                                 f.close()
    624 
    625                                 lines = os.popen(cmd).readlines()
    626                                 parsed_data =  ''.join(lines)
    627 
    628                                 os.unlink(tmp_file)
    629 
    630                 else:
    631                         self.logger.debug('\t No html2text conversion tool specified in email2trac.conf')
    632 
    633                 return parsed_data
     606#           return parsed_data
     607#           
     608#       except ImportError::
     609#           pass
     610
     611        parsed_data = None
     612        if self.parameters.html2text_cmd:
     613            tmp_file = tempfile.mktemp('email2trac.html')
     614            cmd = '%s %s' %(self.parameters.html2text_cmd, tmp_file)
     615            self.logger.debug('\t html2text conversion %s'%(cmd))
     616   
     617            if self.parameters.dry_run:
     618                print 'DRY_RUN: html2text conversion command: %s\n' %(cmd)
     619
     620            else:
     621                f = open(tmp_file, "w+")
     622                f.write(data)
     623                f.close()
     624
     625                lines = os.popen(cmd).readlines()
     626                parsed_data =  ''.join(lines)
     627
     628                os.unlink(tmp_file)
     629
     630        else:
     631            self.logger.debug('\t No html2text conversion tool specified in email2trac.conf')
     632
     633        return parsed_data
    634634
    635635########## TRAC ticket functions  ###########################################################
    636636
    637         def check_permission_participants(self, tkt, action):
    638                 """
    639                 Check if the mailer is allowed to update the ticket
    640                 """
    641                 self.logger.debug('function check_permission_participants')
    642 
    643                 if tkt['reporter'].lower() in [self.author, self.email_addr]:
    644                         self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
    645 
    646                         return True
    647 
    648                 perm = PermissionSystem(self.env)
    649                 if perm.check_permission(action, self.author):
    650                         self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
    651 
    652                         return True
    653                
    654                 # Is the updater in the CC?
    655                 try:
    656                         cc_list = tkt['cc'].split(',')
    657                         for cc in cc_list:
    658                                 if self.email_addr.lower() in cc.strip():
    659                                         self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
    660 
    661                                         return True
    662 
    663                 except KeyError:
    664                         pass
    665 
    666                 return False
    667 
    668         def check_permission(self, tkt, action):
    669                 """
    670                 check if the reporter has the right permission for the action:
     637    def check_permission_participants(self, tkt, action):
     638        """
     639        Check if the mailer is allowed to update the ticket
     640        """
     641        self.logger.debug('function check_permission_participants')
     642
     643        if tkt['reporter'].lower() in [self.author, self.email_addr]:
     644            self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
     645
     646            return True
     647
     648        perm = PermissionSystem(self.env)
     649        if perm.check_permission(action, self.author):
     650            self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
     651
     652            return True
     653       
     654        # Is the updater in the CC?
     655        try:
     656            cc_list = tkt['cc'].split(',')
     657            for cc in cc_list:
     658                if self.email_addr.lower() in cc.strip():
     659                    self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
     660
     661                    return True
     662
     663        except KeyError:
     664            pass
     665
     666        return False
     667
     668    def check_permission(self, tkt, action):
     669        """
     670        check if the reporter has the right permission for the action:
    671671          - TICKET_CREATE
    672672          - TICKET_MODIFY
     
    674674          - TICKET_CHGPROP
    675675
    676                 There are three models:
    677                         - None      : no checking at all
    678                         - trac      : check the permission via trac permission model
    679                         - email2trac: ....
    680                 """
    681                 self.logger.debug("function check_permission")
    682 
    683                 if self.parameters.ticket_permission_system in ['trac']:
    684 
    685                         perm = PermissionSystem(self.env)
    686                         if perm.check_permission(action, self.author):
    687                                 return True
    688                         else:
    689                                 return False
    690 
    691                 elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
    692                         return (self.check_permission_participants(tkt, action))       
    693 
    694                 ## Default is to allow everybody ticket updates and ticket creation
    695                 #
    696                 else:
    697                                 return True
    698 
    699 
    700         def update_ticket_fields(self, ticket, user_dict, new=None):
    701                 """
    702                 This will update the ticket fields. It will check if the
    703                 given fields are known and if the right values are specified
    704                 It will only update the ticket field value:
    705                         - If the field is known
    706                         - If the value supplied is valid for the ticket field.
    707                           If not then there are two options:
    708                            1) Skip the value (new=None)
    709                            2) Set default value for field (new=1)
    710                 """
    711                 self.logger.debug("function update_ticket_fields")
    712 
    713                 ## Check only permission model on ticket updates
    714                 #
    715                 if not new:
    716                         if self.parameters.ticket_permission_system:
    717                                 if not self.check_permission(ticket, 'TICKET_CHGPROP'):
    718                                         self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
    719                                         return False
    720 
    721                 ## Build a system dictionary from the ticket fields
    722                 #  with field as index and option as value
    723                 #
    724                 sys_dict = dict()
    725                 for field in ticket.fields:
    726                         try:
    727                                 sys_dict[field['name']] = field['options']
    728 
    729                         except KeyError:
    730                                 #sys_dict[field['name']] = None
    731                                 pass
    732 
    733                 ## Check user supplied fields an compare them with the
    734                 #  system one's
    735                 #
    736                 for field,value in user_dict.items():
    737                         if self.parameters.debug:
    738                                 s = 'user_field\t %s = %s' %(field,value)
    739                                 self.print_unicode(s)
    740 
    741                         ## To prevent mail loop
    742                         #
    743                         if field == 'cc':
    744 
    745                                 cc_list = user_dict['cc'].split(',')
    746 
    747                                 if self.trac_smtp_from in cc_list:
    748                                         self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
    749 
    750                                         cc_list.remove(self.trac_smtp_from)
    751 
    752                                 value = ','.join(cc_list)
    753                                
    754 
    755                         ## Check if every value is allowed for this field
    756                         #
    757                         if sys_dict.has_key(field):
    758 
    759                                 if value in sys_dict[field]:
    760                                         ticket[field] = value
    761                                 else:
    762                                         ## Must we set a default if value is not allowed
    763                                         #
    764                                         if new:
    765                                                 value = self.get_config('ticket', 'default_%s' %(field) )
    766 
    767                         else:
    768                                 ## Only set if we have a value
    769                                 #
    770                                 #if value:
    771                                 #       ticket[field] = value
    772                                 ticket[field] = value
    773 
    774                         if self.parameters.debug:
    775                                 s = 'ticket_field\t %s = %s' %(field,  ticket[field])
    776                                 self.print_unicode(s)
    777 
    778         def ticket_update(self, m, id, spam):
    779                 """
    780                 If the current email is a reply to an existing ticket, this function
    781                 will append the contents of this email to that ticket, instead of
    782                 creating a new one.
    783                 """
    784                 self.logger.debug("function ticket_update")
    785 
    786                 if not self.parameters.ticket_update:
    787                         self.logger.debug("ticket_update disabled")
    788                         return False
    789 
    790                 ## Must we update ticket fields
    791                 #
    792                 update_fields = dict()
    793                 try:
    794                         id, keywords = string.split(id, '?')
    795 
    796                         update_fields = self.str_to_dict(keywords)
    797 
    798                         ## Strip '#'
    799                         #
    800                         self.id = int(id[1:])
    801 
    802                 except ValueError:
    803 
    804                         ## Strip '#'
    805                         #
    806                         self.id = int(id[1:])
    807 
    808                 self.logger.debug("ticket_update id %s" %id)
    809 
    810                 ## When is the change committed
    811                 #
    812                 if self.VERSION < 0.11:
    813                         when = int(time.time())
    814                 else:
    815                         when = datetime.now(util.datefmt.utc)
    816 
    817                 try:
    818                         tkt = Ticket(self.env, self.id, self.db)
    819 
    820                 except util.TracError, detail:
    821 
    822                         ## Not a valid ticket
    823                         #
    824                         self.id = None
    825                         return False
    826 
    827                 ## Check the permission of the reporter
    828                 #
    829                 if self.parameters.ticket_permission_system:
    830                         if not self.check_permission(tkt, 'TICKET_APPEND'):
    831                                 self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
    832                                 return False
    833 
    834                 ## How many changes has this ticket
    835                 #
    836                 # cnum = len(tkt.get_changelog())
    837                 grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
    838                 cnum = sum(1 for e in grouped) + 1
    839 
    840 
    841                 ## reopen the ticket if it is was closed
    842                 #  We must use the ticket workflow framework
    843                 #
    844                 if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
    845 
    846                         self.logger.debug('Workflow ticket update fields: ')
    847 
    848                         from trac.ticket.default_workflow import ConfigurableTicketWorkflow
    849                         from trac.test import Mock, MockPerm
    850 
    851                         req = Mock(authname=self.author, perm=MockPerm(), args={})
    852                         try:
    853                                 workflow = self.parameters['workflow_%s' %tkt['status']]
    854                         except KeyError:
    855                                 ## fallback for compability (Will be deprecated)
    856                                 #
    857                                 if tkt['status'] in ['closed']:
    858                                         workflow = self.parameters.workflow
    859                                 else:   
    860                                         workflow = None
    861 
    862                         controller = ConfigurableTicketWorkflow(self.env)
    863                         #print controller.actions
    864                         #print controller.actions.keys()
    865                         #print controller.get_ticket_actions(req, tkt)
    866                         #print controller.actions[workflow]
    867                         #print controller.actions[workflow]['permissions'] is a list
    868 
    869                         if workflow:
    870 
    871                                 if self.parameters.ticket_permission_system:
    872 
    873                                         if self.check_permission(tkt, controller.actions[workflow]['permissions'][0]):
    874                                                 fields = controller.get_ticket_changes(req, tkt, workflow)
    875                                         else:
    876                                                 fields = dict()
    877                                                 self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author)
    878 
    879                                 else:
    880                                         fields = controller.get_ticket_changes(req, tkt, workflow)
    881 
    882                                 for key in fields.keys():
    883                                         self.logger.debug('\t %s : %s' %(key, fields[key]))
    884                                         tkt[key] = fields[key]
    885 
    886                 ## Old pre 0.11 situation
    887                 #
    888                 elif self.parameters.email_triggers_workflow:
    889 
    890                         self.logger.debug('email triggers workflow pre trac 0.11')
    891 
    892                         if tkt['status'] in ['closed']:
    893                                 tkt['status'] = 'reopened'
    894                                 tkt['resolution'] = ''
    895 
    896                 else:
    897                         self.logger.debug('email triggers workflow disabled')
    898 
    899                 ## Must we update some ticket fields properties via subject line
    900                 #
    901                 if update_fields:
    902                         self.update_ticket_fields(tkt, update_fields)
    903 
    904                 message_parts = self.get_message_parts(m)
    905                 message_parts = self.unique_attachment_names(message_parts)
    906 
    907                 ## Must we update some ticket fields properties via body_text
    908                 #
    909                 if self.properties:
    910                                 self.update_ticket_fields(tkt, self.properties)
    911 
    912                 if self.parameters.email_header:
    913                         message_parts.insert(0, self.email_header_txt(m))
    914 
    915                 body_text = self.get_body_text(message_parts)
    916 
    917                 error_with_attachments = self.attach_attachments(message_parts)
    918 
    919                 if body_text.strip() or update_fields or self.properties:
    920                         if self.parameters.dry_run:
    921                                 print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
    922                         else:
    923                                 if error_with_attachments:
    924                                         body_text = '%s\\%s' %(error_with_attachments, body_text)
    925                                 self.logger.debug('tkt.save_changes(%s, %d)' %(self.author, cnum))
    926                                 tkt.save_changes(self.author, body_text, when, None, str(cnum))
    927                        
    928 
    929                 if not spam:
    930                         self.notify(tkt, False, when)
    931 
    932                 return True
    933 
    934         def set_ticket_fields(self, ticket):
    935                 """
    936                 set the ticket fields to value specified
    937                         - /etc/email2trac.conf with <prefix>_<field>
    938                         - trac default values, trac.ini
    939                 """
    940                 self.logger.debug('function set_ticket_fields')
    941 
    942                 user_dict = dict()
    943 
    944                 for field in ticket.fields:
    945 
    946                         name = field['name']
    947 
    948                         ## default trac value
    949                         #
    950                         if not field.get('custom'):
    951                                 value = self.get_config('ticket', 'default_%s' %(name) )
    952                                 if (name in ['resolution']) and (value in ['fixed']):
    953                                         value = None
    954                         else:
    955                                 ##  Else get the default value for reporter
    956                                 #
    957                                 value = field.get('value')
    958                                 options = field.get('options')
    959 
    960                                 if value and options and (value not in options):
    961                                         value = options[int(value)]
    962        
    963                         if self.parameters.debug:
    964                                 s = 'trac[%s] = %s' %(name, value)
    965                                 self.print_unicode(s)
    966 
    967                         ## email2trac.conf settings
    968                         #
    969                         prefix = self.parameters.ticket_prefix
    970                         try:
    971                                 value = self.parameters['%s_%s' %(prefix, name)]
    972                                 if self.parameters.debug:
    973                                         s = 'email2trac[%s] = %s ' %(name, value)
    974                                         self.print_unicode(s)
    975 
    976                         except KeyError, detail:
    977                                 pass
    978                
    979                         if value:
    980                                 user_dict[name] = value
    981                                 if self.parameters.debug:
    982                                         s = 'used %s = %s' %(name, value)
    983                                         self.print_unicode(s)
    984 
    985                 self.update_ticket_fields(ticket, user_dict, new=1)
    986 
    987                 if 'status' not in user_dict.keys():
    988                         ticket['status'] = 'new'
    989 
    990 
    991         def ticket_update_by_subject(self, subject):
    992                 """
    993                 This list of Re: prefixes is probably incomplete. Taken from
    994                 wikipedia. Here is how the subject is matched
    995                   - Re: <subject>
    996                   - Re: (<Mail list label>:)+ <subject>
    997 
    998                 So we must have the last column
    999                 """
    1000                 self.logger.debug('function ticket_update_by_subject')
    1001 
    1002                 matched_id = None
    1003                 if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
    1004                                
    1005                         SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
    1006                         result = SUBJECT_RE.search(subject)
    1007 
    1008                         if result:
    1009                                 ## This is a reply
    1010                                 #
    1011                                 orig_subject = result.group(1)
    1012 
    1013                                 self.logger.debug('subject search string: %s' %(orig_subject))
    1014 
    1015                                 cursor = self.db.cursor()
    1016                                 summaries = [orig_subject, '%%: %s' % orig_subject]
    1017 
    1018                                 ## Convert days to seconds
    1019                                 #
    1020                                 lookback = int(time.mktime(time.gmtime())) - \
    1021                                                 self.parameters.ticket_update_by_subject_lookback * 24 * 3600
    1022 
    1023 
    1024                                 for summary in summaries:
    1025                                         self.logger.debug('Looking for summary matching: "%s"' % summary)
    1026 
    1027                                         sql = """SELECT id FROM ticket
    1028                                                         WHERE changetime >= %s AND summary LIKE %s
    1029                                                         ORDER BY changetime DESC"""
    1030                                         cursor.execute(sql, [lookback, summary.strip()])
    1031 
    1032                                         for row in cursor:
    1033                                                 (matched_id,) = row
    1034 
    1035                                                 self.logger.debug('Found matching ticket id: %d' % matched_id)
    1036 
    1037                                                 break
    1038 
    1039                                         if matched_id:
    1040                                                 matched_id = '#%d' % matched_id
    1041                                                 return (matched_id, orig_subject)
    1042                                        
    1043                                         subject = orig_subject
    1044 
    1045                 return (matched_id, subject)
    1046 
    1047 
    1048         def new_ticket(self, msg, subject, spam, set_fields = None):
    1049                 """
    1050                 Create a new ticket
    1051                 """
    1052                 self.logger.debug('function new_ticket')
    1053 
    1054                 tkt = Ticket(self.env)
    1055 
    1056                 self.set_reply_fields(tkt, msg)
    1057 
    1058                 self.set_ticket_fields(tkt)
    1059 
    1060                 ## Check the permission of the reporter
    1061                 #
    1062                 if self.parameters.ticket_permission_system:
    1063                         if not self.check_permission(tkt, 'TICKET_CREATE'):
    1064                                 self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
    1065                                 return False
    1066 
    1067                 ## Old style setting for component, will be removed
    1068                 #
    1069                 if spam:
    1070                         tkt['component'] = 'Spam'
    1071 
    1072                 elif self.parameters.has_key('component'):
    1073                         tkt['component'] = self.parameters['component']
    1074 
    1075                 if not msg['Subject']:
    1076                         tkt['summary'] = u'(No subject)'
    1077                 else:
    1078                         tkt['summary'] = subject
    1079 
    1080 
    1081                 if set_fields:
    1082                         rest, keywords = string.split(set_fields, '?')
    1083 
    1084                         if keywords:
    1085                                 update_fields = self.str_to_dict(keywords)
    1086                                 self.update_ticket_fields(tkt, update_fields)
    1087 
    1088 
    1089                 message_parts = self.get_message_parts(msg)
    1090 
    1091                 ## Must we update some ticket fields properties via body_text
    1092                 #
    1093                 if self.properties:
    1094                                 self.update_ticket_fields(tkt, self.properties)
    1095 
    1096                 message_parts = self.unique_attachment_names(message_parts)
    1097                
    1098                 ## produce e-mail like header
    1099                 #
    1100                 head = ''
    1101                 if self.parameters.email_header:
    1102                         head = self.email_header_txt(msg)
    1103                         message_parts.insert(0, head)
    1104                        
    1105                 body_text = self.get_body_text(message_parts)
    1106 
    1107                 tkt['description'] = body_text
    1108 
    1109                 ## When is the change committed
    1110                 #
    1111                 if self.VERSION < 0.11:
    1112                         when = int(time.time())
    1113                 else:
    1114                         when = datetime.now(util.datefmt.utc)
    1115 
    1116                 if self.parameters.dry_run:
    1117                         print 'DRY_RUN: tkt.insert()'
    1118                 else:
    1119                         self.id = tkt.insert()
    1120        
    1121                 changed = False
    1122                 comment = ''
    1123 
    1124                 ## some routines in trac are dependend on ticket id     
    1125                 #  like alternate notify template
    1126                 #
    1127                 if self.parameters.alternate_notify_template:
    1128                         tkt['id'] = self.id
    1129                         changed = True
    1130 
    1131                 ## Rewrite the description if we have mailto enabled
    1132                 #
    1133                 if self.parameters.mailto_link:
    1134                         changed = True
    1135                         comment = u'\nadded mailto line\n'
    1136                         mailto = self.html_mailto_link( m['Subject'])
    1137 
    1138                         tkt['description'] = u'%s\r\n%s%s\r\n' \
    1139                                 %(head, mailto, body_text)
    1140        
    1141                 ## Save the attachments to the ticket   
    1142                 #
    1143                 error_with_attachments =  self.attach_attachments(message_parts)
    1144 
    1145                 if error_with_attachments:
    1146                         changed = True
    1147                         comment = '%s\n%s\n' %(comment, error_with_attachments)
    1148 
    1149                 if changed:
    1150                         if self.parameters.dry_run:
    1151                                 print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
    1152                         else:
    1153                                 tkt.save_changes(tkt['reporter'], comment)
    1154                                 #print tkt.get_changelog(self.db, when)
    1155 
    1156                 if not spam:
    1157                         self.notify(tkt, True)
    1158 
    1159 
    1160         def attach_attachments(self, message_parts, update=False):
    1161                 '''
    1162                 save any attachments as files in the ticket's directory
    1163                 '''
    1164                 self.logger.debug('function attach_attachments()')
    1165 
    1166                 if self.parameters.dry_run:
    1167                         print "DRY_RUN: no attachments attached to tickets"
    1168                         return ''
    1169 
    1170                 count = 0
    1171 
    1172                 ## Get Maxium attachment size
    1173                 #
    1174                 max_size = int(self.get_config('attachment', 'max_size'))
    1175                 status   = None
    1176                
    1177                 for item in message_parts:
    1178                         ## Skip body parts
    1179                         #
    1180                         if not isinstance(item, tuple):
    1181                                 continue
    1182                                
    1183                         (original, filename, part) = item
    1184 
    1185                         ## We have to determine the size so we use this temporary solution. we must escape it
    1186                         #  else we get UnicodeErrors.
    1187                         #
    1188                         path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, util.text.unicode_quote(filename)))
    1189                         text = part.get_payload(decode=1)
    1190                         if not text:
    1191                                 text = '(None)'
    1192                         fd.write(text)
    1193                         fd.close()
    1194 
    1195                         ## get the file_size
    1196                         #
    1197                         stats = os.lstat(path)
    1198                         file_size = stats[ST_SIZE]
    1199 
    1200                         ## Check if the attachment size is allowed
    1201                         #
    1202                         if (max_size != -1) and (file_size > max_size):
    1203                                 status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
    1204                                         %(status, original, file_size, max_size)
    1205 
    1206                                 os.unlink(path)
    1207                                 continue
    1208                         else:
    1209                                 count = count + 1
    1210                                        
    1211                         ## Insert the attachment
    1212                         #
    1213                         fd = open(path, 'rb')
    1214                         if self.system == 'discussion':
    1215                                 att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
    1216                                   % (self.id,))
    1217                         else:
    1218                                 self.logger.debug('Attach %s to ticket %d' %(util.text.unicode_quote(filename), self.id))
    1219                                 att = attachment.Attachment(self.env, 'ticket', self.id)
     676        There are three models:
     677            - None      : no checking at all
     678            - trac      : check the permission via trac permission model
     679            - email2trac: ....
     680        """
     681        self.logger.debug("function check_permission")
     682
     683        if self.parameters.ticket_permission_system in ['trac']:
     684
     685            perm = PermissionSystem(self.env)
     686            if perm.check_permission(action, self.author):
     687                return True
     688            else:
     689                return False
     690
     691        elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
     692            return (self.check_permission_participants(tkt, action))   
     693
     694        ## Default is to allow everybody ticket updates and ticket creation
     695        #
     696        else:
     697                return True
     698
     699
     700    def update_ticket_fields(self, ticket, user_dict, new=None):
     701        """
     702        This will update the ticket fields. It will check if the
     703        given fields are known and if the right values are specified
     704        It will only update the ticket field value:
     705            - If the field is known
     706            - If the value supplied is valid for the ticket field.
     707              If not then there are two options:
     708               1) Skip the value (new=None)
     709               2) Set default value for field (new=1)
     710        """
     711        self.logger.debug("function update_ticket_fields")
     712
     713        ## Check only permission model on ticket updates
     714        #
     715        if not new:
     716            if self.parameters.ticket_permission_system:
     717                if not self.check_permission(ticket, 'TICKET_CHGPROP'):
     718                    self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
     719                    return False
     720
     721        ## Build a system dictionary from the ticket fields
     722        #  with field as index and option as value
     723        #
     724        sys_dict = dict()
     725        for field in ticket.fields:
     726            try:
     727                sys_dict[field['name']] = field['options']
     728
     729            except KeyError:
     730                #sys_dict[field['name']] = None
     731                pass
     732
     733        ## Check user supplied fields an compare them with the
     734        #  system one's
     735        #
     736        for field,value in user_dict.items():
     737            if self.parameters.debug:
     738                s = 'user_field\t %s = %s' %(field,value)
     739                self.print_unicode(s)
     740
     741            ## To prevent mail loop
     742            #
     743            if field == 'cc':
     744
     745                cc_list = user_dict['cc'].split(',')
     746
     747                if self.trac_smtp_from in cc_list:
     748                    self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
     749
     750                    cc_list.remove(self.trac_smtp_from)
     751
     752                value = ','.join(cc_list)
     753               
     754
     755            ## Check if every value is allowed for this field
     756            #
     757            if sys_dict.has_key(field):
     758
     759                if value in sys_dict[field]:
     760                    ticket[field] = value
     761                else:
     762                    ## Must we set a default if value is not allowed
     763                    #
     764                    if new:
     765                        value = self.get_config('ticket', 'default_%s' %(field) )
     766
     767            else:
     768                ## Only set if we have a value
     769                #
     770                #if value:
     771                #   ticket[field] = value
     772                ticket[field] = value
     773
     774            if self.parameters.debug:
     775                s = 'ticket_field\t %s = %s' %(field,  ticket[field])
     776                self.print_unicode(s)
     777
     778    def ticket_update(self, m, id, spam):
     779        """
     780        If the current email is a reply to an existing ticket, this function
     781        will append the contents of this email to that ticket, instead of
     782        creating a new one.
     783        """
     784        self.logger.debug("function ticket_update")
     785
     786        if not self.parameters.ticket_update:
     787            self.logger.debug("ticket_update disabled")
     788            return False
     789
     790        ## Must we update ticket fields
     791        #
     792        update_fields = dict()
     793        try:
     794            id, keywords = string.split(id, '?')
     795
     796            update_fields = self.str_to_dict(keywords)
     797
     798            ## Strip '#'
     799            #
     800            self.id = int(id[1:])
     801
     802        except ValueError:
     803
     804            ## Strip '#'
     805            #
     806            self.id = int(id[1:])
     807
     808        self.logger.debug("ticket_update id %s" %id)
     809
     810        ## When is the change committed
     811        #
     812        if self.VERSION < 0.11:
     813            when = int(time.time())
     814        else:
     815            when = datetime.now(util.datefmt.utc)
     816
     817        try:
     818            tkt = Ticket(self.env, self.id, self.db)
     819
     820        except util.TracError, detail:
     821
     822            ## Not a valid ticket
     823            #
     824            self.id = None
     825            return False
     826
     827        ## Check the permission of the reporter
     828        #
     829        if self.parameters.ticket_permission_system:
     830            if not self.check_permission(tkt, 'TICKET_APPEND'):
     831                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
     832                return False
     833
     834        ## How many changes has this ticket
     835        #
     836        # cnum = len(tkt.get_changelog())
     837        grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
     838        cnum = sum(1 for e in grouped) + 1
     839
     840
     841        ## reopen the ticket if it is was closed
     842        #  We must use the ticket workflow framework
     843        #
     844        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
     845
     846            self.logger.debug('Workflow ticket update fields: ')
     847
     848            from trac.ticket.default_workflow import ConfigurableTicketWorkflow
     849            from trac.test import Mock, MockPerm
     850
     851            req = Mock(authname=self.author, perm=MockPerm(), args={})
     852            try:
     853                workflow = self.parameters['workflow_%s' %tkt['status']]
     854            except KeyError:
     855                ## fallback for compability (Will be deprecated)
     856                #
     857                if tkt['status'] in ['closed']:
     858                    workflow = self.parameters.workflow
     859                else:   
     860                    workflow = None
     861
     862            controller = ConfigurableTicketWorkflow(self.env)
     863            #print controller.actions
     864            #print controller.actions.keys()
     865            #print controller.get_ticket_actions(req, tkt)
     866            #print controller.actions[workflow]
     867            #print controller.actions[workflow]['permissions'] is a list
     868
     869            if workflow:
     870
     871                if self.parameters.ticket_permission_system:
     872
     873                    if self.check_permission(tkt, controller.actions[workflow]['permissions'][0]):
     874                        fields = controller.get_ticket_changes(req, tkt, workflow)
     875                    else:
     876                        fields = dict()
     877                        self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author)
     878
     879                else:
     880                    fields = controller.get_ticket_changes(req, tkt, workflow)
     881
     882                for key in fields.keys():
     883                    self.logger.debug('\t %s : %s' %(key, fields[key]))
     884                    tkt[key] = fields[key]
     885
     886        ## Old pre 0.11 situation
     887        #
     888        elif self.parameters.email_triggers_workflow:
     889
     890            self.logger.debug('email triggers workflow pre trac 0.11')
     891
     892            if tkt['status'] in ['closed']:
     893                tkt['status'] = 'reopened'
     894                tkt['resolution'] = ''
     895
     896        else:
     897            self.logger.debug('email triggers workflow disabled')
     898
     899        ## Must we update some ticket fields properties via subject line
     900        #
     901        if update_fields:
     902            self.update_ticket_fields(tkt, update_fields)
     903
     904        message_parts = self.get_message_parts(m)
     905        message_parts = self.unique_attachment_names(message_parts)
     906
     907        ## Must we update some ticket fields properties via body_text
     908        #
     909        if self.properties:
     910                self.update_ticket_fields(tkt, self.properties)
     911
     912        if self.parameters.email_header:
     913            message_parts.insert(0, self.email_header_txt(m))
     914
     915        body_text = self.get_body_text(message_parts)
     916
     917        error_with_attachments = self.attach_attachments(message_parts)
     918
     919        if body_text.strip() or update_fields or self.properties:
     920            if self.parameters.dry_run:
     921                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
     922            else:
     923                if error_with_attachments:
     924                    body_text = '%s\\%s' %(error_with_attachments, body_text)
     925                self.logger.debug('tkt.save_changes(%s, %d)' %(self.author, cnum))
     926                tkt.save_changes(self.author, body_text, when, None, str(cnum))
     927           
     928
     929        if not spam:
     930            self.notify(tkt, False, when)
     931
     932        return True
     933
     934    def set_ticket_fields(self, ticket):
     935        """
     936        set the ticket fields to value specified
     937            - /etc/email2trac.conf with <prefix>_<field>
     938            - trac default values, trac.ini
     939        """
     940        self.logger.debug('function set_ticket_fields')
     941
     942        user_dict = dict()
     943
     944        for field in ticket.fields:
     945
     946            name = field['name']
     947
     948            ## default trac value
     949            #
     950            if not field.get('custom'):
     951                value = self.get_config('ticket', 'default_%s' %(name) )
     952                if (name in ['resolution']) and (value in ['fixed']):
     953                    value = None
     954            else:
     955                ##  Else get the default value for reporter
     956                #
     957                value = field.get('value')
     958                options = field.get('options')
     959
     960                if value and options and (value not in options):
     961                    value = options[int(value)]
     962   
     963            if self.parameters.debug:
     964                s = 'trac[%s] = %s' %(name, value)
     965                self.print_unicode(s)
     966
     967            ## email2trac.conf settings
     968            #
     969            prefix = self.parameters.ticket_prefix
     970            try:
     971                value = self.parameters['%s_%s' %(prefix, name)]
     972                if self.parameters.debug:
     973                    s = 'email2trac[%s] = %s ' %(name, value)
     974                    self.print_unicode(s)
     975
     976            except KeyError, detail:
     977                pass
     978       
     979            if value:
     980                user_dict[name] = value
     981                if self.parameters.debug:
     982                    s = 'used %s = %s' %(name, value)
     983                    self.print_unicode(s)
     984
     985        self.update_ticket_fields(ticket, user_dict, new=1)
     986
     987        if 'status' not in user_dict.keys():
     988            ticket['status'] = 'new'
     989
     990
     991    def ticket_update_by_subject(self, subject):
     992        """
     993        This list of Re: prefixes is probably incomplete. Taken from
     994        wikipedia. Here is how the subject is matched
     995          - Re: <subject>
     996          - Re: (<Mail list label>:)+ <subject>
     997
     998        So we must have the last column
     999        """
     1000        self.logger.debug('function ticket_update_by_subject')
     1001
     1002        matched_id = None
     1003        if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
     1004               
     1005            SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
     1006            result = SUBJECT_RE.search(subject)
     1007
     1008            if result:
     1009                ## This is a reply
     1010                #
     1011                orig_subject = result.group(1)
     1012
     1013                self.logger.debug('subject search string: %s' %(orig_subject))
     1014
     1015                cursor = self.db.cursor()
     1016                summaries = [orig_subject, '%%: %s' % orig_subject]
     1017
     1018                ## Convert days to seconds
     1019                #
     1020                lookback = int(time.mktime(time.gmtime())) - \
     1021                        self.parameters.ticket_update_by_subject_lookback * 24 * 3600
     1022
     1023
     1024                for summary in summaries:
     1025                    self.logger.debug('Looking for summary matching: "%s"' % summary)
     1026
     1027                    sql = """SELECT id FROM ticket
     1028                            WHERE changetime >= %s AND summary LIKE %s
     1029                            ORDER BY changetime DESC"""
     1030                    cursor.execute(sql, [lookback, summary.strip()])
     1031
     1032                    for row in cursor:
     1033                        (matched_id,) = row
     1034
     1035                        self.logger.debug('Found matching ticket id: %d' % matched_id)
     1036
     1037                        break
     1038
     1039                    if matched_id:
     1040                        matched_id = '#%d' % matched_id
     1041                        return (matched_id, orig_subject)
     1042                   
     1043                    subject = orig_subject
     1044
     1045        return (matched_id, subject)
     1046
     1047
     1048    def new_ticket(self, msg, subject, spam, set_fields = None):
     1049        """
     1050        Create a new ticket
     1051        """
     1052        self.logger.debug('function new_ticket')
     1053
     1054        tkt = Ticket(self.env)
     1055
     1056        self.set_reply_fields(tkt, msg)
     1057
     1058        self.set_ticket_fields(tkt)
     1059
     1060        ## Check the permission of the reporter
     1061        #
     1062        if self.parameters.ticket_permission_system:
     1063            if not self.check_permission(tkt, 'TICKET_CREATE'):
     1064                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
     1065                return False
     1066
     1067        ## Old style setting for component, will be removed
     1068        #
     1069        if spam:
     1070            tkt['component'] = 'Spam'
     1071
     1072        elif self.parameters.has_key('component'):
     1073            tkt['component'] = self.parameters['component']
     1074
     1075        if not msg['Subject']:
     1076            tkt['summary'] = u'(No subject)'
     1077        else:
     1078            tkt['summary'] = subject
     1079
     1080
     1081        if set_fields:
     1082            rest, keywords = string.split(set_fields, '?')
     1083
     1084            if keywords:
     1085                update_fields = self.str_to_dict(keywords)
     1086                self.update_ticket_fields(tkt, update_fields)
     1087
     1088
     1089        message_parts = self.get_message_parts(msg)
     1090
     1091        ## Must we update some ticket fields properties via body_text
     1092        #
     1093        if self.properties:
     1094                self.update_ticket_fields(tkt, self.properties)
     1095
     1096        message_parts = self.unique_attachment_names(message_parts)
     1097       
     1098        ## produce e-mail like header
     1099        #
     1100        head = ''
     1101        if self.parameters.email_header:
     1102            head = self.email_header_txt(msg)
     1103            message_parts.insert(0, head)
     1104           
     1105        body_text = self.get_body_text(message_parts)
     1106
     1107        tkt['description'] = body_text
     1108
     1109        ## When is the change committed
     1110        #
     1111        if self.VERSION < 0.11:
     1112            when = int(time.time())
     1113        else:
     1114            when = datetime.now(util.datefmt.utc)
     1115
     1116        if self.parameters.dry_run:
     1117            print 'DRY_RUN: tkt.insert()'
     1118        else:
     1119            self.id = tkt.insert()
     1120   
     1121        changed = False
     1122        comment = ''
     1123
     1124        ## some routines in trac are dependend on ticket id
     1125        #  like alternate notify template
     1126        #
     1127        if self.parameters.alternate_notify_template:
     1128            tkt['id'] = self.id
     1129            changed = True
     1130
     1131        ## Rewrite the description if we have mailto enabled
     1132        #
     1133        if self.parameters.mailto_link:
     1134            changed = True
     1135            comment = u'\nadded mailto line\n'
     1136            mailto = self.html_mailto_link( m['Subject'])
     1137
     1138            tkt['description'] = u'%s\r\n%s%s\r\n' \
     1139                %(head, mailto, body_text)
     1140   
     1141        ## Save the attachments to the ticket   
     1142        #
     1143        error_with_attachments =  self.attach_attachments(message_parts)
     1144
     1145        if error_with_attachments:
     1146            changed = True
     1147            comment = '%s\n%s\n' %(comment, error_with_attachments)
     1148
     1149        if changed:
     1150            if self.parameters.dry_run:
     1151                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
     1152            else:
     1153                tkt.save_changes(tkt['reporter'], comment)
     1154                #print tkt.get_changelog(self.db, when)
     1155
     1156        if not spam:
     1157            self.notify(tkt, True)
     1158
     1159
     1160    def attach_attachments(self, message_parts, update=False):
     1161        '''
     1162        save any attachments as files in the ticket's directory
     1163        '''
     1164        self.logger.debug('function attach_attachments()')
     1165
     1166        if self.parameters.dry_run:
     1167            print "DRY_RUN: no attachments attached to tickets"
     1168            return ''
     1169
     1170        count = 0
     1171
     1172        ## Get Maxium attachment size
     1173        #
     1174        max_size = int(self.get_config('attachment', 'max_size'))
     1175        status   = None
     1176       
     1177        for item in message_parts:
     1178            ## Skip body parts
     1179            #
     1180            if not isinstance(item, tuple):
     1181                continue
     1182               
     1183            (original, filename, part) = item
     1184
     1185            ## We have to determine the size so we use this temporary solution. we must escape it
     1186            #  else we get UnicodeErrors.
     1187            #
     1188            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, util.text.unicode_quote(filename)))
     1189            text = part.get_payload(decode=1)
     1190            if not text:
     1191                text = '(None)'
     1192            fd.write(text)
     1193            fd.close()
     1194
     1195            ## get the file_size
     1196            #
     1197            stats = os.lstat(path)
     1198            file_size = stats[ST_SIZE]
     1199
     1200            ## Check if the attachment size is allowed
     1201            #
     1202            if (max_size != -1) and (file_size > max_size):
     1203                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
     1204                    %(status, original, file_size, max_size)
     1205
     1206                os.unlink(path)
     1207                continue
     1208            else:
     1209                count = count + 1
     1210                   
     1211            ## Insert the attachment
     1212            #
     1213            fd = open(path, 'rb')
     1214            if self.system == 'discussion':
     1215                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
     1216                  % (self.id,))
     1217            else:
     1218                self.logger.debug('Attach %s to ticket %d' %(util.text.unicode_quote(filename), self.id))
     1219                att = attachment.Attachment(self.env, 'ticket', self.id)
    12201220 
    1221                         ## This will break the ticket_update system, the body_text is vaporized
    1222                         #  ;-(
    1223                         #
    1224                         if not update:
    1225                                 att.author = self.author
    1226                                 att.description = self.email_to_unicode('Added by email2trac')
    1227 
    1228                         try:
    1229                                 self.logger.debug('Insert atachment')
    1230                                 att.insert(filename, fd, file_size)
    1231                         except OSError, detail:
    1232                                 self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
    1233                                 status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
    1234 
    1235                         ## Remove the created temporary filename
    1236                         #
    1237                         fd.close()
    1238                         os.unlink(path)
    1239 
    1240                 ## return error
    1241                 #
    1242                 return status
     1221            ## This will break the ticket_update system, the body_text is vaporized
     1222            #  ;-(
     1223            #
     1224            if not update:
     1225                att.author = self.author
     1226                att.description = self.email_to_unicode('Added by email2trac')
     1227
     1228            try:
     1229                self.logger.debug('Insert atachment')
     1230                att.insert(filename, fd, file_size)
     1231            except OSError, detail:
     1232                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
     1233                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
     1234
     1235            ## Remove the created temporary filename
     1236            #
     1237            fd.close()
     1238            os.unlink(path)
     1239
     1240        ## return error
     1241        #
     1242        return status
    12431243
    12441244########## Fullblog functions  #################################################
    12451245
    1246         def blog(self, id):
    1247                 """
    1248                 The blog create/update function
    1249                 """
    1250                 ## import the modules
    1251                 #
    1252                 from tracfullblog.core import FullBlogCore
    1253                 from tracfullblog.model import BlogPost, BlogComment
    1254                 from trac.test import Mock, MockPerm
    1255 
    1256                 ## instantiate blog core
    1257                 #
    1258                 blog = FullBlogCore(self.env)
    1259                 req = Mock(authname='anonymous', perm=MockPerm(), args={})
    1260 
    1261                 if id:
    1262 
    1263                         ## update blog
    1264                         #
    1265                         comment = BlogComment(self.env, id)
    1266                         comment.author = self.author
    1267 
    1268                         message_parts = self.get_message_parts(m)
    1269                         comment.comment = self.get_body_text(message_parts)
    1270 
    1271                         blog.create_comment(req, comment)
    1272 
    1273                 else:
    1274                         ## create blog
    1275                         #
    1276                         import time
    1277                         post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
    1278 
    1279                         #post = BlogPost(self.env, blog._get_default_postname(self.env))
    1280                        
    1281                         post.author = self.author
    1282                         post.title = self.email_to_unicode(m['Subject'])
    1283 
    1284                         message_parts = self.get_message_parts(m)
    1285                         post.body = self.get_body_text(message_parts)
    1286                        
    1287                         blog.create_post(req, post, self.author, u'Created by email2trac', False)
     1246    def blog(self, id):
     1247        """
     1248        The blog create/update function
     1249        """
     1250        ## import the modules
     1251        #
     1252        from tracfullblog.core import FullBlogCore
     1253        from tracfullblog.model import BlogPost, BlogComment
     1254        from trac.test import Mock, MockPerm
     1255
     1256        ## instantiate blog core
     1257        #
     1258        blog = FullBlogCore(self.env)
     1259        req = Mock(authname='anonymous', perm=MockPerm(), args={})
     1260
     1261        if id:
     1262
     1263            ## update blog
     1264            #
     1265            comment = BlogComment(self.env, id)
     1266            comment.author = self.author
     1267
     1268            message_parts = self.get_message_parts(m)
     1269            comment.comment = self.get_body_text(message_parts)
     1270
     1271            blog.create_comment(req, comment)
     1272
     1273        else:
     1274            ## create blog
     1275            #
     1276            import time
     1277            post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
     1278
     1279            #post = BlogPost(self.env, blog._get_default_postname(self.env))
     1280           
     1281            post.author = self.author
     1282            post.title = self.email_to_unicode(m['Subject'])
     1283
     1284            message_parts = self.get_message_parts(m)
     1285            post.body = self.get_body_text(message_parts)
     1286           
     1287            blog.create_post(req, post, self.author, u'Created by email2trac', False)
    12881288
    12891289
    12901290########## Discussion functions  ##############################################
    12911291
    1292         def discussion_topic(self, content, subject):
    1293 
    1294                 ## Import modules.
    1295                 #
    1296                 from tracdiscussion.api import DiscussionApi
    1297                 from trac.util.datefmt import to_timestamp, utc
    1298 
    1299                 self.logger.debug('Creating a new topic in forum:', self.id)
    1300 
    1301                 ## Get dissussion API component.
    1302                 #
    1303                 api = self.env[DiscussionApi]
    1304                 context = self._create_context(content, subject)
    1305 
    1306                 ## Get forum for new topic.
    1307                 #
    1308                 forum = api.get_forum(context, self.id)
    1309 
    1310                 if not forum:
    1311                         self.logger.error("ERROR: Replied forum doesn't exist")
    1312 
    1313                 ## Prepare topic.
    1314                 #
    1315                 topic = {'forum' : forum['id'],
    1316                                 'subject' : context.subject,
    1317                                 'time': to_timestamp(datetime.now(utc)),
    1318                                 'author' : self.author,
    1319                                 'subscribers' : [self.email_addr],
    1320                                 'body' : self.get_body_text(context.content_parts)}
    1321 
    1322                 ## Add topic to DB and commit it.
    1323                 #
    1324                 self._add_topic(api, context, topic)
    1325                 self.db.commit()
    1326 
    1327         def discussion_topic_reply(self, content, subject):
    1328 
    1329                 ## Import modules.
    1330                 #
    1331                 from tracdiscussion.api import DiscussionApi
    1332                 from trac.util.datefmt import to_timestamp, utc
    1333 
    1334                 self.logger.debug('Replying to discussion topic', self.id)
    1335 
    1336                 ## Get dissussion API component.
    1337                 #
    1338                 api = self.env[DiscussionApi]
    1339                 context = self._create_context(content, subject)
    1340 
    1341                 ## Get replied topic.
    1342                 #
    1343                 topic = api.get_topic(context, self.id)
    1344 
    1345                 if not topic:
    1346                         self.logger.error("ERROR: Replied topic doesn't exist")
    1347 
    1348                 ## Prepare message.
    1349                 #
    1350                 message = {'forum' : topic['forum'],
    1351                                    'topic' : topic['id'],
    1352                                    'replyto' : -1,
    1353                                    'time' : to_timestamp(datetime.now(utc)),
    1354                                    'author' : self.author,
    1355                                    'body' : self.get_body_text(context.content_parts)}
    1356 
    1357                 ## Add message to DB and commit it.
    1358                 #
    1359                 self._add_message(api, context, message)
    1360                 self.db.commit()
    1361 
    1362         def discussion_message_reply(self, content, subject):
    1363 
    1364                 ## Import modules.
    1365                 #
    1366                 from tracdiscussion.api import DiscussionApi
    1367                 from trac.util.datefmt import to_timestamp, utc
    1368 
    1369                 self.logger.debug('Replying to discussion message', self.id)
    1370 
    1371                 ## Get dissussion API component.
    1372                 #
    1373                 api = self.env[DiscussionApi]
    1374                 context = self._create_context(content, subject)
    1375 
    1376                 ## Get replied message.
    1377                 #
    1378                 message = api.get_message(context, self.id)
    1379 
    1380                 if not message:
    1381                         self.logger.error("ERROR: Replied message doesn't exist")
    1382 
    1383                 ## Prepare message.
    1384                 #
    1385                 message = {'forum' : message['forum'],
    1386                                    'topic' : message['topic'],
    1387                                    'replyto' : message['id'],
    1388                                    'time' : to_timestamp(datetime.now(utc)),
    1389                                    'author' : self.author,
    1390                                    'body' : self.get_body_text(context.content_parts)}
    1391 
    1392                 ## Add message to DB and commit it.
    1393                 #
    1394                 self._add_message(api, context, message)
    1395                 self.db.commit()
    1396 
    1397         def _create_context(self, content, subject):
    1398 
    1399                 ## Import modules.
    1400                 #
    1401                 from trac.mimeview import Context
    1402                 from trac.web.api import Request
    1403                 from trac.perm import PermissionCache
    1404 
    1405                 ## TODO: Read server base URL from config.
    1406                 #  Create request object to mockup context creation.
    1407                 #
    1408                 environ = {'SERVER_PORT' : 80,
    1409                                    'SERVER_NAME' : 'test',
    1410                                    'REQUEST_METHOD' : 'POST',
    1411                                    'wsgi.url_scheme' : 'http',
    1412                                    'wsgi.input' : sys.stdin}
    1413                 chrome =  {'links': {},
    1414                                    'scripts': [],
    1415                                    'ctxtnav': [],
    1416                                    'warnings': [],
    1417                                    'notices': []}
    1418 
    1419                 if self.env.base_url_for_redirect:
    1420                         environ['trac.base_url'] = self.env.base_url
    1421 
    1422                 req = Request(environ, None)
    1423                 req.chrome = chrome
    1424                 req.tz = 'missing'
    1425                 req.authname = self.author
    1426                 req.perm = PermissionCache(self.env, self.author)
    1427                 req.locale = None
    1428 
    1429                 ## Create and return context.
    1430                 #
    1431                 context = Context.from_request(req)
    1432                 context.realm = 'discussion-email2trac'
    1433                 context.cursor = self.db.cursor()
    1434                 context.content = content
    1435                 context.subject = subject
    1436 
    1437                 ## Read content parts from content.
    1438                 #
    1439                 context.content_parts = self.get_message_parts(content)
    1440                 context.content_parts = self.unique_attachment_names(
    1441                   context.content_parts)
    1442 
    1443                 return context
    1444 
    1445         def _add_topic(self, api, context, topic):
    1446                 context.req.perm.assert_permission('DISCUSSION_APPEND')
    1447 
    1448                 ## Filter topic.
    1449                 #
    1450                 for discussion_filter in api.discussion_filters:
    1451                         accept, topic_or_error = discussion_filter.filter_topic(
    1452                           context, topic)
    1453                         if accept:
    1454                                 topic = topic_or_error
    1455                         else:
    1456                                 raise TracError(topic_or_error)
    1457 
    1458                 ## Add a new topic.
    1459                 #
    1460                 api.add_topic(context, topic)
    1461 
    1462                 ## Get inserted topic with new ID.
    1463                 #
    1464                 topic = api.get_topic_by_time(context, topic['time'])
    1465 
    1466                 ## Attach attachments.
    1467                 #
    1468                 self.id = topic['id']
    1469                 self.attach_attachments(context.content_parts, True)
    1470 
    1471                 ## Notify change listeners.
    1472                 #
    1473                 for listener in api.topic_change_listeners:
    1474                         listener.topic_created(context, topic)
    1475 
    1476         def _add_message(self, api, context, message):
    1477                 context.req.perm.assert_permission('DISCUSSION_APPEND')
    1478 
    1479                 ## Filter message.
    1480                 #
    1481                 for discussion_filter in api.discussion_filters:
    1482                         accept, message_or_error = discussion_filter.filter_message(
    1483                           context, message)
    1484                         if accept:
    1485                                 message = message_or_error
    1486                         else:
    1487                                 raise TracError(message_or_error)
    1488 
    1489                 ## Add message.
    1490                 #
    1491                 api.add_message(context, message)
    1492 
    1493                 ## Get inserted message with new ID.
    1494                 #
    1495                 message = api.get_message_by_time(context, message['time'])
    1496 
    1497                 ## Attach attachments.
    1498                 #
    1499                 self.id = message['topic']
    1500                 self.attach_attachments(context.content_parts, True)
    1501 
    1502                 ## Notify change listeners.
    1503                 #
    1504                 for listener in api.message_change_listeners:
    1505                         listener.message_created(context, message)
     1292    def discussion_topic(self, content, subject):
     1293
     1294        ## Import modules.
     1295        #
     1296        from tracdiscussion.api import DiscussionApi
     1297        from trac.util.datefmt import to_timestamp, utc
     1298
     1299        self.logger.debug('Creating a new topic in forum:', self.id)
     1300
     1301        ## Get dissussion API component.
     1302        #
     1303        api = self.env[DiscussionApi]
     1304        context = self._create_context(content, subject)
     1305
     1306        ## Get forum for new topic.
     1307        #
     1308        forum = api.get_forum(context, self.id)
     1309
     1310        if not forum:
     1311            self.logger.error("ERROR: Replied forum doesn't exist")
     1312
     1313        ## Prepare topic.
     1314        #
     1315        topic = {'forum' : forum['id'],
     1316                'subject' : context.subject,
     1317                'time': to_timestamp(datetime.now(utc)),
     1318                'author' : self.author,
     1319                'subscribers' : [self.email_addr],
     1320                'body' : self.get_body_text(context.content_parts)}
     1321
     1322        ## Add topic to DB and commit it.
     1323        #
     1324        self._add_topic(api, context, topic)
     1325        self.db.commit()
     1326
     1327    def discussion_topic_reply(self, content, subject):
     1328
     1329        ## Import modules.
     1330        #
     1331        from tracdiscussion.api import DiscussionApi
     1332        from trac.util.datefmt import to_timestamp, utc
     1333
     1334        self.logger.debug('Replying to discussion topic', self.id)
     1335
     1336        ## Get dissussion API component.
     1337        #
     1338        api = self.env[DiscussionApi]
     1339        context = self._create_context(content, subject)
     1340
     1341        ## Get replied topic.
     1342        #
     1343        topic = api.get_topic(context, self.id)
     1344
     1345        if not topic:
     1346            self.logger.error("ERROR: Replied topic doesn't exist")
     1347
     1348        ## Prepare message.
     1349        #
     1350        message = {'forum' : topic['forum'],
     1351                   'topic' : topic['id'],
     1352                   'replyto' : -1,
     1353                   'time' : to_timestamp(datetime.now(utc)),
     1354                   'author' : self.author,
     1355                   'body' : self.get_body_text(context.content_parts)}
     1356
     1357        ## Add message to DB and commit it.
     1358        #
     1359        self._add_message(api, context, message)
     1360        self.db.commit()
     1361
     1362    def discussion_message_reply(self, content, subject):
     1363
     1364        ## Import modules.
     1365        #
     1366        from tracdiscussion.api import DiscussionApi
     1367        from trac.util.datefmt import to_timestamp, utc
     1368
     1369        self.logger.debug('Replying to discussion message', self.id)
     1370
     1371        ## Get dissussion API component.
     1372        #
     1373        api = self.env[DiscussionApi]
     1374        context = self._create_context(content, subject)
     1375
     1376        ## Get replied message.
     1377        #
     1378        message = api.get_message(context, self.id)
     1379
     1380        if not message:
     1381            self.logger.error("ERROR: Replied message doesn't exist")
     1382
     1383        ## Prepare message.
     1384        #
     1385        message = {'forum' : message['forum'],
     1386                   'topic' : message['topic'],
     1387                   'replyto' : message['id'],
     1388                   'time' : to_timestamp(datetime.now(utc)),
     1389                   'author' : self.author,
     1390                   'body' : self.get_body_text(context.content_parts)}
     1391
     1392        ## Add message to DB and commit it.
     1393        #
     1394        self._add_message(api, context, message)
     1395        self.db.commit()
     1396
     1397    def _create_context(self, content, subject):
     1398
     1399        ## Import modules.
     1400        #
     1401        from trac.mimeview import Context
     1402        from trac.web.api import Request
     1403        from trac.perm import PermissionCache
     1404
     1405        ## TODO: Read server base URL from config.
     1406        #  Create request object to mockup context creation.
     1407        #
     1408        environ = {'SERVER_PORT' : 80,
     1409                   'SERVER_NAME' : 'test',
     1410                   'REQUEST_METHOD' : 'POST',
     1411                   'wsgi.url_scheme' : 'http',
     1412                   'wsgi.input' : sys.stdin}
     1413        chrome =  {'links': {},
     1414                   'scripts': [],
     1415                   'ctxtnav': [],
     1416                   'warnings': [],
     1417                   'notices': []}
     1418
     1419        if self.env.base_url_for_redirect:
     1420            environ['trac.base_url'] = self.env.base_url
     1421
     1422        req = Request(environ, None)
     1423        req.chrome = chrome
     1424        req.tz = 'missing'
     1425        req.authname = self.author
     1426        req.perm = PermissionCache(self.env, self.author)
     1427        req.locale = None
     1428
     1429        ## Create and return context.
     1430        #
     1431        context = Context.from_request(req)
     1432        context.realm = 'discussion-email2trac'
     1433        context.cursor = self.db.cursor()
     1434        context.content = content
     1435        context.subject = subject
     1436
     1437        ## Read content parts from content.
     1438        #
     1439        context.content_parts = self.get_message_parts(content)
     1440        context.content_parts = self.unique_attachment_names(
     1441          context.content_parts)
     1442
     1443        return context
     1444
     1445    def _add_topic(self, api, context, topic):
     1446        context.req.perm.assert_permission('DISCUSSION_APPEND')
     1447
     1448        ## Filter topic.
     1449        #
     1450        for discussion_filter in api.discussion_filters:
     1451            accept, topic_or_error = discussion_filter.filter_topic(
     1452              context, topic)
     1453            if accept:
     1454                topic = topic_or_error
     1455            else:
     1456                raise TracError(topic_or_error)
     1457
     1458        ## Add a new topic.
     1459        #
     1460        api.add_topic(context, topic)
     1461
     1462        ## Get inserted topic with new ID.
     1463        #
     1464        topic = api.get_topic_by_time(context, topic['time'])
     1465
     1466        ## Attach attachments.
     1467        #
     1468        self.id = topic['id']
     1469        self.attach_attachments(context.content_parts, True)
     1470
     1471        ## Notify change listeners.
     1472        #
     1473        for listener in api.topic_change_listeners:
     1474            listener.topic_created(context, topic)
     1475
     1476    def _add_message(self, api, context, message):
     1477        context.req.perm.assert_permission('DISCUSSION_APPEND')
     1478
     1479        ## Filter message.
     1480        #
     1481        for discussion_filter in api.discussion_filters:
     1482            accept, message_or_error = discussion_filter.filter_message(
     1483              context, message)
     1484            if accept:
     1485                message = message_or_error
     1486            else:
     1487                raise TracError(message_or_error)
     1488
     1489        ## Add message.
     1490        #
     1491        api.add_message(context, message)
     1492
     1493        ## Get inserted message with new ID.
     1494        #
     1495        message = api.get_message_by_time(context, message['time'])
     1496
     1497        ## Attach attachments.
     1498        #
     1499        self.id = message['topic']
     1500        self.attach_attachments(context.content_parts, True)
     1501
     1502        ## Notify change listeners.
     1503        #
     1504        for listener in api.message_change_listeners:
     1505            listener.message_created(context, message)
    15061506
    15071507########## MAIN function  ######################################################
    15081508
    1509         def parse(self, fp):
    1510                 """
    1511                 """
    1512                 self.logger.debug('Main function parse')
    1513                 global m
    1514 
    1515                 m = email.message_from_file(fp)
    1516                
    1517                 if not m:
    1518                         self.logger.debug('This is not a valid email message format')
    1519                         return
    1520                        
    1521                 ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
    1522                 #
    1523                 try:
    1524                         m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
    1525                 except AttributeError, detail:
    1526                         pass
    1527 
    1528                 if self.parameters.debug:         # save the entire e-mail message text
    1529                         self.save_email_for_debug(m, True)
    1530 
    1531                 self.db = self.env.get_db_cnx()
    1532                 self.get_sender_info(m)
    1533 
    1534                 if not self.email_header_acl('white_list', self.email_addr, True):
    1535                         self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
    1536                         return False
    1537 
    1538                 if self.email_header_acl('black_list', self.email_addr, False):
    1539                         self.logger.info('Message rejected : %s in black list' %(self.email_addr))
    1540                         return False
    1541 
    1542                 if not self.email_header_acl('recipient_list', self.to_email_addr, True):
    1543                         self.logger.info('Message rejected : %s not in recipient list' %(self.to_email_addr))
    1544                         return False
    1545 
    1546                 ## If spam drop the message
    1547                 #
    1548                 if self.spam(m) == 'drop':
    1549                         return False
    1550 
    1551                 elif self.spam(m) == 'spam':
    1552                         spam_msg = True
    1553                 else:
    1554                         spam_msg = False
    1555 
    1556                 if not m['Subject']:
    1557                         subject  = 'No Subject'
    1558                 else:
    1559                         subject  = self.email_to_unicode(m['Subject'])
    1560 
    1561                 self.logger.debug('subject: %s' %subject)
    1562 
    1563                 ## [hic] #1529: Re: LRZ
    1564                 #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
    1565                 #
    1566                 ticket_regex = r'''
    1567                         (?P<new_fields>[#][?].*)
    1568                         |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
    1569                         '''
    1570                 ## Check if  FullBlogPlugin is installed
    1571                 #
    1572                 blog_enabled = None
    1573                 blog_regex = ''
    1574                 if self.get_config('components', 'tracfullblog.*') in ['enabled']:
    1575                         self.logger.debug('Trac BLOG support enabled')
    1576                         blog_enabled = True
    1577                         blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
    1578 
    1579 
    1580                 ## Check if DiscussionPlugin is installed
    1581                 #
    1582                 discussion_enabled = None
    1583                 discussion_regex = ''
    1584                 if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
    1585                         self.logger.debug('Trac Discussion support enabled')
    1586                         discussion_enabled = True
    1587                         discussion_regex = r'''
    1588                         |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
    1589                         |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
    1590                         |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
    1591                         '''
    1592 
    1593 
    1594                 regex_str = ticket_regex + blog_regex + discussion_regex
    1595                 SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
    1596 
    1597                 ## Find out if this is a ticket, a blog or a discussion
    1598                 #
    1599                 result =  SYSTEM_RE.search(subject)
    1600 
    1601                 if result:
    1602                         ## update ticket + fields
    1603                         #
    1604                         if result.group('reply'):
    1605                                 self.system = 'ticket'
    1606 
    1607                                 ## Skip the last ':' character
    1608                                 #
    1609                                 if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
    1610                                         self.new_ticket(m, subject, spam_msg)
    1611 
    1612                         ## New ticket + fields
    1613                         #
    1614                         elif result.group('new_fields'):
    1615                                 self.system = 'ticket'
    1616                                 self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
    1617 
    1618                         if blog_enabled:
    1619                                 if result.group('blog'):
    1620                                         self.system = 'blog'
    1621                                         self.blog(result.group('blog_id'))
    1622 
    1623                         if discussion_enabled:
    1624                                 ## New topic.
    1625                                 #
    1626                                 if result.group('forum'):
    1627                                         self.system = 'discussion'
    1628                                         self.id = int(result.group('forum_id'))
    1629                                         self.discussion_topic(m, subject[result.end('forum'):])
    1630 
    1631                                 ## Reply to topic.
    1632                                 #
    1633                                 elif result.group('topic'):
    1634                                         self.system = 'discussion'
    1635                                         self.id = int(result.group('topic_id'))
    1636                                         self.discussion_topic_reply(m, subject[result.end('topic'):])
    1637 
    1638                                 ## Reply to topic message.
    1639                                 #
    1640                                 elif result.group('message'):
    1641                                         self.system = 'discussion'
    1642                                         self.id = int(result.group('message_id'))
    1643                                         self.discussion_message_reply(m, subject[result.end('message'):])
    1644 
    1645                 else:
    1646                         self.system = 'ticket'
    1647                         (matched_id, subject) = self.ticket_update_by_subject(subject)
    1648                         if matched_id:
    1649                                 if not self.ticket_update(m, matched_id, spam_msg):
    1650                                         self.new_ticket(m, subject, spam_msg)
    1651                         else:
    1652                                 ## No update by subject, so just create a new ticket
    1653                                 #
    1654                                 self.new_ticket(m, subject, spam_msg)
     1509    def parse(self, fp):
     1510        """
     1511        """
     1512        self.logger.debug('Main function parse')
     1513        global m
     1514
     1515        m = email.message_from_file(fp)
     1516       
     1517        if not m:
     1518            self.logger.debug('This is not a valid email message format')
     1519            return
     1520           
     1521        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
     1522        #
     1523        try:
     1524            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
     1525        except AttributeError, detail:
     1526            pass
     1527
     1528        if self.parameters.debug:     # save the entire e-mail message text
     1529            self.save_email_for_debug(m, True)
     1530
     1531        self.db = self.env.get_db_cnx()
     1532        self.get_sender_info(m)
     1533
     1534        if not self.email_header_acl('white_list', self.email_addr, True):
     1535            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
     1536            return False
     1537
     1538        if self.email_header_acl('black_list', self.email_addr, False):
     1539            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
     1540            return False
     1541
     1542        if not self.email_header_acl('recipient_list', self.to_email_addr, True):
     1543            self.logger.info('Message rejected : %s not in recipient list' %(self.to_email_addr))
     1544            return False
     1545
     1546        ## If spam drop the message
     1547        #
     1548        if self.spam(m) == 'drop':
     1549            return False
     1550
     1551        elif self.spam(m) == 'spam':
     1552            spam_msg = True
     1553        else:
     1554            spam_msg = False
     1555
     1556        if not m['Subject']:
     1557            subject  = 'No Subject'
     1558        else:
     1559            subject  = self.email_to_unicode(m['Subject'])
     1560
     1561        self.logger.debug('subject: %s' %subject)
     1562
     1563        ## [hic] #1529: Re: LRZ
     1564        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
     1565        #
     1566        ticket_regex = r'''
     1567            (?P<new_fields>[#][?].*)
     1568            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
     1569            '''
     1570        ## Check if  FullBlogPlugin is installed
     1571        #
     1572        blog_enabled = None
     1573        blog_regex = ''
     1574        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
     1575            self.logger.debug('Trac BLOG support enabled')
     1576            blog_enabled = True
     1577            blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
     1578
     1579
     1580        ## Check if DiscussionPlugin is installed
     1581        #
     1582        discussion_enabled = None
     1583        discussion_regex = ''
     1584        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
     1585            self.logger.debug('Trac Discussion support enabled')
     1586            discussion_enabled = True
     1587            discussion_regex = r'''
     1588            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
     1589            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
     1590            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
     1591            '''
     1592
     1593
     1594        regex_str = ticket_regex + blog_regex + discussion_regex
     1595        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
     1596
     1597        ## Find out if this is a ticket, a blog or a discussion
     1598        #
     1599        result =  SYSTEM_RE.search(subject)
     1600
     1601        if result:
     1602            ## update ticket + fields
     1603            #
     1604            if result.group('reply'):
     1605                self.system = 'ticket'
     1606
     1607                ## Skip the last ':' character
     1608                #
     1609                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
     1610                    self.new_ticket(m, subject, spam_msg)
     1611
     1612            ## New ticket + fields
     1613            #
     1614            elif result.group('new_fields'):
     1615                self.system = 'ticket'
     1616                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
     1617
     1618            if blog_enabled:
     1619                if result.group('blog'):
     1620                    self.system = 'blog'
     1621                    self.blog(result.group('blog_id'))
     1622
     1623            if discussion_enabled:
     1624                ## New topic.
     1625                #
     1626                if result.group('forum'):
     1627                    self.system = 'discussion'
     1628                    self.id = int(result.group('forum_id'))
     1629                    self.discussion_topic(m, subject[result.end('forum'):])
     1630
     1631                ## Reply to topic.
     1632                #
     1633                elif result.group('topic'):
     1634                    self.system = 'discussion'
     1635                    self.id = int(result.group('topic_id'))
     1636                    self.discussion_topic_reply(m, subject[result.end('topic'):])
     1637
     1638                ## Reply to topic message.
     1639                #
     1640                elif result.group('message'):
     1641                    self.system = 'discussion'
     1642                    self.id = int(result.group('message_id'))
     1643                    self.discussion_message_reply(m, subject[result.end('message'):])
     1644
     1645        else:
     1646            self.system = 'ticket'
     1647            (matched_id, subject) = self.ticket_update_by_subject(subject)
     1648            if matched_id:
     1649                if not self.ticket_update(m, matched_id, spam_msg):
     1650                    self.new_ticket(m, subject, spam_msg)
     1651            else:
     1652                ## No update by subject, so just create a new ticket
     1653                #
     1654                self.new_ticket(m, subject, spam_msg)
    16551655
    16561656
    16571657########## BODY TEXT functions  ###########################################################
    16581658
    1659         def strip_signature(self, text):
    1660                 """
    1661                 Strip signature from message, inspired by Mailman software
    1662                 """
    1663                 self.logger.debug('function strip_signature')
    1664 
    1665                 body = []
    1666                 for line in text.splitlines():
    1667                         if line == '-- ':
    1668                                 break
    1669                         body.append(line)
    1670 
    1671                 return ('\n'.join(body))
    1672 
    1673         def reflow(self, text, delsp = 0):
    1674                 """
    1675                 Reflow the message based on the format="flowed" specification (RFC 3676)
    1676                 """
    1677                 flowedlines = []
    1678                 quotelevel = 0
    1679                 prevflowed = 0
    1680 
    1681                 for line in text.splitlines():
    1682                         from re import match
    1683                        
    1684                         ## Figure out the quote level and the content of the current line
    1685                         #
    1686                         m = match('(>*)( ?)(.*)', line)
    1687                         linequotelevel = len(m.group(1))
    1688                         line = m.group(3)
    1689 
    1690                         ## Determine whether this line is flowed
    1691                         #
    1692                         if line and line != '-- ' and line[-1] == ' ':
    1693                                 flowed = 1
    1694                         else:
    1695                                 flowed = 0
    1696 
    1697                         if flowed and delsp and line and line[-1] == ' ':
    1698                                 line = line[:-1]
    1699 
    1700                         ## If the previous line is flowed, append this line to it
    1701                         #
    1702                         if prevflowed and line != '-- ' and linequotelevel == quotelevel:
    1703                                 flowedlines[-1] += line
    1704 
    1705                         ## Otherwise, start a new line
    1706                         #
    1707                         else:
    1708                                 flowedlines.append('>' * linequotelevel + line)
    1709 
    1710                         prevflowed = flowed
    1711                        
    1712 
    1713                 return '\n'.join(flowedlines)
    1714 
    1715         def strip_quotes(self, text):
    1716                 """
    1717                 Strip quotes from message by Nicolas Mendoza
    1718                 """
    1719                 self.logger.debug('function strip_quotes')
    1720 
    1721                 body = []
    1722                 for line in text.splitlines():
    1723                         try:
    1724 
    1725                                 if line.startswith(self.parameters.email_quote):
    1726                                         continue
    1727 
    1728                         except UnicodeDecodeError:
    1729 
    1730                                 tmp_line = self.email_to_unicode(line)
    1731                                 if tmp_line.startswith(self.parameters.email_quote):
    1732                                         continue
    1733                                
    1734                         body.append(line)
    1735 
    1736                 return ('\n'.join(body))
    1737 
    1738         def inline_properties(self, text):
    1739                 """
    1740                 Parse text if we use inline keywords to set ticket fields
    1741                 """
    1742                 self.logger.debug('function inline_properties')
    1743 
    1744                 properties = dict()
    1745                 body = list()
    1746 
    1747                 INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
    1748 
    1749                 for line in text.splitlines():
    1750                         match = INLINE_EXP.match(line)
    1751                         if match:
    1752                                 keyword, value = match.groups()
    1753                                 self.properties[keyword] = value.strip()
    1754 
    1755                                 self.logger.debug('inline properties: %s : %s' %(keyword,value))
    1756 
    1757                         else:
    1758                                 body.append(line)
    1759                                
    1760                 return '\n'.join(body)
    1761 
    1762 
    1763         def wrap_text(self, text, replace_whitespace = False):
    1764                 """
    1765                 Will break a lines longer then given length into several small
    1766                 lines of size given length
    1767                 """
    1768                 import textwrap
    1769 
    1770                 LINESEPARATOR = '\n'
    1771                 reformat = ''
    1772 
    1773                 for s in text.split(LINESEPARATOR):
    1774                         tmp = textwrap.fill(s, self.parameters.use_textwrap)
    1775                         if tmp:
    1776                                 reformat = '%s\n%s' %(reformat,tmp)
    1777                         else:
    1778                                 reformat = '%s\n' %reformat
    1779 
    1780                 return reformat
    1781 
    1782                 # Python2.4 and higher
    1783                 #
    1784                 #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
    1785                 #
     1659    def strip_signature(self, text):
     1660        """
     1661        Strip signature from message, inspired by Mailman software
     1662        """
     1663        self.logger.debug('function strip_signature')
     1664
     1665        body = []
     1666        for line in text.splitlines():
     1667            if line == '-- ':
     1668                break
     1669            body.append(line)
     1670
     1671        return ('\n'.join(body))
     1672
     1673    def reflow(self, text, delsp = 0):
     1674        """
     1675        Reflow the message based on the format="flowed" specification (RFC 3676)
     1676        """
     1677        flowedlines = []
     1678        quotelevel = 0
     1679        prevflowed = 0
     1680
     1681        for line in text.splitlines():
     1682            from re import match
     1683           
     1684            ## Figure out the quote level and the content of the current line
     1685            #
     1686            m = match('(>*)( ?)(.*)', line)
     1687            linequotelevel = len(m.group(1))
     1688            line = m.group(3)
     1689
     1690            ## Determine whether this line is flowed
     1691            #
     1692            if line and line != '-- ' and line[-1] == ' ':
     1693                flowed = 1
     1694            else:
     1695                flowed = 0
     1696
     1697            if flowed and delsp and line and line[-1] == ' ':
     1698                line = line[:-1]
     1699
     1700            ## If the previous line is flowed, append this line to it
     1701            #
     1702            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
     1703                flowedlines[-1] += line
     1704
     1705            ## Otherwise, start a new line
     1706            #
     1707            else:
     1708                flowedlines.append('>' * linequotelevel + line)
     1709
     1710            prevflowed = flowed
     1711           
     1712
     1713        return '\n'.join(flowedlines)
     1714
     1715    def strip_quotes(self, text):
     1716        """
     1717        Strip quotes from message by Nicolas Mendoza
     1718        """
     1719        self.logger.debug('function strip_quotes')
     1720
     1721        body = []
     1722        for line in text.splitlines():
     1723            try:
     1724
     1725                if line.startswith(self.parameters.email_quote):
     1726                    continue
     1727
     1728            except UnicodeDecodeError:
     1729
     1730                tmp_line = self.email_to_unicode(line)
     1731                if tmp_line.startswith(self.parameters.email_quote):
     1732                    continue
     1733               
     1734            body.append(line)
     1735
     1736        return ('\n'.join(body))
     1737
     1738    def inline_properties(self, text):
     1739        """
     1740        Parse text if we use inline keywords to set ticket fields
     1741        """
     1742        self.logger.debug('function inline_properties')
     1743
     1744        properties = dict()
     1745        body = list()
     1746
     1747        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
     1748
     1749        for line in text.splitlines():
     1750            match = INLINE_EXP.match(line)
     1751            if match:
     1752                keyword, value = match.groups()
     1753                self.properties[keyword] = value.strip()
     1754
     1755                self.logger.debug('inline properties: %s : %s' %(keyword,value))
     1756
     1757            else:
     1758                body.append(line)
     1759               
     1760        return '\n'.join(body)
     1761
     1762
     1763    def wrap_text(self, text, replace_whitespace = False):
     1764        """
     1765        Will break a lines longer then given length into several small
     1766        lines of size given length
     1767        """
     1768        import textwrap
     1769
     1770        LINESEPARATOR = '\n'
     1771        reformat = ''
     1772
     1773        for s in text.split(LINESEPARATOR):
     1774            tmp = textwrap.fill(s, self.parameters.use_textwrap)
     1775            if tmp:
     1776                reformat = '%s\n%s' %(reformat,tmp)
     1777            else:
     1778                reformat = '%s\n' %reformat
     1779
     1780        return reformat
     1781
     1782        # Python2.4 and higher
     1783        #
     1784        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
     1785        #
    17861786
    17871787########## EMAIL attachements functions ###########################################################
    17881788
    1789         def inline_part(self, part):
    1790                 """
    1791                 """
    1792                 self.logger.debug('function inline_part()')
    1793 
    1794                 return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
    1795 
    1796         def get_message_parts(self, msg):
    1797                 """
    1798                 parses the email message and returns a list of body parts and attachments
    1799                 body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
    1800                 """
    1801                 self.logger.debug('function get_message_parts()')
    1802 
    1803                 message_parts = list()
    1804        
    1805                 ALTERNATIVE_MULTIPART = False
    1806 
    1807                 for part in msg.walk():
    1808                         content_maintype = part.get_content_maintype()
    1809                         content_type =  part.get_content_type()
    1810 
    1811                         self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
    1812                         self.logger.debug('\t Message part: Content-Type: %s' % content_type)
    1813 
    1814                         ## Check content type
    1815                         #
    1816                         if content_type in self.STRIP_CONTENT_TYPES:
    1817                                 self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
    1818                                 continue
    1819 
    1820                         ## Catch some mulitpart execptions
    1821                         #
    1822                         if content_type == 'multipart/alternative':
    1823                                 ALTERNATIVE_MULTIPART = True
    1824                                 continue
    1825 
    1826                         ## Skip multipart containers
    1827                         #
    1828                         if content_maintype == 'multipart':
    1829                                 self.logger.debug("\t Skipping multipart container")
    1830                                 continue
    1831                        
    1832                         ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
    1833                         #
    1834                         inline = self.inline_part(part)
    1835 
    1836                         ## Drop HTML message
    1837                         #
    1838                         if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
    1839                                 if content_type == 'text/html':
    1840                                         self.logger.debug('\t Skipping alternative HTML message')
    1841                                         ALTERNATIVE_MULTIPART = False
    1842                                         continue
    1843 
    1844 
    1845                         ## Save all non plain text message as attachment
    1846                         #
    1847                         if not content_type in ['text/plain']:
    1848 
    1849                                 if self.parameters.debug:
    1850                                         s = '\t Filename: %s' % part.get_filename()
    1851                                         self.print_unicode(s)
    1852 
    1853                                 ## First try to use email header function to convert filename.
    1854                                 #  If this fails the use the plain filename
    1855                                 #
    1856                                 try:
    1857                                         filename = self.email_to_unicode(part.get_filename())
    1858                                 except UnicodeEncodeError, detail:
    1859                                         filename = part.get_filename()
    1860 
    1861                                 message_parts.append((filename, part))
    1862 
    1863                                 ## We van only convert html messages
    1864                                 #
    1865                                 if not content_type == 'text/html':
    1866                                         self.logger.debug('\t Appending content_type = %s' %(content_type))
    1867                                         continue
    1868 
    1869                         if not inline:
    1870                                         self.logger.debug('\t Skipping %s, not an inline messsage part' %(content_type))
    1871                                         continue
    1872                                
    1873                         ## Try to decode message part. We have a html or plain text messafe
    1874                         #
    1875                         body_text = part.get_payload(decode=1)
    1876                         if not body_text:                       
    1877                                 body_text = part.get_payload(decode=0)
    1878 
    1879                         ## Try to convert html message
    1880                         #
    1881                         if content_type == 'text/html':
    1882 
    1883                                 body_text = self.html_2_txt(body_text)
    1884                                 if not body_text:
    1885                                         continue
    1886 
    1887                         format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
    1888                         delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
    1889 
    1890                         if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
    1891                                 body_text = self.reflow(body_text, delsp == 'yes')
    1892        
    1893                         if self.parameters.strip_signature:
    1894                                 body_text = self.strip_signature(body_text)
    1895 
    1896                         if self.parameters.strip_quotes:
    1897                                 body_text = self.strip_quotes(body_text)
    1898 
    1899                         if self.parameters.inline_properties:
    1900                                 body_text = self.inline_properties(body_text)
    1901 
    1902                         if self.parameters.use_textwrap:
    1903                                 body_text = self.wrap_text(body_text)
    1904 
    1905                         ## Get contents charset (iso-8859-15 if not defined in mail headers)
    1906                         #
    1907                         charset = part.get_content_charset()
    1908                         if not charset:
    1909                                 charset = 'iso-8859-15'
    1910 
    1911                         try:
    1912                                 ubody_text = unicode(body_text, charset)
    1913 
    1914                         except UnicodeError, detail:
    1915                                 ubody_text = unicode(body_text, 'iso-8859-15')
    1916 
    1917                         except LookupError, detail:
    1918                                 ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
    1919 
    1920                         if self.parameters.verbatim_format:
    1921                                 message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
    1922                         else:
    1923                                 message_parts.append('%s' %ubody_text)
    1924 
    1925                 return message_parts
    1926                
    1927         def unique_attachment_names(self, message_parts):
    1928                 """
    1929             Make sure we have unique names attachments:
    1930                   - check if it contains illegal characters
    1931                   - Rename "None" filenames to "untitled-part"
    1932                 """
    1933                 self.logger.debug('function unique_attachment_names()')
    1934                 renamed_parts = []
    1935                 attachment_names = set()
    1936 
    1937                 for item in message_parts:
    1938                        
    1939                         ## If not an attachment, leave it alone
    1940                         #
    1941                         if not isinstance(item, tuple):
    1942                                 renamed_parts.append(item)
    1943                                 continue
    1944                                
    1945                         (filename, part) = item
    1946 
    1947                         ## If filename = None, use a default one
    1948                         #
    1949                         if filename in [ 'None']:
    1950                                 filename = 'untitled-part'
    1951                                 self.logger.info('Rename filename "None" to: %s' %filename)
    1952 
    1953                                 ## Guess the extension from the content type, use non strict mode
    1954                                 #  some additional non-standard but commonly used MIME types
    1955                                 #  are also recognized
    1956                                 #
    1957                                 ext = mimetypes.guess_extension(part.get_content_type(), False)
    1958                                 if not ext:
    1959                                         ext = '.bin'
    1960 
    1961                                 filename = '%s%s' % (filename, ext)
    1962 
    1963                         ## Discard relative paths for windows/unix in attachment names
    1964                         #
    1965                         #filename = filename.replace('\\', '/').replace(':', '/')
    1966                         filename = filename.replace('\\', '_')
    1967                         filename = filename.replace('/', '_')
    1968 
    1969                         ## remove linefeed char
    1970                         #
    1971                         for forbidden_char in ['\r', '\n']:
    1972                                 filename = filename.replace(forbidden_char,'')
    1973 
    1974                         ## We try to normalize the filename to utf-8 NFC if we can.
    1975                         #  Files uploaded from OS X might be in NFD.
    1976                         #  Check python version and then try it
    1977                         #
    1978                         #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
    1979                         #       try:
    1980                         #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
    1981                         #       except TypeError:
    1982                         #               pass
    1983 
    1984                         ## Make the filename unique for this ticket
    1985                         #
    1986                         num = 0
    1987                         unique_filename = filename
    1988                         dummy_filename, ext = os.path.splitext(filename)
    1989 
    1990                         while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
    1991                                 num += 1
    1992                                 unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
    1993                                
    1994                         if self.parameters.debug:
    1995                                 s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename)
    1996                                 self.print_unicode(s)
    1997 
    1998                         attachment_names.add(unique_filename)
    1999 
    2000                         renamed_parts.append((filename, unique_filename, part))
    2001        
    2002                 return renamed_parts
    2003                        
    2004                        
    2005         def attachment_exists(self, filename):
    2006 
    2007                 if self.parameters.debug:
    2008                         s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
    2009                         self.print_unicode(s)
    2010 
    2011                 ## We have no valid ticket id
    2012                 #
    2013                 if not self.id:
    2014                         return False
    2015 
    2016                 try:
    2017                         if self.system == 'discussion':
    2018                                 att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
    2019                                   % (self.id,), filename)
    2020                         else:
    2021                                 att = attachment.Attachment(self.env, 'ticket', self.id,
    2022                                   filename)
    2023                         return True
    2024                 except attachment.ResourceNotFound:
    2025                         return False
     1789    def inline_part(self, part):
     1790        """
     1791        """
     1792        self.logger.debug('function inline_part()')
     1793
     1794        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
     1795
     1796    def get_message_parts(self, msg):
     1797        """
     1798        parses the email message and returns a list of body parts and attachments
     1799        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
     1800        """
     1801        self.logger.debug('function get_message_parts()')
     1802
     1803        message_parts = list()
     1804   
     1805        ALTERNATIVE_MULTIPART = False
     1806
     1807        for part in msg.walk():
     1808            content_maintype = part.get_content_maintype()
     1809            content_type =  part.get_content_type()
     1810
     1811            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
     1812            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
     1813
     1814            ## Check content type
     1815            #
     1816            if content_type in self.STRIP_CONTENT_TYPES:
     1817                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
     1818                continue
     1819
     1820            ## Catch some mulitpart execptions
     1821            #
     1822            if content_type == 'multipart/alternative':
     1823                ALTERNATIVE_MULTIPART = True
     1824                continue
     1825
     1826            ## Skip multipart containers
     1827            #
     1828            if content_maintype == 'multipart':
     1829                self.logger.debug("\t Skipping multipart container")
     1830                continue
     1831           
     1832            ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
     1833            #
     1834            inline = self.inline_part(part)
     1835
     1836            ## Drop HTML message
     1837            #
     1838            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
     1839                if content_type == 'text/html':
     1840                    self.logger.debug('\t Skipping alternative HTML message')
     1841                    ALTERNATIVE_MULTIPART = False
     1842                    continue
     1843
     1844
     1845            ## Save all non plain text message as attachment
     1846            #
     1847            if not content_type in ['text/plain']:
     1848
     1849                if self.parameters.debug:
     1850                    s = '\t Filename: %s' % part.get_filename()
     1851                    self.print_unicode(s)
     1852
     1853                ## First try to use email header function to convert filename.
     1854                #  If this fails the use the plain filename
     1855                #
     1856                try:
     1857                    filename = self.email_to_unicode(part.get_filename())
     1858                except UnicodeEncodeError, detail:
     1859                    filename = part.get_filename()
     1860
     1861                message_parts.append((filename, part))
     1862
     1863                ## We van only convert html messages
     1864                #
     1865                if not content_type == 'text/html':
     1866                    self.logger.debug('\t Appending content_type = %s' %(content_type))
     1867                    continue
     1868
     1869            if not inline:
     1870                    self.logger.debug('\t Skipping %s, not an inline messsage part' %(content_type))
     1871                    continue
     1872               
     1873            ## Try to decode message part. We have a html or plain text messafe
     1874            #
     1875            body_text = part.get_payload(decode=1)
     1876            if not body_text:           
     1877                body_text = part.get_payload(decode=0)
     1878
     1879            ## Try to convert html message
     1880            #
     1881            if content_type == 'text/html':
     1882
     1883                body_text = self.html_2_txt(body_text)
     1884                if not body_text:
     1885                    continue
     1886
     1887            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
     1888            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
     1889
     1890            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
     1891                body_text = self.reflow(body_text, delsp == 'yes')
     1892   
     1893            if self.parameters.strip_signature:
     1894                body_text = self.strip_signature(body_text)
     1895
     1896            if self.parameters.strip_quotes:
     1897                body_text = self.strip_quotes(body_text)
     1898
     1899            if self.parameters.inline_properties:
     1900                body_text = self.inline_properties(body_text)
     1901
     1902            if self.parameters.use_textwrap:
     1903                body_text = self.wrap_text(body_text)
     1904
     1905            ## Get contents charset (iso-8859-15 if not defined in mail headers)
     1906            #
     1907            charset = part.get_content_charset()
     1908            if not charset:
     1909                charset = 'iso-8859-15'
     1910
     1911            try:
     1912                ubody_text = unicode(body_text, charset)
     1913
     1914            except UnicodeError, detail:
     1915                ubody_text = unicode(body_text, 'iso-8859-15')
     1916
     1917            except LookupError, detail:
     1918                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
     1919
     1920            if self.parameters.verbatim_format:
     1921                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
     1922            else:
     1923                message_parts.append('%s' %ubody_text)
     1924
     1925        return message_parts
     1926       
     1927    def unique_attachment_names(self, message_parts):
     1928        """
     1929        Make sure we have unique names attachments:
     1930          - check if it contains illegal characters
     1931          - Rename "None" filenames to "untitled-part"
     1932        """
     1933        self.logger.debug('function unique_attachment_names()')
     1934        renamed_parts = []
     1935        attachment_names = set()
     1936
     1937        for item in message_parts:
     1938           
     1939            ## If not an attachment, leave it alone
     1940            #
     1941            if not isinstance(item, tuple):
     1942                renamed_parts.append(item)
     1943                continue
     1944               
     1945            (filename, part) = item
     1946
     1947            ## If filename = None, use a default one
     1948            #
     1949            if filename in [ 'None']:
     1950                filename = 'untitled-part'
     1951                self.logger.info('Rename filename "None" to: %s' %filename)
     1952
     1953                ## Guess the extension from the content type, use non strict mode
     1954                #  some additional non-standard but commonly used MIME types
     1955                #  are also recognized
     1956                #
     1957                ext = mimetypes.guess_extension(part.get_content_type(), False)
     1958                if not ext:
     1959                    ext = '.bin'
     1960
     1961                filename = '%s%s' % (filename, ext)
     1962
     1963            ## Discard relative paths for windows/unix in attachment names
     1964            #
     1965            #filename = filename.replace('\\', '/').replace(':', '/')
     1966            filename = filename.replace('\\', '_')
     1967            filename = filename.replace('/', '_')
     1968
     1969            ## remove linefeed char
     1970            #
     1971            for forbidden_char in ['\r', '\n']:
     1972                filename = filename.replace(forbidden_char,'')
     1973
     1974            ## We try to normalize the filename to utf-8 NFC if we can.
     1975            #  Files uploaded from OS X might be in NFD.
     1976            #  Check python version and then try it
     1977            #
     1978            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
     1979            #   try:
     1980            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
     1981            #   except TypeError:
     1982            #       pass
     1983
     1984            ## Make the filename unique for this ticket
     1985            #
     1986            num = 0
     1987            unique_filename = filename
     1988            dummy_filename, ext = os.path.splitext(filename)
     1989
     1990            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
     1991                num += 1
     1992                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
     1993               
     1994            if self.parameters.debug:
     1995                s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename)
     1996                self.print_unicode(s)
     1997
     1998            attachment_names.add(unique_filename)
     1999
     2000            renamed_parts.append((filename, unique_filename, part))
     2001   
     2002        return renamed_parts
     2003           
     2004           
     2005    def attachment_exists(self, filename):
     2006
     2007        if self.parameters.debug:
     2008            s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
     2009            self.print_unicode(s)
     2010
     2011        ## We have no valid ticket id
     2012        #
     2013        if not self.id:
     2014            return False
     2015
     2016        try:
     2017            if self.system == 'discussion':
     2018                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
     2019                  % (self.id,), filename)
     2020            else:
     2021                att = attachment.Attachment(self.env, 'ticket', self.id,
     2022                  filename)
     2023            return True
     2024        except attachment.ResourceNotFound:
     2025            return False
    20262026
    20272027########## TRAC Ticket Text ###########################################################
    2028                        
    2029         def get_body_text(self, message_parts):
    2030                 """
    2031                 """
    2032                 self.logger.debug('function get_body_text()')
    2033 
    2034                 body_text = []
    2035                
    2036                 for part in message_parts:
    2037                
    2038                         ## Plain text part, append it
    2039                         #
    2040                         if not isinstance(part, tuple):
    2041                                 body_text.extend(part.strip().splitlines())
    2042                                 body_text.append("")
    2043                                 continue
    2044 
    2045                         (original, filename, part) = part
    2046                         inline = self.inline_part(part)
    2047 
    2048                         ## Skip generation of attachment link if html is converted to text
    2049                         #
    2050                         if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
    2051                                 s = 'Skipping attachment link for html part: %s' %(filename)
    2052                                 self.print_unicode(s)
    2053                                 continue
    2054                        
    2055                         if part.get_content_maintype() == 'image' and inline:
    2056                                 if self.system != 'discussion':
    2057                                         s = 'wiki image link for: %s' %(filename)
    2058                                         self.print_unicode(s)
    2059                                         body_text.append('[[Image(%s)]]' % filename)
    2060                                 body_text.append("")
    2061                         else:
    2062                                 if self.system != 'discussion':
    2063                                         s = 'wiki attachment link for: %s' %(filename)
    2064                                         self.print_unicode(s)
    2065                                         body_text.append('[attachment:"%s"]' % filename)
    2066                                 body_text.append("")
    2067 
    2068                 ## Convert list body_texts to string
    2069                 #
    2070                 body_text = '\r\n'.join(body_text)
    2071                 return body_text
    2072 
    2073         def html_mailto_link(self, subject):
    2074                 """
    2075                 This function returns a HTML mailto tag with the ticket id and author email address
    2076                 """
    2077                 self.logger.debug("function html_mailto_link")
    2078                 if not self.author:
    2079                         author = self.email_addr
    2080                 else:   
    2081                         author = self.author
    2082 
    2083                 if not self.parameters.mailto_cc:
    2084                         self.parameters.mailto_cc = ''
    2085 
    2086                 ## use urllib to escape the chars
    2087                 #
    2088                 s = '%s?Subject=%s&Cc=%s' %(
    2089                        urllib.quote(self.email_addr),
    2090                            urllib.quote('Re: #%s: %s' %(self.id, subject)),
    2091                            urllib.quote(self.parameters.mailto_cc)
    2092                            )
    2093 
    2094                 if self.VERSION in [ 0.10 ]:
    2095                         s = '\r\n{{{\r\n#!html\r\n<a\r\n href="mailto:%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
    2096                 else:
    2097                         s = '[mailto:"%s" Reply to: %s]' %(s, author)
    2098 
    2099                 self.logger.debug("\tmailto link %s" %s)
    2100                 return s
     2028           
     2029    def get_body_text(self, message_parts):
     2030        """
     2031        """
     2032        self.logger.debug('function get_body_text()')
     2033
     2034        body_text = []
     2035       
     2036        for part in message_parts:
     2037       
     2038            ## Plain text part, append it
     2039            #
     2040            if not isinstance(part, tuple):
     2041                body_text.extend(part.strip().splitlines())
     2042                body_text.append("")
     2043                continue
     2044
     2045            (original, filename, part) = part
     2046            inline = self.inline_part(part)
     2047
     2048            ## Skip generation of attachment link if html is converted to text
     2049            #
     2050            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
     2051                s = 'Skipping attachment link for html part: %s' %(filename)
     2052                self.print_unicode(s)
     2053                continue
     2054           
     2055            if part.get_content_maintype() == 'image' and inline:
     2056                if self.system != 'discussion':
     2057                    s = 'wiki image link for: %s' %(filename)
     2058                    self.print_unicode(s)
     2059                    body_text.append('[[Image(%s)]]' % filename)
     2060                body_text.append("")
     2061            else:
     2062                if self.system != 'discussion':
     2063                    s = 'wiki attachment link for: %s' %(filename)
     2064                    self.print_unicode(s)
     2065                    body_text.append('[attachment:"%s"]' % filename)
     2066                body_text.append("")
     2067
     2068        ## Convert list body_texts to string
     2069        #
     2070        body_text = '\r\n'.join(body_text)
     2071        return body_text
     2072
     2073    def html_mailto_link(self, subject):
     2074        """
     2075        This function returns a HTML mailto tag with the ticket id and author email address
     2076        """
     2077        self.logger.debug("function html_mailto_link")
     2078        if not self.author:
     2079            author = self.email_addr
     2080        else:   
     2081            author = self.author
     2082
     2083        if not self.parameters.mailto_cc:
     2084            self.parameters.mailto_cc = ''
     2085
     2086        ## use urllib to escape the chars
     2087        #
     2088        s = '%s?Subject=%s&Cc=%s' %(
     2089               urllib.quote(self.email_addr),
     2090               urllib.quote('Re: #%s: %s' %(self.id, subject)),
     2091               urllib.quote(self.parameters.mailto_cc)
     2092               )
     2093
     2094        if self.VERSION in [ 0.10 ]:
     2095            s = '\r\n{{{\r\n#!html\r\n<a\r\n href="mailto:%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
     2096        else:
     2097            s = '[mailto:"%s" Reply to: %s]' %(s, author)
     2098
     2099        self.logger.debug("\tmailto link %s" %s)
     2100        return s
    21012101
    21022102########## TRAC notify section ###########################################################
    21032103
    2104         def notify(self, tkt, new=True, modtime=0):
    2105                 """
    2106                 A wrapper for the TRAC notify function. So we can use templates
    2107                 """
    2108                 self.logger.debug('function notify()')
    2109 
    2110                 if self.parameters.dry_run:
    2111                                 print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
    2112                                 return
    2113                 try:
    2114 
    2115                         #from trac.ticket.web_ui import TicketModule
    2116                         #from trac.ticket.notification import TicketNotificationSystem
    2117                         #ticket_sys = TicketNotificationSystem(self.env)
    2118                         #a = TicketModule(self.env)
    2119                         #print a.__dict__
    2120                         #tn_sys = TicketNotificationSystem(self.env)
    2121                         #print tn_sys
    2122                         #print tn_sys.__dict__
    2123                         #sys.exit(0)
    2124 
    2125                         ## create false {abs_}href properties, to trick Notify()
    2126                         #
    2127                         if not (self.VERSION in [0.11, 0.12]):
    2128                                 self.env.abs_href = Href(self.get_config('project', 'url'))
    2129                                 self.env.href = Href(self.get_config('project', 'url'))
    2130 
    2131 
    2132                         tn = TicketNotifyEmail(self.env)
    2133 
    2134                         if self.parameters.alternate_notify_template:
    2135 
    2136                                 if self.VERSION >= 0.11:
    2137 
    2138                                         from trac.web.chrome import Chrome
    2139 
    2140                                         if  self.parameters.alternate_notify_template_update and not new:
    2141                                                 tn.template_name = self.parameters.alternate_notify_template_update
    2142                                         else:
    2143                                                 tn.template_name = self.parameters.alternate_notify_template
    2144 
    2145                                         tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
    2146                                                
    2147                                 else:
    2148 
    2149                                         tn.template_name = self.parameters.alternate_notify_template
    2150 
    2151                         tn.notify(tkt, new, modtime)
    2152 
    2153                 except Exception, e:
    2154                         self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
     2104    def notify(self, tkt, new=True, modtime=0):
     2105        """
     2106        A wrapper for the TRAC notify function. So we can use templates
     2107        """
     2108        self.logger.debug('function notify()')
     2109
     2110        if self.parameters.dry_run:
     2111                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
     2112                return
     2113        try:
     2114
     2115            #from trac.ticket.web_ui import TicketModule
     2116            #from trac.ticket.notification import TicketNotificationSystem
     2117            #ticket_sys = TicketNotificationSystem(self.env)
     2118            #a = TicketModule(self.env)
     2119            #print a.__dict__
     2120            #tn_sys = TicketNotificationSystem(self.env)
     2121            #print tn_sys
     2122            #print tn_sys.__dict__
     2123            #sys.exit(0)
     2124
     2125            ## create false {abs_}href properties, to trick Notify()
     2126            #
     2127            if not (self.VERSION in [0.11, 0.12]):
     2128                self.env.abs_href = Href(self.get_config('project', 'url'))
     2129                self.env.href = Href(self.get_config('project', 'url'))
     2130
     2131
     2132            tn = TicketNotifyEmail(self.env)
     2133
     2134            if self.parameters.alternate_notify_template:
     2135
     2136                if self.VERSION >= 0.11:
     2137
     2138                    from trac.web.chrome import Chrome
     2139
     2140                    if  self.parameters.alternate_notify_template_update and not new:
     2141                        tn.template_name = self.parameters.alternate_notify_template_update
     2142                    else:
     2143                        tn.template_name = self.parameters.alternate_notify_template
     2144
     2145                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
     2146                       
     2147                else:
     2148
     2149                    tn.template_name = self.parameters.alternate_notify_template
     2150
     2151            tn.notify(tkt, new, modtime)
     2152
     2153        except Exception, e:
     2154            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
    21552155
    21562156
     
    21592159
    21602160def ReadConfig(file, name):
    2161         """
    2162         Parse the config file
    2163         """
    2164         if not os.path.isfile(file):
    2165                 print 'File %s does not exist' %file
    2166                 sys.exit(1)
    2167 
    2168         config = trac_config.Configuration(file)
    2169        
    2170         parentdir = config.get('DEFAULT', 'parentdir')
    2171         sections = config.sections()
    2172 
    2173         ## use some trac internals to get the defaults
    2174         #
    2175         tmp = config.parser.defaults()
    2176         project =  SaraDict()
    2177 
    2178         for option, value in tmp.items():
    2179                 try:
    2180                         project[option] = int(value)
    2181                 except ValueError:
    2182                         project[option] = value
    2183 
    2184         if name:
    2185                 if name in sections:
    2186                         project =  SaraDict()
    2187                         for option, value in  config.options(name):
    2188                                 try:
    2189                                         project[option] = int(value)
    2190                                 except ValueError:
    2191                                         project[option] = value
    2192 
    2193                 elif not parentdir:
    2194                         print "Not a valid project name: %s, valid names are: %s" %(name, sections)
    2195                         print "or set parentdir in the [DEFAULT] section"
    2196                         sys.exit(1)
    2197 
    2198         ## If parentdir then set project dir to parentdir + name
    2199         #
    2200         if not project.has_key('project'):
    2201                 if not parentdir:
    2202                         print "You must set project or parentdir in your configuration file"
    2203                         sys.exit(1)
    2204                 elif not name:
    2205                         print "You must configure a  project section in your configuration file"
    2206                 else:
    2207                         project['project'] = os.path.join(parentdir, name)
    2208 
    2209         return project
     2161    """
     2162    Parse the config file
     2163    """
     2164    if not os.path.isfile(file):
     2165        print 'File %s does not exist' %file
     2166        sys.exit(1)
     2167
     2168    config = trac_config.Configuration(file)
     2169   
     2170    parentdir = config.get('DEFAULT', 'parentdir')
     2171    sections = config.sections()
     2172
     2173    ## use some trac internals to get the defaults
     2174    #
     2175    tmp = config.parser.defaults()
     2176    project =  SaraDict()
     2177
     2178    for option, value in tmp.items():
     2179        try:
     2180            project[option] = int(value)
     2181        except ValueError:
     2182            project[option] = value
     2183
     2184    if name:
     2185        if name in sections:
     2186            project =  SaraDict()
     2187            for option, value in  config.options(name):
     2188                try:
     2189                    project[option] = int(value)
     2190                except ValueError:
     2191                    project[option] = value
     2192
     2193        elif not parentdir:
     2194            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
     2195            print "or set parentdir in the [DEFAULT] section"
     2196            sys.exit(1)
     2197
     2198    ## If parentdir then set project dir to parentdir + name
     2199    #
     2200    if not project.has_key('project'):
     2201        if not parentdir:
     2202            print "You must set project or parentdir in your configuration file"
     2203            sys.exit(1)
     2204        elif not name:
     2205            print "You must configure a  project section in your configuration file"
     2206        else:
     2207            project['project'] = os.path.join(parentdir, name)
     2208
     2209    return project
    22102210
    22112211########## Setup Logging ###############################################################
    22122212
    22132213def setup_log(parameters, project_name, interactive=None):
    2214         """
    2215         Setup loging
    2216 
    2217         Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
     2214    """
     2215    Setup loging
     2216
     2217    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
    22182218    would be interpreted by the ConfigParser itself.
    2219         """
    2220         logger = logging.getLogger('email2trac %s' %project_name)
    2221 
    2222         if interactive:
    2223                 parameters.log_type = 'stderr'
    2224 
    2225         if not parameters.log_type:
    2226                 if sys.platform in ['win32', 'cygwin']:
    2227                         paramters.log_type = 'eventlog'
    2228                 else:
    2229                         parameters.log_type = 'syslog'
    2230 
    2231         if parameters.log_type == 'file':
    2232 
    2233                 if not parameters.log_file:
    2234                         parameters.log_file = 'email2trac.log'
    2235 
    2236                 if not os.path.isabs(parameters.log_file):
    2237                         parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
    2238 
    2239                 log_handler = logging.FileHandler(parameters.log_file)
    2240 
    2241         elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
    2242                 ## Requires win32 extensions
    2243                 #
    2244                 log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
    2245 
    2246         elif parameters.log_type in ('syslog', 'unix'):
    2247                 log_handler = logging.handlers.SysLogHandler('/dev/log')
    2248 
    2249         elif parameters.log_type in ('stderr'):
    2250                 log_handler = logging.StreamHandler(sys.stderr)
    2251 
    2252         else:
    2253                 log_handler = logging.handlers.BufferingHandler(0)
    2254 
    2255         if parameters.log_format:
    2256                 parameters.log_format = parameters.log_format.replace('$(', '%(')
    2257         else:
    2258                 parameters.log_format = '%(name)s: %(message)s'
    2259 
    2260         log_formatter = logging.Formatter(parameters.log_format)
    2261         log_handler.setFormatter(log_formatter)
    2262         logger.addHandler(log_handler)
    2263 
    2264         if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
    2265                 logger.setLevel(logging.DEBUG)
    2266                 parameters.debug = 1
    2267 
    2268         elif parameters.log_level in ['INFO'] or parameters.verbose:
    2269                 logger.setLevel(logging.INFO)
    2270 
    2271         elif parameters.log_level in ['WARNING']:
    2272                 logger.setLevel(logging.WARNING)
    2273 
    2274         elif parameters.log_level in ['ERROR']:
    2275                 logger.setLevel(logging.ERROR)
    2276 
    2277         elif parameters.log_level in ['CRITICAL']:
    2278                 logger.setLevel(logging.CRITICAL)
    2279 
    2280         else:
    2281                 logger.setLevel(logging.INFO)
    2282 
    2283         return logger
     2219    """
     2220    logger = logging.getLogger('email2trac %s' %project_name)
     2221
     2222    if interactive:
     2223        parameters.log_type = 'stderr'
     2224
     2225    if not parameters.log_type:
     2226        if sys.platform in ['win32', 'cygwin']:
     2227            paramters.log_type = 'eventlog'
     2228        else:
     2229            parameters.log_type = 'syslog'
     2230
     2231    if parameters.log_type == 'file':
     2232
     2233        if not parameters.log_file:
     2234            parameters.log_file = 'email2trac.log'
     2235
     2236        if not os.path.isabs(parameters.log_file):
     2237            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
     2238
     2239        log_handler = logging.FileHandler(parameters.log_file)
     2240
     2241    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
     2242        ## Requires win32 extensions
     2243        #
     2244        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
     2245
     2246    elif parameters.log_type in ('syslog', 'unix'):
     2247        log_handler = logging.handlers.SysLogHandler('/dev/log')
     2248
     2249    elif parameters.log_type in ('stderr'):
     2250        log_handler = logging.StreamHandler(sys.stderr)
     2251
     2252    else:
     2253        log_handler = logging.handlers.BufferingHandler(0)
     2254
     2255    if parameters.log_format:
     2256        parameters.log_format = parameters.log_format.replace('$(', '%(')
     2257    else:
     2258        parameters.log_format = '%(name)s: %(message)s'
     2259
     2260    log_formatter = logging.Formatter(parameters.log_format)
     2261    log_handler.setFormatter(log_formatter)
     2262    logger.addHandler(log_handler)
     2263
     2264    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
     2265        logger.setLevel(logging.DEBUG)
     2266        parameters.debug = 1
     2267
     2268    elif parameters.log_level in ['INFO'] or parameters.verbose:
     2269        logger.setLevel(logging.INFO)
     2270
     2271    elif parameters.log_level in ['WARNING']:
     2272        logger.setLevel(logging.WARNING)
     2273
     2274    elif parameters.log_level in ['ERROR']:
     2275        logger.setLevel(logging.ERROR)
     2276
     2277    elif parameters.log_level in ['CRITICAL']:
     2278        logger.setLevel(logging.CRITICAL)
     2279
     2280    else:
     2281        logger.setLevel(logging.INFO)
     2282
     2283    return logger
    22842284
    22852285
    22862286if __name__ == '__main__':
    2287         ## Default config file
    2288         #
    2289         configfile = '@email2trac_conf@'
    2290         project = ''
    2291         component = ''
    2292         ticket_prefix = 'default'
    2293         dry_run = None
    2294         verbose = None
    2295         debug_interactive = None
    2296 
    2297         SHORT_OPT = 'cdhf:np:t:v'
    2298         LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
    2299 
    2300         try:
    2301                 opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
    2302         except getopt.error,detail:
    2303                 print __doc__
    2304                 print detail
    2305                 sys.exit(1)
    2306        
    2307         project_name = None
    2308         for opt,value in opts:
    2309                 if opt in [ '-h', '--help']:
    2310                         print __doc__
    2311                         sys.exit(0)
    2312                 elif opt in ['-c', '--component']:
    2313                         component = value
    2314                 elif opt in ['-d', '--debug']:
    2315                         debug_interactive = 1
    2316                 elif opt in ['-f', '--file']:
    2317                         configfile = value
    2318                 elif opt in ['-n', '--dry-run']:
    2319                         dry_run = True
    2320                 elif opt in ['-p', '--project']:
    2321                         project_name = value
    2322                 elif opt in ['-t', '--ticket_prefix']:
    2323                         ticket_prefix = value
    2324                 elif opt in ['-v', '--verbose']:
    2325                         verbose = True
    2326        
    2327         settings = ReadConfig(configfile, project_name)
    2328 
    2329         ## The default prefix for ticket values in email2trac.conf
    2330         #
    2331         settings.ticket_prefix = ticket_prefix
    2332         settings.dry_run = dry_run
    2333         settings.verbose = verbose
    2334 
    2335         if not settings.debug and debug_interactive:
    2336                 settings.debug = debug_interactive
    2337 
    2338         if not settings.project:
    2339                 print __doc__
    2340                 print 'No Trac project is defined in the email2trac config file.'
    2341                 sys.exit(1)
    2342 
    2343         logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
    2344        
    2345         if component:
    2346                 settings['component'] = component
    2347 
    2348         ## Determine major trac version used to be in email2trac.conf
    2349         # Quick hack for 0.12
    2350         #
    2351         version = '0.%s' %(trac_version.split('.')[1])
    2352         if version.startswith('0.12'):
    2353                 version = '0.12'
    2354 
    2355         logger.debug("Found trac version: %s" %(version))
    2356        
    2357         try:
    2358                 if version == '0.10':
    2359                         from trac import attachment
    2360                         from trac.env import Environment
    2361                         from trac.ticket import Ticket
    2362                         from trac.web.href import Href
    2363                         from trac import util
    2364                         from trac.ticket.web_ui import TicketModule
    2365 
    2366                         #
    2367                         # return  util.text.to_unicode(str)
    2368                         #
    2369                         # see http://projects.edgewall.com/trac/changeset/2799
    2370                         from trac.ticket.notification import TicketNotifyEmail
    2371                         from trac import config as trac_config
    2372                         from trac.core import TracError
    2373 
    2374                 elif version == '0.11':
    2375                         from trac import attachment
    2376                         from trac.env import Environment
    2377                         from trac.ticket import Ticket
    2378                         from trac.web.href import Href
    2379                         from trac import config as trac_config
    2380                         from trac import util
    2381                         from trac.core import TracError
    2382                         from trac.perm import PermissionSystem
    2383                         from trac.ticket.web_ui import TicketModule
    2384 
    2385                         #
    2386                         # return  util.text.to_unicode(str)
    2387                         #
    2388                         # see http://projects.edgewall.com/trac/changeset/2799
    2389                         from trac.ticket.notification import TicketNotifyEmail
    2390 
    2391                 elif version == '0.12':
    2392                         from trac import attachment
    2393                         from trac.env import Environment
    2394                         from trac.ticket import Ticket
    2395                         from trac.web.href import Href
    2396                         from trac import config as trac_config
    2397                         from trac import util
    2398                         from trac.core import TracError
    2399                         from trac.perm import PermissionSystem
    2400                         from trac.ticket.web_ui import TicketModule
    2401 
    2402                         #
    2403                         # return  util.text.to_unicode(str)
    2404                         #
    2405                         # see http://projects.edgewall.com/trac/changeset/2799
    2406                         from trac.ticket.notification import TicketNotifyEmail
    2407 
    2408 
    2409                 else:
    2410                         logger.error('TRAC version %s is not supported' %version)
    2411                         sys.exit(1)
    2412 
    2413                 ## Must be set before environment is created
    2414                 #
    2415                 if settings.has_key('python_egg_cache'):
    2416                         python_egg_cache = str(settings['python_egg_cache'])
    2417                         os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
    2418 
    2419                 if settings.debug > 0:
    2420                         logger.debug('Loading environment %s', settings.project)
    2421 
    2422                 try:
    2423                         env = Environment(settings['project'], create=0)
    2424                 except IOError, detail:
    2425                         print "Trac project does not exists: %s" %(settings['project'])
    2426                         sys.exit(1)
    2427 
    2428                 tktparser = TicketEmailParser(env, settings, logger, float(version))
    2429                 tktparser.parse(sys.stdin)
    2430 
    2431         ## Catch all errors and use the logging module
    2432         #
    2433         except Exception, error:
    2434 
    2435                 etype, evalue, etb = sys.exc_info()
    2436                 for e in traceback.format_exception(etype, evalue, etb):
    2437                         logger.critical(e)
    2438 
    2439                 if m:
    2440                         tktparser.save_email_for_debug(m, True)
    2441 
    2442                 sys.exit(1)
     2287    ## Default config file
     2288    #
     2289    configfile = '@email2trac_conf@'
     2290    project = ''
     2291    component = ''
     2292    ticket_prefix = 'default'
     2293    dry_run = None
     2294    verbose = None
     2295    debug_interactive = None
     2296
     2297    SHORT_OPT = 'cdhf:np:t:v'
     2298    LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
     2299
     2300    try:
     2301        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
     2302    except getopt.error,detail:
     2303        print __doc__
     2304        print detail
     2305        sys.exit(1)
     2306   
     2307    project_name = None
     2308    for opt,value in opts:
     2309        if opt in [ '-h', '--help']:
     2310            print __doc__
     2311            sys.exit(0)
     2312        elif opt in ['-c', '--component']:
     2313            component = value
     2314        elif opt in ['-d', '--debug']:
     2315            debug_interactive = 1
     2316        elif opt in ['-f', '--file']:
     2317            configfile = value
     2318        elif opt in ['-n', '--dry-run']:
     2319            dry_run = True
     2320        elif opt in ['-p', '--project']:
     2321            project_name = value
     2322        elif opt in ['-t', '--ticket_prefix']:
     2323            ticket_prefix = value
     2324        elif opt in ['-v', '--verbose']:
     2325            verbose = True
     2326   
     2327    settings = ReadConfig(configfile, project_name)
     2328
     2329    ## The default prefix for ticket values in email2trac.conf
     2330    #
     2331    settings.ticket_prefix = ticket_prefix
     2332    settings.dry_run = dry_run
     2333    settings.verbose = verbose
     2334
     2335    if not settings.debug and debug_interactive:
     2336        settings.debug = debug_interactive
     2337
     2338    if not settings.project:
     2339        print __doc__
     2340        print 'No Trac project is defined in the email2trac config file.'
     2341        sys.exit(1)
     2342
     2343    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
     2344   
     2345    if component:
     2346        settings['component'] = component
     2347
     2348    ## Determine major trac version used to be in email2trac.conf
     2349    # Quick hack for 0.12
     2350    #
     2351    version = '0.%s' %(trac_version.split('.')[1])
     2352    if version.startswith('0.12'):
     2353        version = '0.12'
     2354
     2355    logger.debug("Found trac version: %s" %(version))
     2356   
     2357    try:
     2358        if version == '0.10':
     2359            from trac import attachment
     2360            from trac.env import Environment
     2361            from trac.ticket import Ticket
     2362            from trac.web.href import Href
     2363            from trac import util
     2364            from trac.ticket.web_ui import TicketModule
     2365
     2366            #
     2367            # return  util.text.to_unicode(str)
     2368            #
     2369            # see http://projects.edgewall.com/trac/changeset/2799
     2370            from trac.ticket.notification import TicketNotifyEmail
     2371            from trac import config as trac_config
     2372            from trac.core import TracError
     2373
     2374        elif version == '0.11':
     2375            from trac import attachment
     2376            from trac.env import Environment
     2377            from trac.ticket import Ticket
     2378            from trac.web.href import Href
     2379            from trac import config as trac_config
     2380            from trac import util
     2381            from trac.core import TracError
     2382            from trac.perm import PermissionSystem
     2383            from trac.ticket.web_ui import TicketModule
     2384
     2385            #
     2386            # return  util.text.to_unicode(str)
     2387            #
     2388            # see http://projects.edgewall.com/trac/changeset/2799
     2389            from trac.ticket.notification import TicketNotifyEmail
     2390
     2391        elif version == '0.12':
     2392            from trac import attachment
     2393            from trac.env import Environment
     2394            from trac.ticket import Ticket
     2395            from trac.web.href import Href
     2396            from trac import config as trac_config
     2397            from trac import util
     2398            from trac.core import TracError
     2399            from trac.perm import PermissionSystem
     2400            from trac.ticket.web_ui import TicketModule
     2401
     2402            #
     2403            # return  util.text.to_unicode(str)
     2404            #
     2405            # see http://projects.edgewall.com/trac/changeset/2799
     2406            from trac.ticket.notification import TicketNotifyEmail
     2407
     2408
     2409        else:
     2410            logger.error('TRAC version %s is not supported' %version)
     2411            sys.exit(1)
     2412
     2413        ## Must be set before environment is created
     2414        #
     2415        if settings.has_key('python_egg_cache'):
     2416            python_egg_cache = str(settings['python_egg_cache'])
     2417            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
     2418
     2419        if settings.debug > 0:
     2420            logger.debug('Loading environment %s', settings.project)
     2421
     2422        try:
     2423            env = Environment(settings['project'], create=0)
     2424        except IOError, detail:
     2425            print "Trac project does not exists: %s" %(settings['project'])
     2426            sys.exit(1)
     2427
     2428        tktparser = TicketEmailParser(env, settings, logger, float(version))
     2429        tktparser.parse(sys.stdin)
     2430
     2431    ## Catch all errors and use the logging module
     2432    #
     2433    except Exception, error:
     2434
     2435        etype, evalue, etb = sys.exc_info()
     2436        for e in traceback.format_exception(etype, evalue, etb):
     2437            logger.critical(e)
     2438
     2439        if m:
     2440            tktparser.save_email_for_debug(m, True)
     2441
     2442        sys.exit(1)
    24432443# EOB
Note: See TracChangeset for help on using the changeset viewer.