source: trunk/email2trac.py.in @ 650

Last change on this file since 650 was 650, checked in by bas, 10 years ago

added bloodhound comments to Changelog, see #331

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 93.2 KB
RevLine 
[22]1#!@PYTHON@
2#
3# This file is part of the email2trac utils
4#
[628]5#       Copyright 2002 SURFsara
[22]6#
[595]7#       Licensed under the Apache License, Version 2.0 (the "License");
8#       you may not use this file except in compliance with the License.
9#       You may obtain a copy of the License at
[22]10#
[595]11#       http://www.apache.org/licenses/LICENSE-2.0
[22]12#
[595]13#       Unless required by applicable law or agreed to in writing, software
14#       distributed under the License is distributed on an "AS IS" BASIS,
15#       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16#       See the License for the specific language governing permissions and
17#       limitations under the License.
18#
[80]19# For vi/emacs or other use tabstop=4 (vi: set ts=4)
20#
[22]21"""
[547]22email2trac.py -- Email -> TRAC tickets
[22]23
[547]24A MTA filter to create Trac tickets from inbound emails.
[22]25
[547]26first proof of concept from:
27 Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28 Copyright 2005, Edgewall Software
[22]29
[282]30Authors:
[628]31  Bas van der Vlies <bas.vandervlies@surfsara.nl>
32  Walter de Jong <walter.dejong@surfsara.nl>
[22]33
34How to use
35----------
[628]36 * See https://oss.trac.surfsara.nl/email2trac/
[282]37
[22]38 * Commandline opions:
[205]39                -h,--help
[624]40                -A, --agilo
[650]41                -B, --bh_product <name>
[507]42                -d, --debug
[625]43                -E, --virtualenv <path>
[205]44                -f,--file  <configuration file>
[410]45                -n,--dry-run
[205]46                -p, --project <project name>
[415]47                -t, --ticket_prefix <name>
[626]48                -v, --verbose
[22]49
50SVN Info:
51        $Id: email2trac.py.in 650 2014-01-14 17:32:48Z bas $
52"""
53import os
54import sys
55import string
56import getopt
57import time
58import email
[136]59import email.Iterators
60import email.Header
[22]61import re
62import urllib
63import unicodedata
64import mimetypes
[96]65import traceback
[403]66import logging
67import logging.handlers
[404]68import UserDict
[498]69import tempfile
[433]70
[520]71from datetime import timedelta, datetime
[433]72from stat import *
[22]73
[508]74## Some global variables
[96]75#
76m = None
[22]77
[404]78class SaraDict(UserDict.UserDict):
[507]79    def __init__(self, dictin = None):
80        UserDict.UserDict.__init__(self)
81        self.name = None
82       
83        if dictin:
84            if dictin.has_key('name'):
85                self.name = dictin['name']
86                del dictin['name']
87            self.data = dictin
88           
89    def get_value(self, name):
90        if self.has_key(name):
91            return self[name]
92        else:
93            return None
94               
95    def __repr__(self):
96        return repr(self.data)
[182]97
[507]98    def __str__(self):
99        return str(self.data)
100           
101    def __getattr__(self, name):
102        """
103        override the class attribute get method. Return the value
104        from the dictionary
105        """
106        if self.data.has_key(name):
107            return self.data[name]
108        else:
109            return None
110           
111    def __setattr__(self, name, value):
112        """
113        override the class attribute set method only when the UserDict
114        has set its class attribute
115        """
116        if self.__dict__.has_key('data'):
117            self.data[name] = value
118        else:
119            self.__dict__[name] = value
[404]120
[507]121    def __iter__(self):
122        return iter(self.data.keys())
[404]123
[633]124
125
[22]126class TicketEmailParser(object):
[507]127    env = None
128    comment = '> '
[359]129
[507]130    def __init__(self, env, parameters, logger, version):
131        self.env = env
[22]132
[507]133        # Database connection
134        #
135        self.db = None
[22]136
[507]137        # Save parameters
138        #
139        self.parameters = parameters
140        self.logger = logger
[206]141
[507]142        # Some useful mail constants
143        #
144        self.email_name = None
145        self.email_addr = None
146        self.email_from = None
147        self.author     = None
148        self.id         = None
[587]149
150        ## Will be set to True if the user has ben registered
151        #
152        self.allow_registered_user = False
[507]153       
154        self.STRIP_CONTENT_TYPES = list()
[72]155
[507]156        ## fields properties via body_text
157        #
158        self.properties = dict()
[452]159
[507]160        self.VERSION = version
[402]161
[507]162        self.get_config = self.env.config.get
[404]163
[507]164        ## init function ##
165        #
166        self.setup_parameters()
[402]167
[507]168    def setup_parameters(self):
169        if self.parameters.umask:
170            os.umask(self.parameters.umask)
[402]171
[507]172        if not self.parameters.spam_level:
173            self.parameters.spam_level = 0
[22]174
[507]175        if not self.parameters.spam_header:
176            self.parameters.spam_header = 'X-Spam-Score'
[207]177
[507]178        if not self.parameters.email_quote:
[607]179            self.parameters.email_quote = '^> .*'
[614]180            self.parameters.skip_line_regex = '^> .*'
[22]181
[507]182        if not self.parameters.ticket_update_by_subject_lookback:
183            self.parameters.ticket_update_by_subject_lookback = 30
[360]184
[507]185        if self.parameters.verbatim_format == None:
186            self.parameters.verbatim_format = 1
[118]187
[507]188        if self.parameters.reflow == None:
189            self.parameters.reflow = 1
[231]190
[507]191        if self.parameters.binhex:
192            self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]193
[507]194        if self.parameters.applesingle:
195            self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]196
[507]197        if self.parameters.appledouble:
198            self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]199
[507]200        if self.parameters.strip_content_types:
201            items = self.parameters.strip_content_types.split(',')
202            for item in items:
203                self.STRIP_CONTENT_TYPES.append(item.strip())
[294]204
[507]205        if self.parameters.tmpdir:
206            self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
207        else:
208            self.parameters.tmpdir = os.path.normcase('/tmp')
[257]209
[507]210        if self.parameters.email_triggers_workflow == None:
211            self.parameters.email_triggers_workflow = 1
[191]212
[507]213        if not self.parameters.subject_field_separator:
214            self.parameters.subject_field_separator = '&'
215        else:
216            self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
[297]217
[606]218        if not self.parameters.strip_signature_regex:
[612]219            self.parameters.strip_signature_regex = '^-- $'
[614]220            self.parameters.cut_line_regex = '^-- $'
[606]221
[507]222        self.trac_smtp_from = self.get_config('notification', 'smtp_from')
223        self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
[631]224        self.smtp_replyto = self.get_config('notification', 'smtp_replyto')
[636]225        self.trac_smtp_always_cc = self.get_config('notification', 'smtp_always_cc')
226        self.trac_smtp_always_bcc = self.get_config('notification', 'smtp_always_bcc')
[305]227
[507]228        self.system = None
[359]229
[341]230########## Email Header Functions ###########################################################
[339]231
[507]232    def spam(self, message):
233        """
234        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
235        # Note if Spam_level then '*' are included
236        """
[580]237        self.logger.debug('function spam')
238
[507]239        spam = False
240        if message.has_key(self.parameters.spam_header):
241            spam_l = string.split(message[self.parameters.spam_header])
[578]242            #self.logger.info('Spam header: %s' %(message[self.parameters.spam_header]))
[22]243
[507]244            try:
245                number = spam_l[0].count('*')
246            except IndexError, detail:
247                number = 0
248               
249            if number >= self.parameters.spam_level:
250                spam = True
251               
[580]252        ## treat virus mails as spam
[507]253        #
254        elif message.has_key('X-Virus-found'):         
255            spam = True
[194]256
[580]257        ## Astaro firewall spam handling
[507]258        #
[580]259        elif message.get('X-Spam-Flag') == "YES" and message.get('X-Spam-Result') == "Spam":
260            spam = True
261
262        ## How to handle SPAM messages
263        #
[507]264        if self.parameters.drop_spam and spam:
[194]265
[507]266            self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
267            return 'drop'   
[194]268
[507]269        elif spam:
[194]270
[507]271            return 'Spam'   
272        else:
[22]273
[507]274            return False
[191]275
[572]276    def email_header_acl(self, keyword, message_field, default):
[507]277        """
278        This function wil check if the email address is allowed or denied
279        to send mail to the ticket list
280        """
281        self.logger.debug('function email_header_acl: %s' %keyword)
[409]282
[507]283        try:
284            mail_addresses = self.parameters[keyword]
[221]285
[507]286            # Check if we have an empty string
287            #
288            if not mail_addresses:
289                return default
[221]290
[507]291        except KeyError, detail:
[572]292            self.logger.debug('\t %s not defined, all messages are allowed.' %(keyword))
[206]293
[507]294            return default
[206]295
[507]296        mail_addresses = string.split(mail_addresses, ',')
[572]297        message_addresses = string.split(message_field, ',')
[221]298
[507]299        for entry in mail_addresses:
300            entry = entry.strip()
301            TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
[149]302
[572]303            for addr in message_addresses:
304                addr = addr.strip()
305
306                if self.parameters.compare_function_list in [ 'matches', 'match']:
307                    s = '\t%s matches %s' %(addr, entry)
308                    result =  TO_RE.match(addr)
309                else:
310                    s = '\t%s contains %s' %(addr, entry)
311                    result =  TO_RE.search(addr)
312
313                if result:
314                    self.logger.debug(s)
315                    return True
316
[507]317        return False
[208]318
[507]319    def email_header_txt(self, m):
320        """
321        Display To and CC addresses in description field
322        """
323        s = ''
[334]324
[507]325        if m['To'] and len(m['To']) > 0:
326            s = "'''To:''' %s\r\n" %(m['To'])
327        if m['Cc'] and len(m['Cc']) > 0:
328            s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]329
[507]330        return  self.email_to_unicode(s)
[22]331
[138]332
[507]333    def get_sender_info(self, message):
334        """
335        Get the default author name and email address from the message
336        """
[576]337        self.logger.debug('function get_sender_info')
[43]338
[570]339        to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
340        self.email_to_addrs = list()
[226]341
[570]342        for n,e in to_addrs:
343            self.email_to_addrs.append(e)
344
345        self.email_to_addrs = ','.join(self.email_to_addrs)
346
347
[576]348        self.email_name, self.email_addr  = email.Utils.parseaddr(message['from'])
[142]349
[576]350        ## decode email name can contain charset
351        #
352        self.email_name = self.email_to_unicode(self.email_name)
353
354        dstr = '\t email name: %s, email address: %s' %(self.email_name, self.email_addr) 
355        self.logger.debug(dstr)
356
[507]357        ## Trac can not handle author's name that contains spaces
358        #
[518]359        if self.email_addr.lower() == self.trac_smtp_from.lower():
[507]360            if self.email_name:
361                self.author = self.email_name
362            else:
363                self.author = "email2trac"
364        else:
365            self.author = self.email_addr
[304]366
[507]367        if self.parameters.ignore_trac_user_settings:
368            return
[194]369
[507]370        # Is this a registered user, use email address as search key:
371        # result:
372        #   u : login name
373        #   n : Name that the user has set in the settings tab
374        #   e : email address that the user has set in the settings tab
375        #
376        users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
377            if (
378                (e and (e.lower() == self.email_addr.lower())) or
[644]379                (u and (u.lower() + '@' + self.smtp_default_domain.lower() == self.email_addr.lower()))
[507]380            )
381            ]
[43]382
[507]383        if len(users) >= 1:
[587]384            self.email_from = users[0][2]
[507]385            self.author = users[0][0]
[45]386
[590]387            self.logger.debug('\tget_sender_info: found registered user: %s' %(self.author) )
388
[587]389            if self.parameters.white_list_registered_users:
[590]390                self.logger.debug('\tget_sender_info: added registered user to white list')
[587]391                self.allow_registered_user = True
392                   
393
[597]394    def set_cc_fields(self, ticket, message, update=False):
[507]395        """
396        Set all the right fields for a new ticket
397        """
[597]398        self.logger.debug('function set_cc_fields')
[72]399
[270]400
[507]401        # Put all CC-addresses in ticket CC field
402        #
403        if self.parameters.reply_all:
[43]404
[636]405            ticket_cc = ''
[299]406
[636]407            msg_cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
[299]408
[636]409            if not msg_cc_addrs:
[507]410                return
[43]411
[597]412            if update:
[636]413                self.logger.debug("\tupdate ticket cc-field")
414                ticket_cc = ticket['cc']
415                ticket_cc_list = ticket_cc.split(',')
[597]416
[636]417            for name,addr in msg_cc_addrs:
[507]418       
419                ## Prevent mail loop
420                #
421                if addr == self.trac_smtp_from:
[597]422                    self.logger.debug("\tSkipping %s email address for CC-field, same as smtp_from address in trac.ini " %(addr))
[507]423                    continue
[43]424
[597]425                ## Alwyas remove reporter email address
426                #
427                elif addr == self.email_addr:
428                    self.logger.debug("\tSkipping reporter email address for CC-field")
429                    continue
430
[636]431                ## Always remove the always_cc address
432                #
433                elif addr == self.trac_smtp_always_cc:
434                    self.logger.debug("\tSkipping smtp_always_cc email address for CC-field")
435                    continue
436
437                ## Always remove the always_bcc address
438                #
439                elif addr == self.trac_smtp_always_bcc:
440                    self.logger.debug("\tSkipping smtp_always_bcc email address for CC-field")
441                    continue
442
[641]443                ## Skip also addr in cc_black_list (email2trac.conf)
444                #
445                if self.email_header_acl('cc_black_list', addr, False):
446                    self.logger.debug("\tSkipping address for CC-field due to cc_black_list")
447                    continue
448
[507]449                else:
[636]450                    ## On update, prevent adding duplicates
451                    #
452                    if update:
453                      if addr in ticket_cc_list:
454                          continue
455                   
456                    if ticket_cc:
457                        ticket_cc = '%s, %s' %(ticket_cc, addr)
[597]458                    else:
[636]459                        ticket_cc = addr
[96]460
[636]461            if ticket_cc:
462                self.logger.debug('\tCC fields: %s' %ticket_cc)
463                ticket['cc'] = self.email_to_unicode(ticket_cc)
[299]464
[339]465
[588]466    def acl_list_from_file(self, f, keyword):
467        """
468        Read the email address from a file
469        """
470        self.logger.debug('function acl_list_from_file %s : %s' %(f, keyword))
471       
472        if  not os.path.isfile(f):
473            self.logger.error('%s_file: %s does not exists' %(keyword, f) )
474        else:
475            ## read whole file and replace '\n' with ''
476            #
477            addr_l = open(f, 'r').readlines()
478            s = ','.join(addr_l).replace('\n','')
479
480            try:
481                self.parameters[keyword] = "%s,%s" %(self.parameters[keyword], s)
482            except KeyError, detail:
483                self.parameters[keyword] = s
484
[339]485########## DEBUG functions  ###########################################################
486
[555]487    def debug_body(self, message_body, temp_file):
488        """
489        """
490        self.logger.debug('function debug_body:')
[310]491
[555]492        body_file = "%s.body" %(temp_file)
493        fx = open(body_file, 'wb')
494
[507]495        if self.parameters.dry_run:
[561]496            self.logger.info('DRY-RUN: not saving body to %s' %(body_file))
[507]497            return
[331]498
[555]499        self.logger.debug('writing body to %s' %(body_file))
[507]500        if not message_body:
501                message_body = '(None)'
[310]502
[507]503        message_body = message_body.encode('utf-8')
[310]504
[507]505        fx.write(message_body)
506        fx.close()
507        try:
508            os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
509        except OSError:
510            pass
[310]511
[555]512    def debug_attachments(self, message_parts, temp_file):
[507]513        """
514        """
515        self.logger.debug('function debug_attachments')
516       
517        n = 0
518        for item in message_parts:
[553]519
520            ## Skip inline text parts
521            #
[507]522            if not isinstance(item, tuple):
523                continue
524               
525            (original, filename, part) = item
[555]526            self.logger.debug('\t part%d: Content-Type: %s' % (n, part.get_content_type()) )
[310]527
[555]528            dummy_filename, ext = os.path.splitext(filename)
529
[507]530            n = n + 1
[555]531            part_name = 'part%d%s' %(n, ext)
532            part_file = "%s.%s" %(temp_file, part_name)
533
534            s = 'writing %s: filename: %s' %(part_file, filename)
[507]535            self.print_unicode(s)
[555]536
537            ## Forbidden chars, just write part files instead of names
[507]538            #
[555]539            #filename = filename.replace('\\', '_')
540            #filename = filename.replace('/', '_')
541            #filename = filename + '.att_email2trac'
542            # part_file = os.path.join(self.parameters.tmpdir, filename)
543            #part_file = util.text.unicode_quote(part_file)
544            #self.print_unicode(part_file)
[331]545
[507]546            if self.parameters.dry_run:
[561]547                self.logger.info('DRY_RUN: NOT saving attachments')
[507]548                continue
[331]549
[507]550            fx = open(part_file, 'wb')
551            text = part.get_payload(decode=1)
[331]552
[507]553            if not text:
554                text = '(None)'
[331]555
[507]556            fx.write(text)
557            fx.close()
[331]558
[507]559            try:
560                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
561            except OSError:
562                pass
[310]563
[528]564    def save_email_for_debug(self, message, project_name, create_tempfile=False):
[309]565
[528]566        if self.parameters.dry_run:
[555]567            self.logger.debug('DRY_RUN: NOT saving email message')
[528]568            return
569
[507]570        if create_tempfile:
[555]571            (fd, tmp_file) = tempfile.mkstemp('.%s.email2trac' %project_name)
[514]572            fx = os.fdopen(fd, 'wb')
[507]573        else:
[555]574            tmp_file = os.path.join(self.parameters.tmpdir, 'msg.%s.email2trac' %project_name)
575            fx = open(tmp_file, 'wb')
[173]576
[44]577
[555]578        self.logger.debug('saving email to %s' %(tmp_file))
[528]579        fx.write('%s' % message)
580        fx.close()
581       
582        try:
[555]583            os.chmod(tmp_file, S_IRWXU|S_IRWXG|S_IRWXO)
[528]584        except OSError:
585            pass
[331]586
[507]587        message_parts = self.get_message_parts(message)
588        message_parts = self.unique_attachment_names(message_parts)
589        body_text = self.get_body_text(message_parts)
[555]590        self.debug_body(body_text, tmp_file)
591        self.debug_attachments(message_parts, tmp_file)
[309]592
[339]593########## Conversion functions  ###########################################################
594
[507]595    def email_to_unicode(self, message_str):
596        """
597        Email has 7 bit ASCII code, convert it to unicode with the charset
598        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
599        understands it.
600        """
601        self.logger.debug("function email_to_unicode")
[341]602
[507]603        results =  email.Header.decode_header(message_str)
[341]604
[507]605        s = None
606        for text,format in results:
607            if format:
608                try:
609                    temp = unicode(text, format)
610                except UnicodeError, detail:
611                    # This always works
612                    #
[525]613                    msg = 'ERROR: Could not find charset: %s, please install' %format
614                    self.logger.error(msg)
[507]615                    temp = unicode(text, 'iso-8859-15')
616                except LookupError, detail:
[514]617                    msg = 'ERROR: Could not find charset: %s, please install' %format
[525]618                    self.logger.error(msg)
[507]619                    #temp = unicode(text, 'iso-8859-15')
620                    temp = message_str
621                       
622            else:
623                temp = string.strip(text)
624                temp = unicode(text, 'iso-8859-15')
[341]625
[507]626            if s:
627                s = '%s %s' %(s, temp)
628            else:
629                s = '%s' %temp
[341]630
[507]631        return s
[341]632
[507]633    def str_to_dict(self, s):
634        """
635        Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
636        """
637        self.logger.debug("function str_to_dict")
[164]638
[507]639        fields = string.split(s, self.parameters.subject_field_separator)
[262]640
[507]641        result = dict()
642        for field in fields:
643            try:
644                index, value = string.split(field, '=')
[169]645
[507]646                # We can not change the description of a ticket via the subject
647                # line. The description is the body of the email
648                #
649                if index.lower() in ['description']:
650                    continue
[169]651
[507]652                if value:
653                    result[index.lower()] = value
[169]654
[507]655            except ValueError:
656                pass
657        return result
[167]658
[507]659    def print_unicode(self,s):
660        """
661        This function prints unicode strings if possible else it will quote it
662        """
663        try:
664            self.logger.debug(s)
665        except UnicodeEncodeError, detail:
666            self.logger.debug(util.text.unicode_quote(s))
[379]667
[503]668
[507]669    def html_2_txt(self, data):
670        """
671        Various routines to convert html syntax to valid trac wiki syntax
672        """
673        self.logger.debug('function html_2_txt')
[503]674
[507]675        ## This routine make an safe html that can be include
676        #  in trac, but no further text processing can be done
677        #
678#       try:
679#           from lxml.html.clean import Cleaner
680#           tags_rm = list()
681#           tags_rm.append('body')
[503]682#
[507]683#           cleaner = Cleaner(remove_tags=tags_rm )
684#           parsed_data = cleaner.clean_html(data)
685#           parsed_data = '\n{{{\n#!html\n' + parsed_data + '\n}}}\n'
[503]686#
[507]687#           return parsed_data
688#           
689#       except ImportError::
690#           pass
[503]691
[507]692        parsed_data = None
693        if self.parameters.html2text_cmd:
[514]694            (fd, tmp_file) = tempfile.mkstemp('email2trac.html')
695            f = os.fdopen(fd, 'w')
696
[507]697            cmd = '%s %s' %(self.parameters.html2text_cmd, tmp_file)
698            self.logger.debug('\t html2text conversion %s'%(cmd))
699   
700            if self.parameters.dry_run:
[561]701                self.logger.info('DRY_RUN: html2text conversion command: %s\n' %(cmd))
[503]702
[507]703            else:
704                f.write(data)
705                f.close()
[503]706
[507]707                lines = os.popen(cmd).readlines()
708                parsed_data =  ''.join(lines)
[503]709
[507]710                os.unlink(tmp_file)
[503]711
[507]712        else:
713            self.logger.debug('\t No html2text conversion tool specified in email2trac.conf')
[503]714
[507]715        return parsed_data
[503]716
[549]717    def check_filename_length(self, name):
718        """
[551]719        To bypass a bug in Trac
720        check if the filename lenght is not larger then OS limit.
[549]721          yes : return truncated filename
722          no  : return unmodified filename
723        """
724        self.logger.debug('function check_filename_length: ')
[550]725
726        if not name:
727            return 'None'
[549]728       
729        dummy_filename, ext = os.path.splitext(name)
730
731        ## Trac uses this format
732        #
[620]733        try:
734            quote_format = util.text.unicode_quote(dummy_filename)
[549]735
[620]736        except UnicodeDecodeError, detail:
737            ## last resort convert to unicode
738            #
739            dummy_filename = util.text.to_unicode(dummy_filename)
740            quote_format = util.text.unicode_quote(dummy_filename)
741
[549]742        ## Determine max filename lenght
743        #
[603]744        try:
745            filemax_length = os.pathconf('/', 'PC_NAME_MAX')
746        except AttributeError, detail:
747            filemax_lenght = 240
[549]748
749        if len(quote_format) <= filemax_length:
750            return name
751
752        else:
753            ## Truncate file to filemax_length and reserve room for extension
754            #  We must break on a boundry
755            #
756            length = filemax_length - 6
757
758            for i in range(0,10):
759
760                truncated = quote_format[ : (length - i)]
761
762                try:
763                    unqoute_truncated = util.text.unicode_unquote(truncated)
764                    unqoute_truncated = unqoute_truncated + ext
765
766                    self.print_unicode('\t ' + unqoute_truncated)
767
768                    break
769
770                except UnicodeDecodeError, detail:
771                    continue
772
773
774            return unqoute_truncated
775
[339]776########## TRAC ticket functions  ###########################################################
777
[546]778    def mail_workflow(self, tkt):
[545]779        """
780        """
[546]781        self.logger.debug('function mail_workflow: ')
[545]782       
783        req = Mock(authname=self.author, perm=MockPerm(), args={})
784        ticket_system = TicketSystem(self.env)
785       
786        try:
787            workflow = self.parameters['workflow_%s' %tkt['status'].lower()]
788
789        except KeyError:
[553]790            ## fallback for compability (Will be deprecated)
791            #  workflow can be none.
[545]792            #
793            workflow = None
794            if tkt['status'] in ['closed']:
795                workflow = self.parameters.workflow
796
797        if workflow:
[556]798
[557]799            ## process all workflow implementations
800            #
[556]801            tkt_module = TicketModule(self.env)
802            field_changes, problems = tkt_module.get_ticket_changes(req, tkt, workflow)
803
804            for field in field_changes.keys():
805
806                ## We have already processed these fields
807                #
[558]808                if not field in ['summary', 'description']:
[556]809                    s = 'workflow : %s, field %s : %s, by %s' \
810                       %(workflow, field, field_changes[field]['new'],field_changes[field]['by'] )
811                    self.logger.debug(s)
812
813                    tkt[field] = field_changes[field]['new']
814
[545]815            return True
816
817        else:
818            return False
819
[507]820    def check_permission_participants(self, tkt, action):
821        """
822        Check if the mailer is allowed to update the ticket
823        """
[538]824        self.logger.debug('function check_permission_participants %s')
[398]825
[518]826        if tkt['reporter'].lower() in [self.author.lower(), self.email_addr.lower()]:
[507]827            self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
[408]828
[507]829            return True
[398]830
[507]831        perm = PermissionSystem(self.env)
832        if perm.check_permission(action, self.author):
833            self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
[408]834
[507]835            return True
836       
837        # Is the updater in the CC?
838        try:
839            cc_list = tkt['cc'].split(',')
840            for cc in cc_list:
[518]841                if self.email_addr.lower() in cc.lower.strip():
[507]842                    self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
[398]843
[507]844                    return True
[398]845
[507]846        except KeyError:
847            pass
[398]848
[507]849        return False
[474]850
[507]851    def check_permission(self, tkt, action):
852        """
853        check if the reporter has the right permission for the action:
[397]854          - TICKET_CREATE
855          - TICKET_MODIFY
[474]856          - TICKET_APPEND
857          - TICKET_CHGPROP
[397]858
[507]859        There are three models:
860            - None      : no checking at all
861            - trac      : check the permission via trac permission model
862            - email2trac: ....
863        """
[538]864        self.logger.debug("function check_permission: %s" %(action))
[397]865
[507]866        if self.parameters.ticket_permission_system in ['trac']:
[398]867
[539]868            perm = PermissionCache(self.env, self.author)
869            if perm.has_permission(action):
[507]870                return True
871            else:
872                return False
[398]873
[507]874        elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
875            return (self.check_permission_participants(tkt, action))   
[398]876
[507]877        ## Default is to allow everybody ticket updates and ticket creation
878        #
879        else:
880                return True
[397]881
882
[507]883    def update_ticket_fields(self, ticket, user_dict, new=None):
884        """
885        This will update the ticket fields. It will check if the
886        given fields are known and if the right values are specified
887        It will only update the ticket field value:
888            - If the field is known
889            - If the value supplied is valid for the ticket field.
890              If not then there are two options:
891               1) Skip the value (new=None)
892               2) Set default value for field (new=1)
893        """
894        self.logger.debug("function update_ticket_fields")
[169]895
[645]896        if self.parameters.bh_product:
897            if 'product' in user_dict:
898                if user_dict['product'] != ticket.env.product.prefix:
899                    self.logging.warning("bloodhound products cannot be changed "
900                                         "- ignoring")
901                user_dict.pop('product')
902
[507]903        ## Check only permission model on ticket updates
904        #
905        if not new:
906            if self.parameters.ticket_permission_system:
907                if not self.check_permission(ticket, 'TICKET_CHGPROP'):
908                    self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
909                    return False
[474]910
[507]911        ## Build a system dictionary from the ticket fields
912        #  with field as index and option as value
913        #
914        sys_dict = dict()
[530]915
[507]916        for field in ticket.fields:
[530]917
[507]918            try:
919                sys_dict[field['name']] = field['options']
[169]920
[507]921            except KeyError:
[531]922                sys_dict[field['name']] = None
[507]923                pass
[169]924
[507]925        ## Check user supplied fields an compare them with the
926        #  system one's
927        #
[549]928        for field,value in user_dict.items():
929       
930            s = 'user_field\t %s = %s' %(field,value)
931            self.print_unicode(s)
[524]932
[531]933            if not field in sys_dict.keys(): 
[524]934                self.logger.debug('%s is not a valid field for tickets' %(field))
935                continue
936
[507]937            ## To prevent mail loop
938            #
939            if field == 'cc':
[301]940
[507]941                cc_list = user_dict['cc'].split(',')
[301]942
[507]943                if self.trac_smtp_from in cc_list:
944                    self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
[408]945
[507]946                    cc_list.remove(self.trac_smtp_from)
[301]947
[507]948                value = ','.join(cc_list)
949               
[301]950
[507]951            ## Check if every value is allowed for this field
952            #
[531]953            if sys_dict[field]:
[169]954
[507]955                if value in sys_dict[field]:
956                    ticket[field] = value
957                else:
958                    ## Must we set a default if value is not allowed
959                    #
960                    if new:
961                        value = self.get_config('ticket', 'default_%s' %(field) )
[169]962
[507]963            else:
[553]964
[507]965                ticket[field] = value
[202]966
[549]967            s = 'ticket_field\t %s = %s' %(field,  ticket[field])
968            self.print_unicode(s)
[345]969
[507]970    def ticket_update(self, m, id, spam):
971        """
972        If the current email is a reply to an existing ticket, this function
973        will append the contents of this email to that ticket, instead of
974        creating a new one.
975        """
976        self.logger.debug("function ticket_update")
[202]977
[507]978        if not self.parameters.ticket_update:
979            self.logger.debug("ticket_update disabled")
980            return False
[456]981
[507]982        ## Must we update ticket fields
983        #
984        update_fields = dict()
985        try:
986            id, keywords = string.split(id, '?')
[262]987
[507]988            update_fields = self.str_to_dict(keywords)
[165]989
[507]990            ## Strip '#'
991            #
992            self.id = int(id[1:])
[165]993
[507]994        except ValueError:
[390]995
[507]996            ## Strip '#'
997            #
998            self.id = int(id[1:])
[164]999
[597]1000        self.logger.debug("\tticket id: %s" %id)
[71]1001
[507]1002        ## When is the change committed
1003        #
1004        if self.VERSION < 0.11:
1005            when = int(time.time())
1006        else:
1007            when = datetime.now(util.datefmt.utc)
[77]1008
[507]1009        try:
1010            tkt = Ticket(self.env, self.id, self.db)
[390]1011
[507]1012        except util.TracError, detail:
[390]1013
[507]1014            ## Not a valid ticket
1015            #
[604]1016            self.logger.info("\tCreating a new ticket, ticket id: %s does not exists" %id)
[507]1017            self.id = None
1018            return False
[126]1019
[507]1020        ## Check the permission of the reporter
1021        #
1022        if self.parameters.ticket_permission_system:
1023            if not self.check_permission(tkt, 'TICKET_APPEND'):
1024                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
1025                return False
[398]1026
[507]1027        ## How many changes has this ticket
1028        #
1029        grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
1030        cnum = sum(1 for e in grouped) + 1
[288]1031
[486]1032
[507]1033        ## reopen the ticket if it is was closed
1034        #  We must use the ticket workflow framework
1035        #
1036        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
[257]1037
[546]1038            if not self.mail_workflow(tkt):
[538]1039
[507]1040                if tkt['status'] in ['closed']:
[526]1041                    tkt['status'] = 'reopened'
1042                    tkt['resolution'] = ''
1043
[507]1044        ## Old pre 0.11 situation
1045        #
1046        elif self.parameters.email_triggers_workflow:
[470]1047
[597]1048            self.logger.debug('\temail triggers workflow pre trac 0.11')
[470]1049
[507]1050            if tkt['status'] in ['closed']:
1051                tkt['status'] = 'reopened'
1052                tkt['resolution'] = ''
[257]1053
[507]1054        else:
[597]1055            self.logger.debug('\temail triggers workflow disabled')
[469]1056
[507]1057        ## Must we update some ticket fields properties via subject line
1058        #
1059        if update_fields:
1060            self.update_ticket_fields(tkt, update_fields)
[166]1061
[597]1062
[507]1063        message_parts = self.get_message_parts(m)
1064        message_parts = self.unique_attachment_names(message_parts)
[210]1065
[597]1066        ## Must we update some ticket fields properties via inline comments
1067        # in body_text
[507]1068        #
1069        if self.properties:
1070                self.update_ticket_fields(tkt, self.properties)
[309]1071
[597]1072        ## Must we update the CC ticket field
1073        #
1074        self.set_cc_fields(tkt, m, update=True)
1075
[507]1076        if self.parameters.email_header:
1077            message_parts.insert(0, self.email_header_txt(m))
[76]1078
[507]1079        body_text = self.get_body_text(message_parts)
[236]1080
[507]1081        error_with_attachments = self.attach_attachments(message_parts)
[348]1082
[507]1083        if body_text.strip() or update_fields or self.properties:
[543]1084
[507]1085            if self.parameters.dry_run:
[561]1086                s = 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) %s %s' %(self.author, cnum)
1087                self.logger.info(s)
[543]1088
[507]1089            else:
1090                if error_with_attachments:
1091                    body_text = '%s\\%s' %(error_with_attachments, body_text)
[543]1092
[597]1093                self.logger.debug('\ttkt.save_changes(%s, %d)' %(self.author, cnum))
[507]1094                tkt.save_changes(self.author, body_text, when, None, str(cnum))
1095           
1096        if not spam:
1097            self.notify(tkt, False, when)
[72]1098
[507]1099        return True
[71]1100
[507]1101    def set_ticket_fields(self, ticket):
1102        """
1103        set the ticket fields to value specified
1104            - /etc/email2trac.conf with <prefix>_<field>
1105            - trac default values, trac.ini
1106        """
1107        self.logger.debug('function set_ticket_fields')
[410]1108
[507]1109        user_dict = dict()
[202]1110
[507]1111        for field in ticket.fields:
[202]1112
[507]1113            name = field['name']
[202]1114
[507]1115            ## default trac value
1116            #
1117            if not field.get('custom'):
1118                value = self.get_config('ticket', 'default_%s' %(name) )
[599]1119
1120                ## skip this field can only be set by email2trac.conf
1121                #
1122                if name in ['resolution']:
[507]1123                    value = None
[599]1124
[507]1125            else:
1126                ##  Else get the default value for reporter
1127                #
1128                value = field.get('value')
1129                options = field.get('options')
[345]1130
[507]1131                if value and options and (value not in options):
1132                     value = options[int(value)]
1133   
[549]1134            s = 'trac[%s] = %s' %(name, value)
1135            self.print_unicode(s)
[202]1136
[507]1137            ## email2trac.conf settings
1138            #
1139            prefix = self.parameters.ticket_prefix
1140            try:
1141                value = self.parameters['%s_%s' %(prefix, name)]
[202]1142
[549]1143                s = 'email2trac[%s] = %s ' %(name, value)
1144                self.print_unicode(s)
1145
[507]1146            except KeyError, detail:
1147                pass
1148       
1149            if value:
1150                user_dict[name] = value
[202]1151
[550]1152                s = 'used %s = %s' %(name, value)
1153                self.print_unicode(s)
1154
[507]1155        self.update_ticket_fields(ticket, user_dict, new=1)
[202]1156
[507]1157        if 'status' not in user_dict.keys():
1158            ticket['status'] = 'new'
[202]1159
1160
[507]1161    def ticket_update_by_subject(self, subject):
1162        """
1163        This list of Re: prefixes is probably incomplete. Taken from
1164        wikipedia. Here is how the subject is matched
1165          - Re: <subject>
1166          - Re: (<Mail list label>:)+ <subject>
[202]1167
[507]1168        So we must have the last column
1169        """
1170        self.logger.debug('function ticket_update_by_subject')
[356]1171
[533]1172        found_id = None
[507]1173        if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
1174               
1175            SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
1176            result = SUBJECT_RE.search(subject)
[356]1177
[507]1178            if result:
1179                ## This is a reply
1180                #
1181                orig_subject = result.group(1)
[356]1182
[507]1183                self.logger.debug('subject search string: %s' %(orig_subject))
[356]1184
[507]1185                cursor = self.db.cursor()
1186                summaries = [orig_subject, '%%: %s' % orig_subject]
[356]1187
[520]1188                ## Time resolution is in micoseconds
[507]1189                #
[520]1190                search_date = datetime.now(util.datefmt.utc) - timedelta(days=self.parameters.ticket_update_by_subject_lookback)
[356]1191
[529]1192                if self.VERSION < 0.12:
1193                    lookback = util.datefmt.to_timestamp(search_date)
1194                else:
1195                    lookback = util.datefmt.to_utimestamp(search_date)
1196
[507]1197                for summary in summaries:
1198                    self.logger.debug('Looking for summary matching: "%s"' % summary)
[408]1199
[533]1200                    sql = """SELECT id, reporter FROM ticket
[507]1201                            WHERE changetime >= %s AND summary LIKE %s
1202                            ORDER BY changetime DESC"""
[533]1203
[507]1204                    cursor.execute(sql, [lookback, summary.strip()])
[356]1205
[507]1206                    for row in cursor:
[408]1207
[533]1208                        (matched_id, sql_reporter) = row
[408]1209
[533]1210                        ## Save first entry.
1211                        #
1212                        if not found_id:
1213                            found_id = matched_id
1214                           
1215                        ## If subject and reporter are the same. The we certainly have found the right ticket
1216                        #
1217                        if sql_reporter == self.author:
1218                            self.logger.debug('Found matching reporter: %s with ticket id: %d' %(sql_reporter, matched_id))
1219                            found_id = matched_id
1220                            break
[356]1221
[533]1222                    if found_id:
1223                        self.logger.debug('Found matching ticket id: %d' % found_id)
1224                        found_id = '#%d' % found_id
1225                        return (found_id, orig_subject)
1226                   
1227        return (found_id, subject)
[356]1228
1229
[507]1230    def new_ticket(self, msg, subject, spam, set_fields = None):
1231        """
1232        Create a new ticket
1233        """
1234        self.logger.debug('function new_ticket')
[250]1235
[507]1236        tkt = Ticket(self.env)
[326]1237
[597]1238        ## self.author can be email address of an username
1239        #
1240        tkt['reporter'] = self.author
[326]1241
[597]1242        self.set_cc_fields(tkt, msg)
1243
[507]1244        self.set_ticket_fields(tkt)
[202]1245
[507]1246        ## Check the permission of the reporter
1247        #
1248        if self.parameters.ticket_permission_system:
1249            if not self.check_permission(tkt, 'TICKET_CREATE'):
1250                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
1251                return False
[397]1252
[507]1253        ## Old style setting for component, will be removed
1254        #
1255        if spam:
1256            tkt['component'] = 'Spam'
[204]1257
[507]1258        elif self.parameters.has_key('component'):
1259            tkt['component'] = self.parameters['component']
[201]1260
[507]1261        if not msg['Subject']:
1262            tkt['summary'] = u'(No subject)'
1263        else:
1264            tkt['summary'] = subject
[22]1265
1266
[507]1267        if set_fields:
1268            rest, keywords = string.split(set_fields, '?')
[262]1269
[507]1270            if keywords:
1271                update_fields = self.str_to_dict(keywords)
1272                self.update_ticket_fields(tkt, update_fields)
[262]1273
[296]1274
[604]1275        message_parts = self.get_message_parts(msg, True)
[309]1276
[507]1277        ## Must we update some ticket fields properties via body_text
1278        #
1279        if self.properties:
1280                self.update_ticket_fields(tkt, self.properties)
[309]1281
[507]1282        message_parts = self.unique_attachment_names(message_parts)
1283       
1284        ## produce e-mail like header
1285        #
1286        head = ''
1287        if self.parameters.email_header:
1288            head = self.email_header_txt(msg)
1289            message_parts.insert(0, head)
1290           
1291        body_text = self.get_body_text(message_parts)
[45]1292
[507]1293        tkt['description'] = body_text
[90]1294
[507]1295        ## When is the change committed
1296        #
1297        if self.VERSION < 0.11:
1298            when = int(time.time())
1299        else:
1300            when = datetime.now(util.datefmt.utc)
[45]1301
[507]1302        if self.parameters.dry_run:
[561]1303            self.logger.info('DRY_RUN: tkt.insert()')
[507]1304        else:
1305            self.id = tkt.insert()
1306   
1307        changed = False
1308        comment = ''
[77]1309
[507]1310        ## some routines in trac are dependend on ticket id
1311        #  like alternate notify template
1312        #
1313        if self.parameters.alternate_notify_template:
1314            tkt['id'] = self.id
1315            changed = True
[273]1316
[507]1317        ## Rewrite the description if we have mailto enabled
1318        #
1319        if self.parameters.mailto_link:
1320            changed = True
1321            comment = u'\nadded mailto line\n'
[592]1322
[581]1323            #mailto = self.html_mailto_link( m['Subject'])
1324            mailto = self.html_mailto_link(subject)
[253]1325
[507]1326            tkt['description'] = u'%s\r\n%s%s\r\n' \
1327                %(head, mailto, body_text)
1328   
1329        ## Save the attachments to the ticket   
1330        #
1331        error_with_attachments =  self.attach_attachments(message_parts)
[295]1332
[507]1333        if error_with_attachments:
1334            changed = True
1335            comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1336
[545]1337        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
[546]1338            if self.mail_workflow(tkt):
[545]1339                changed = True
1340
[507]1341        if changed:
1342            if self.parameters.dry_run:
[561]1343                s = 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1344                self.logger.info(s)
1345
[507]1346            else:
1347                tkt.save_changes(tkt['reporter'], comment)
[90]1348
[507]1349        if not spam:
[545]1350            self.notify(tkt, True)
[45]1351
[260]1352
[507]1353    def attach_attachments(self, message_parts, update=False):
1354        '''
1355        save any attachments as files in the ticket's directory
1356        '''
1357        self.logger.debug('function attach_attachments()')
[342]1358
[507]1359        if self.parameters.dry_run:
[555]1360            self.logger.debug("DRY_RUN: no attachments attached to tickets")
[507]1361            return ''
[342]1362
[507]1363        count = 0
[342]1364
[507]1365        ## Get Maxium attachment size
1366        #
1367        max_size = int(self.get_config('attachment', 'max_size'))
1368        status   = None
1369       
1370        for item in message_parts:
1371            ## Skip body parts
1372            #
1373            if not isinstance(item, tuple):
1374                continue
1375               
1376            (original, filename, part) = item
[477]1377
[548]1378            ## We have to determine the size so we use this temporary solution.
[507]1379            #
[548]1380            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, 'email2trac_tmp.att'))
[507]1381            text = part.get_payload(decode=1)
1382            if not text:
1383                text = '(None)'
1384            fd.write(text)
1385            fd.close()
[342]1386
[507]1387            ## get the file_size
1388            #
1389            stats = os.lstat(path)
1390            file_size = stats[ST_SIZE]
[342]1391
[507]1392            ## Check if the attachment size is allowed
1393            #
1394            if (max_size != -1) and (file_size > max_size):
1395                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1396                    %(status, original, file_size, max_size)
[342]1397
[507]1398                os.unlink(path)
1399                continue
1400            else:
1401                count = count + 1
1402                   
1403            ## Insert the attachment
1404            #
1405            fd = open(path, 'rb')
1406            if self.system == 'discussion':
[553]1407                att = attachment.Attachment(self.env, 'discussion', 'topic/%s' % (self.id,))
[585]1408            elif self.system == 'blog':
1409                att = attachment.Attachment(self.env, 'blog', '%s' % (self.id,))
[507]1410            else:
[548]1411                s = 'Attach %s to ticket %d' %(filename, self.id)
1412                self.print_unicode(s)
[507]1413                att = attachment.Attachment(self.env, 'ticket', self.id)
[359]1414 
[507]1415            ## This will break the ticket_update system, the body_text is vaporized
1416            #  ;-(
1417            #
1418            if not update:
1419                att.author = self.author
1420                att.description = self.email_to_unicode('Added by email2trac')
[342]1421
[553]1422            try:
[549]1423
[507]1424                self.logger.debug('Insert atachment')
1425                att.insert(filename, fd, file_size)
[553]1426
[507]1427            except OSError, detail:
[553]1428
[507]1429                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1430                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
[342]1431
[507]1432            ## Remove the created temporary filename
1433            #
1434            fd.close()
1435            os.unlink(path)
[342]1436
[507]1437        ## return error
1438        #
1439        return status
[342]1440
[359]1441########## Fullblog functions  #################################################
[339]1442
[585]1443    def blog(self, msg, subject, id, params):
[507]1444        """
1445        The blog create/update function
1446        """
1447        ## import the modules
1448        #
1449        from tracfullblog.core import FullBlogCore
1450        from tracfullblog.model import BlogPost, BlogComment
[260]1451
[507]1452        ## instantiate blog core
1453        #
1454        blog = FullBlogCore(self.env)
1455        req = Mock(authname='anonymous', perm=MockPerm(), args={})
[312]1456
[585]1457        ## parameters from the subject
1458        #
1459        params = self.str_to_dict((params or '').lstrip('?'))
[260]1460
[585]1461        ## preferably get the time from the email date header, or else
1462        #  use current date and time
1463        date = email.Utils.parsedate_tz(msg.get('date'))
1464        if date:
1465            dt = util.datefmt.to_datetime(email.Utils.mktime_tz(date), util.datefmt.utc)
1466        else:
1467            self.logger.warn("No valid date header found")
1468            dt = util.datefmt.to_datetime(None, util.datefmt.utc)
1469
1470        ## blog entry affected
1471        #
1472        self.id = id or util.datefmt.format_datetime(dt, "%Y%m%d%H%M%S", util.datefmt.utc)
1473
1474        ## check wether a blog post exists
1475        #
1476        post = BlogPost(self.env, self.id)
1477        force_update = self.properties.get('update', params.get('update'))
1478
1479        ## message parts
1480        #
1481        message_parts = self.get_message_parts(msg)
1482        message_parts = self.unique_attachment_names(message_parts)
1483
1484        if post.get_versions() and not force_update:
1485
1486            ## add comment to blog entry
[507]1487            #
[585]1488            comment = BlogComment(self.env, self.id)
1489            comment.author = self.properties.get('author', params.get('author', self.author))
[507]1490            comment.comment = self.get_body_text(message_parts)
[585]1491            comment.time = dt
[312]1492
[578]1493            if self.parameters.dry_run:
1494                self.logger.info('DRY-RUN: not adding comment for blog entry "%s"' % id)
1495                return
[585]1496            warnings = blog.create_comment(req, comment)
[260]1497
[507]1498        else:
[585]1499            ## create or update blog entry
[507]1500            #
[585]1501            post.author = self.properties.get('author', params.get('author', self.author))
1502            post.categories = self.properties.get('categories', params.get('categories', ''))
[578]1503            post.title = subject.strip()
[585]1504            post.publish_time = dt
[507]1505            post.body = self.get_body_text(message_parts)
1506           
[578]1507            if self.parameters.dry_run:
1508                self.logger.info('DRY-RUN: not creating blog entry "%s"' % post.title)
1509                return
[585]1510            warnings = blog.create_post(req, post, self.author, u'Created by email2trac', False)
[260]1511
[585]1512        ## check for problems
1513        #
1514        if warnings:
1515            raise TracError(', '.join('blog:%s:%s' % (w[0], w[1]) for w in warnings))
1516       
1517        ## all seems well, attach attachments
1518        #
1519        self.attach_attachments(message_parts)
[260]1520
[585]1521
[359]1522########## Discussion functions  ##############################################
[342]1523
[507]1524    def discussion_topic(self, content, subject):
[342]1525
[507]1526        ## Import modules.
1527        #
1528        from tracdiscussion.api import DiscussionApi
1529        from trac.util.datefmt import to_timestamp, utc
[359]1530
[507]1531        self.logger.debug('Creating a new topic in forum:', self.id)
[359]1532
[507]1533        ## Get dissussion API component.
1534        #
1535        api = self.env[DiscussionApi]
1536        context = self._create_context(content, subject)
[359]1537
[507]1538        ## Get forum for new topic.
1539        #
1540        forum = api.get_forum(context, self.id)
[359]1541
[507]1542        if not forum:
1543            self.logger.error("ERROR: Replied forum doesn't exist")
[359]1544
[507]1545        ## Prepare topic.
1546        #
1547        topic = {'forum' : forum['id'],
1548                 'subject' : context.subject,
1549                 'time': to_timestamp(datetime.now(utc)),
1550                 'author' : self.author,
1551                 'subscribers' : [self.email_addr],
1552                 'body' : self.get_body_text(context.content_parts)}
[359]1553
[507]1554        ## Add topic to DB and commit it.
1555        #
1556        self._add_topic(api, context, topic)
1557        self.db.commit()
[359]1558
[507]1559    def discussion_topic_reply(self, content, subject):
[359]1560
[507]1561        ## Import modules.
1562        #
1563        from tracdiscussion.api import DiscussionApi
1564        from trac.util.datefmt import to_timestamp, utc
[359]1565
[507]1566        self.logger.debug('Replying to discussion topic', self.id)
[359]1567
[507]1568        ## Get dissussion API component.
1569        #
1570        api = self.env[DiscussionApi]
1571        context = self._create_context(content, subject)
[359]1572
[507]1573        ## Get replied topic.
1574        #
1575        topic = api.get_topic(context, self.id)
[359]1576
[507]1577        if not topic:
1578            self.logger.error("ERROR: Replied topic doesn't exist")
[359]1579
[507]1580        ## Prepare message.
1581        #
1582        message = {'forum' : topic['forum'],
1583                   'topic' : topic['id'],
1584                   'replyto' : -1,
1585                   'time' : to_timestamp(datetime.now(utc)),
1586                   'author' : self.author,
1587                   'body' : self.get_body_text(context.content_parts)}
[359]1588
[507]1589        ## Add message to DB and commit it.
1590        #
1591        self._add_message(api, context, message)
1592        self.db.commit()
[359]1593
[507]1594    def discussion_message_reply(self, content, subject):
[359]1595
[507]1596        ## Import modules.
1597        #
1598        from tracdiscussion.api import DiscussionApi
1599        from trac.util.datefmt import to_timestamp, utc
[359]1600
[507]1601        self.logger.debug('Replying to discussion message', self.id)
[359]1602
[507]1603        ## Get dissussion API component.
1604        #
1605        api = self.env[DiscussionApi]
1606        context = self._create_context(content, subject)
[359]1607
[507]1608        ## Get replied message.
1609        #
1610        message = api.get_message(context, self.id)
[359]1611
[507]1612        if not message:
1613            self.logger.error("ERROR: Replied message doesn't exist")
[359]1614
[507]1615        ## Prepare message.
1616        #
1617        message = {'forum' : message['forum'],
1618                   'topic' : message['topic'],
1619                   'replyto' : message['id'],
1620                   'time' : to_timestamp(datetime.now(utc)),
1621                   'author' : self.author,
1622                   'body' : self.get_body_text(context.content_parts)}
[359]1623
[507]1624        ## Add message to DB and commit it.
1625        #
1626        self._add_message(api, context, message)
1627        self.db.commit()
[359]1628
[507]1629    def _create_context(self, content, subject):
[359]1630
[507]1631        ## Import modules.
1632        #
1633        from trac.mimeview import Context
1634        from trac.web.api import Request
1635        from trac.perm import PermissionCache
[359]1636
[507]1637        ## TODO: Read server base URL from config.
1638        #  Create request object to mockup context creation.
1639        #
1640        environ = {'SERVER_PORT' : 80,
1641                   'SERVER_NAME' : 'test',
1642                   'REQUEST_METHOD' : 'POST',
1643                   'wsgi.url_scheme' : 'http',
1644                   'wsgi.input' : sys.stdin}
1645        chrome =  {'links': {},
1646                   'scripts': [],
1647                   'ctxtnav': [],
1648                   'warnings': [],
1649                   'notices': []}
[359]1650
[507]1651        if self.env.base_url_for_redirect:
1652            environ['trac.base_url'] = self.env.base_url
[359]1653
[507]1654        req = Request(environ, None)
1655        req.chrome = chrome
1656        req.tz = 'missing'
1657        req.authname = self.author
1658        req.perm = PermissionCache(self.env, self.author)
1659        req.locale = None
[359]1660
[507]1661        ## Create and return context.
1662        #
1663        context = Context.from_request(req)
1664        context.realm = 'discussion-email2trac'
1665        context.cursor = self.db.cursor()
1666        context.content = content
1667        context.subject = subject
[359]1668
[507]1669        ## Read content parts from content.
1670        #
1671        context.content_parts = self.get_message_parts(content)
1672        context.content_parts = self.unique_attachment_names(
1673          context.content_parts)
[359]1674
[507]1675        return context
[359]1676
[507]1677    def _add_topic(self, api, context, topic):
1678        context.req.perm.assert_permission('DISCUSSION_APPEND')
[359]1679
[507]1680        ## Filter topic.
1681        #
1682        for discussion_filter in api.discussion_filters:
1683            accept, topic_or_error = discussion_filter.filter_topic(
1684              context, topic)
1685            if accept:
1686                topic = topic_or_error
1687            else:
1688                raise TracError(topic_or_error)
[359]1689
[507]1690        ## Add a new topic.
1691        #
1692        api.add_topic(context, topic)
[359]1693
[507]1694        ## Get inserted topic with new ID.
1695        #
1696        topic = api.get_topic_by_time(context, topic['time'])
[359]1697
[507]1698        ## Attach attachments.
1699        #
1700        self.id = topic['id']
1701        self.attach_attachments(context.content_parts, True)
[359]1702
[507]1703        ## Notify change listeners.
1704        #
1705        for listener in api.topic_change_listeners:
1706            listener.topic_created(context, topic)
[359]1707
[507]1708    def _add_message(self, api, context, message):
1709        context.req.perm.assert_permission('DISCUSSION_APPEND')
[359]1710
[507]1711        ## Filter message.
1712        #
1713        for discussion_filter in api.discussion_filters:
1714            accept, message_or_error = discussion_filter.filter_message(
1715              context, message)
1716            if accept:
1717                message = message_or_error
1718            else:
1719                raise TracError(message_or_error)
[359]1720
[507]1721        ## Add message.
1722        #
1723        api.add_message(context, message)
[359]1724
[507]1725        ## Get inserted message with new ID.
1726        #
1727        message = api.get_message_by_time(context, message['time'])
[359]1728
[507]1729        ## Attach attachments.
1730        #
[549]1731
[507]1732        self.attach_attachments(context.content_parts, True)
[359]1733
[507]1734        ## Notify change listeners.
1735        #
1736        for listener in api.message_change_listeners:
1737            listener.message_created(context, message)
[359]1738
1739########## MAIN function  ######################################################
1740
[507]1741    def parse(self, fp):
1742        """
1743        """
1744        self.logger.debug('Main function parse')
1745        global m
[96]1746
[507]1747        m = email.message_from_file(fp)
1748       
1749        if not m:
1750            self.logger.debug('This is not a valid email message format')
1751            return
1752           
1753        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1754        #
1755        try:
1756            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1757        except AttributeError, detail:
1758            pass
[239]1759
[507]1760        if self.parameters.debug:     # save the entire e-mail message text
[528]1761            self.save_email_for_debug(m, self.parameters.project_name, True)
[77]1762
[618]1763        if self.VERSION <= 0.11:
1764            self.db = self.env.get_db_cnx()
1765        else:
1766            self.db = self.env.get_read_db()
1767
[507]1768        self.get_sender_info(m)
[152]1769
[587]1770
[588]1771        if self.parameters.white_list_file:
1772            self.acl_list_from_file(self.parameters.white_list_file, 'white_list')
1773
[587]1774        if not ( self.email_header_acl('white_list', self.email_addr, True) or self.allow_registered_user ) :
1775
[507]1776            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1777            return False
[77]1778
[507]1779        if self.email_header_acl('black_list', self.email_addr, False):
1780            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1781            return False
[221]1782
[570]1783        if not self.email_header_acl('recipient_list', self.email_to_addrs, True):
1784            self.logger.info('Message rejected : %s not in recipient list' %(self.email_to_addrs))
[507]1785            return False
[226]1786
[507]1787        ## If spam drop the message
1788        #
1789        if self.spam(m) == 'drop':
1790            return False
[194]1791
[507]1792        elif self.spam(m) == 'spam':
1793            spam_msg = True
1794        else:
1795            spam_msg = False
[204]1796
[507]1797        if not m['Subject']:
1798            subject  = 'No Subject'
1799        else:
[564]1800            subject  = self.email_to_unicode(m['Subject'])
[304]1801
[564]1802        ## Bug in python logging module <2.6
1803        #
1804        self.logger.info('subject: %s' %repr(subject))
[359]1805
[631]1806        ## First try the new method and then fall back to old method
[621]1807        #
[631]1808        if not self.parse_delivered_to_field(m, subject, spam_msg):
1809            self.parse_subject_field(m, subject, spam_msg)
[621]1810
[631]1811    def parse_delivered_to_field(self, m, subject, spam_msg):
1812        """
1813        """
1814        self.logger.debug('function parse_delivered_to_field')
[621]1815
[631]1816        ## Ticket id is in Delivered-To Field. Trac notify must send this
1817        #  eg: is+390@surfsara.nl
[642]1818       
1819        ## Only run if this parameter is se
1820        #
1821        if not self.parameters.recipient_delimiter:
1822            return False
1823
[631]1824        try:
1825
1826            #print self.parameters.project_name
1827            self.logger.debug('Delivered To: %s' %m['Delivered-To'])
1828
1829            id = m['Delivered-To']
1830            id = id.split(self.parameters.recipient_delimiter)[1]
1831            id = id.split('@')[0]
1832
1833            self.logger.debug('\t Found ticket id: %s' %id)
1834
1835            if not self.ticket_update(m, id, spam_msg):
1836                return False
1837
1838        except KeyError, detail:
1839            pass
1840        except IndexError, detail:
1841            pass
1842
1843        return False
1844
1845    def parse_subject_field(self, m, subject, spam_msg):
1846        """
1847        """
1848        self.logger.debug('function parse_subject_header')
1849
[507]1850        ## [hic] #1529: Re: LRZ
1851        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1852        #
1853        ticket_regex = r'''
1854            (?P<new_fields>[#][?].*)
1855            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1856            '''
[631]1857
[507]1858        ## Check if  FullBlogPlugin is installed
1859        #
1860        blog_enabled = None
1861        blog_regex = ''
1862        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1863            self.logger.debug('Trac BLOG support enabled')
1864            blog_enabled = True
[585]1865            blog_regex = '''|(?P<blog>blog(?P<blog_params>[?][^:]*)?:(?P<blog_id>\S*))'''
[329]1866
[77]1867
[507]1868        ## Check if DiscussionPlugin is installed
1869        #
1870        discussion_enabled = None
1871        discussion_regex = ''
1872        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1873            self.logger.debug('Trac Discussion support enabled')
1874            discussion_enabled = True
1875            discussion_regex = r'''
1876            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1877            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1878            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1879            '''
[77]1880
[359]1881
[507]1882        regex_str = ticket_regex + blog_regex + discussion_regex
1883        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
[359]1884
[507]1885        ## Find out if this is a ticket, a blog or a discussion
1886        #
1887        result =  SYSTEM_RE.search(subject)
[390]1888
[507]1889        if result:
1890            ## update ticket + fields
1891            #
1892            if result.group('reply'):
1893                self.system = 'ticket'
[260]1894
[507]1895                ## Skip the last ':' character
1896                #
1897                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1898                    self.new_ticket(m, subject, spam_msg)
[390]1899
[507]1900            ## New ticket + fields
1901            #
1902            elif result.group('new_fields'):
1903                self.system = 'ticket'
1904                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
[262]1905
[507]1906            if blog_enabled:
1907                if result.group('blog'):
1908                    self.system = 'blog'
[585]1909                    self.blog(m, subject[result.end('blog'):], result.group('blog_id'), result.group('blog_params'))
[359]1910
[507]1911            if discussion_enabled:
1912                ## New topic.
1913                #
1914                if result.group('forum'):
1915                    self.system = 'discussion'
1916                    self.id = int(result.group('forum_id'))
1917                    self.discussion_topic(m, subject[result.end('forum'):])
[359]1918
[507]1919                ## Reply to topic.
1920                #
1921                elif result.group('topic'):
1922                    self.system = 'discussion'
1923                    self.id = int(result.group('topic_id'))
1924                    self.discussion_topic_reply(m, subject[result.end('topic'):])
[359]1925
[507]1926                ## Reply to topic message.
1927                #
1928                elif result.group('message'):
1929                    self.system = 'discussion'
1930                    self.id = int(result.group('message_id'))
1931                    self.discussion_message_reply(m, subject[result.end('message'):])
[359]1932
[507]1933        else:
[553]1934
[507]1935            self.system = 'ticket'
1936            (matched_id, subject) = self.ticket_update_by_subject(subject)
[553]1937
[507]1938            if matched_id:
[553]1939
[507]1940                if not self.ticket_update(m, matched_id, spam_msg):
1941                    self.new_ticket(m, subject, spam_msg)
[553]1942
[507]1943            else:
1944                ## No update by subject, so just create a new ticket
1945                #
1946                self.new_ticket(m, subject, spam_msg)
[353]1947
[356]1948
[343]1949########## BODY TEXT functions  ###########################################################
1950
[507]1951    def strip_signature(self, text):
1952        """
1953        Strip signature from message, inspired by Mailman software
1954        """
[607]1955        self.logger.debug('function strip_signature: %s' %self.parameters.strip_signature_regex)
[462]1956
[507]1957        body = []
[606]1958
1959        STRIP_RE = re.compile( self.parameters.strip_signature_regex )
[507]1960        for line in text.splitlines():
[606]1961
1962            match = STRIP_RE.match(line)
1963            if match:
1964                self.logger.debug('\t"%s "  matched, skiping rest of message' %line)
[507]1965                break
[606]1966
[507]1967            body.append(line)
[136]1968
[507]1969        return ('\n'.join(body))
[136]1970
[507]1971    def reflow(self, text, delsp = 0):
1972        """
1973        Reflow the message based on the format="flowed" specification (RFC 3676)
1974        """
1975        flowedlines = []
1976        quotelevel = 0
1977        prevflowed = 0
[231]1978
[507]1979        for line in text.splitlines():
1980            from re import match
1981           
1982            ## Figure out the quote level and the content of the current line
1983            #
1984            m = match('(>*)( ?)(.*)', line)
1985            linequotelevel = len(m.group(1))
1986            line = m.group(3)
[231]1987
[507]1988            ## Determine whether this line is flowed
1989            #
1990            if line and line != '-- ' and line[-1] == ' ':
1991                flowed = 1
1992            else:
1993                flowed = 0
[231]1994
[507]1995            if flowed and delsp and line and line[-1] == ' ':
1996                line = line[:-1]
[231]1997
[507]1998            ## If the previous line is flowed, append this line to it
1999            #
2000            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
2001                flowedlines[-1] += line
[477]2002
[507]2003            ## Otherwise, start a new line
2004            #
2005            else:
2006                flowedlines.append('>' * linequotelevel + line)
[231]2007
[507]2008            prevflowed = flowed
2009           
[231]2010
[507]2011        return '\n'.join(flowedlines)
[231]2012
[507]2013    def strip_quotes(self, text):
2014        """
2015        Strip quotes from message by Nicolas Mendoza
2016        """
[607]2017        self.logger.debug('function strip_quotes: %s' %self.parameters.email_quote)
[462]2018
[507]2019        body = []
[607]2020
2021        STRIP_RE = re.compile( self.parameters.email_quote )
2022
[507]2023        for line in text.splitlines():
[607]2024
[507]2025            try:
[462]2026
[607]2027                match = STRIP_RE.match(line)
2028                if match:
[637]2029                    self.logger.debug('\t"%s "  matched, skipping rest of message' %line)
[507]2030                    continue
[462]2031
[507]2032            except UnicodeDecodeError:
[462]2033
[507]2034                tmp_line = self.email_to_unicode(line)
[607]2035
2036                match = STRIP_RE.match(tmp_line)
2037                if match:
[637]2038                    self.logger.debug('\t"%s "  matched, skipping rest of message' %line)
[507]2039                    continue
2040               
2041            body.append(line)
[151]2042
[507]2043        return ('\n'.join(body))
[191]2044
[507]2045    def inline_properties(self, text):
2046        """
2047        Parse text if we use inline keywords to set ticket fields
2048        """
2049        self.logger.debug('function inline_properties')
[309]2050
[507]2051        properties = dict()
2052        body = list()
[309]2053
[507]2054        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
[309]2055
[507]2056        for line in text.splitlines():
2057            match = INLINE_EXP.match(line)
2058            if match:
2059                keyword, value = match.groups()
[509]2060
2061                if self.parameters.inline_properties_first_wins:
2062                    if keyword in self.properties.keys():
2063                        continue
2064
[507]2065                self.properties[keyword] = value.strip()
[509]2066                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
[408]2067
[507]2068            else:
2069                body.append(line)
2070               
2071        return '\n'.join(body)
[309]2072
2073
[507]2074    def wrap_text(self, text, replace_whitespace = False):
2075        """
2076        Will break a lines longer then given length into several small
2077        lines of size given length
2078        """
2079        import textwrap
[154]2080
[507]2081        LINESEPARATOR = '\n'
2082        reformat = ''
[151]2083
[507]2084        for s in text.split(LINESEPARATOR):
2085            tmp = textwrap.fill(s, self.parameters.use_textwrap)
2086            if tmp:
2087                reformat = '%s\n%s' %(reformat,tmp)
2088            else:
2089                reformat = '%s\n' %reformat
[153]2090
[507]2091        return reformat
[153]2092
[507]2093        # Python2.4 and higher
2094        #
2095        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
2096        #
[154]2097
[343]2098########## EMAIL attachements functions ###########################################################
2099
[507]2100    def inline_part(self, part):
2101        """
2102        """
2103        self.logger.debug('function inline_part()')
[154]2104
[507]2105        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
[340]2106
[604]2107    def get_message_parts(self, msg, new_email=False):
[507]2108        """
2109        parses the email message and returns a list of body parts and attachments
2110        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
2111        """
2112        self.logger.debug('function get_message_parts()')
[317]2113
[507]2114        message_parts = list()
2115   
2116        ALTERNATIVE_MULTIPART = False
[278]2117
[507]2118        for part in msg.walk():
2119            content_maintype = part.get_content_maintype()
2120            content_type =  part.get_content_type()
[278]2121
[507]2122            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
2123            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
[498]2124
[507]2125            ## Check content type
2126            #
2127            if content_type in self.STRIP_CONTENT_TYPES:
2128                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
2129                continue
[238]2130
[507]2131            ## Catch some mulitpart execptions
2132            #
2133            if content_type == 'multipart/alternative':
2134                ALTERNATIVE_MULTIPART = True
2135                continue
[278]2136
[507]2137            ## Skip multipart containers
2138            #
2139            if content_maintype == 'multipart':
2140                self.logger.debug("\t Skipping multipart container")
2141                continue
2142           
[569]2143            ## Check if this is an inline part. It's inline if there is co Cont-Disp header,
2144            #  or if there is one and it says "inline"
[507]2145            #
2146            inline = self.inline_part(part)
[236]2147
[507]2148            ## Drop HTML message
2149            #
2150            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
[553]2151
[507]2152                if content_type == 'text/html':
2153                    self.logger.debug('\t Skipping alternative HTML message')
2154                    ALTERNATIVE_MULTIPART = False
2155                    continue
[278]2156
[642]2157
2158            #if self.VERSION < 1.0:
2159            #    filename = part.get_filename()
2160
2161            ## convert 7 bit filename to 8 bit unicode
2162            #
2163            raw_filename = part.get_filename()
[643]2164            filename = self.email_to_unicode(raw_filename);
[642]2165
[569]2166            s = '\t unicode filename: %s' %(filename)
2167            self.print_unicode(s)
[643]2168            self.logger.debug('\t raw filename: %s' %repr(raw_filename))
[498]2169
[642]2170            if self.VERSION < 1.0:
2171                filename = self.check_filename_length(filename)
[569]2172
[507]2173            ## Save all non plain text message as attachment
2174            #
2175            if not content_type in ['text/plain']:
[498]2176
[569]2177                message_parts.append( (filename, part) )
[498]2178
[569]2179                ## We only convert html messages
[507]2180                #
2181                if not content_type == 'text/html':
[569]2182                    self.logger.debug('\t Appending %s (%s)' %(repr(filename), content_type))
[507]2183                    continue
[500]2184
[569]2185
2186            ## We have an text or html message
2187            #
[507]2188            if not inline:
[569]2189                    self.logger.debug('\t Appending %s (%s), not an inline messsage part' %(repr(filename), content_type))
2190                    message_parts.append( (filename, part) )
[507]2191                    continue
2192               
2193            ## Try to decode message part. We have a html or plain text messafe
2194            #
2195            body_text = part.get_payload(decode=1)
2196            if not body_text:           
2197                body_text = part.get_payload(decode=0)
[236]2198
[507]2199            ## Try to convert html message
2200            #
2201            if content_type == 'text/html':
[502]2202
[507]2203                body_text = self.html_2_txt(body_text)
2204                if not body_text:
2205                    continue
[498]2206
[507]2207            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
2208            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]2209
[507]2210            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
2211                body_text = self.reflow(body_text, delsp == 'yes')
[22]2212
[604]2213            if new_email and self.parameters.only_strip_on_update:
2214                self.logger.debug('Skip signature/quote stripping for new messages')
2215            else:
2216                if self.parameters.strip_signature:
2217                    body_text = self.strip_signature(body_text)
[191]2218
[604]2219                if self.parameters.strip_quotes:
2220                    body_text = self.strip_quotes(body_text)
2221
[507]2222            if self.parameters.inline_properties:
2223                body_text = self.inline_properties(body_text)
[309]2224
[507]2225            if self.parameters.use_textwrap:
2226                body_text = self.wrap_text(body_text)
[148]2227
[507]2228            ## Get contents charset (iso-8859-15 if not defined in mail headers)
2229            #
2230            charset = part.get_content_charset()
2231            if not charset:
2232                charset = 'iso-8859-15'
[102]2233
[507]2234            try:
2235                ubody_text = unicode(body_text, charset)
[100]2236
[507]2237            except UnicodeError, detail:
2238                ubody_text = unicode(body_text, 'iso-8859-15')
[89]2239
[507]2240            except LookupError, detail:
2241                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]2242
[507]2243            if self.parameters.verbatim_format:
2244                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
2245            else:
2246                message_parts.append('%s' %ubody_text)
[22]2247
[507]2248        return message_parts
2249       
2250    def unique_attachment_names(self, message_parts):
2251        """
2252        Make sure we have unique names attachments:
2253          - check if it contains illegal characters
2254          - Rename "None" filenames to "untitled-part"
2255        """
2256        self.logger.debug('function unique_attachment_names()')
2257        renamed_parts = []
2258        attachment_names = set()
[296]2259
[507]2260        for item in message_parts:
2261           
2262            ## If not an attachment, leave it alone
2263            #
2264            if not isinstance(item, tuple):
2265                renamed_parts.append(item)
2266                continue
2267               
2268            (filename, part) = item
[295]2269
[507]2270            ## If filename = None, use a default one
2271            #
2272            if filename in [ 'None']:
2273                filename = 'untitled-part'
[550]2274                self.logger.info('\t Rename filename "None" to: %s' %filename)
[22]2275
[507]2276                ## Guess the extension from the content type, use non strict mode
2277                #  some additional non-standard but commonly used MIME types
2278                #  are also recognized
2279                #
2280                ext = mimetypes.guess_extension(part.get_content_type(), False)
2281                if not ext:
2282                    ext = '.bin'
[22]2283
[507]2284                filename = '%s%s' % (filename, ext)
[22]2285
[507]2286            ## Discard relative paths for windows/unix in attachment names
2287            #
2288            filename = filename.replace('\\', '_')
2289            filename = filename.replace('/', '_')
[347]2290
[507]2291            ## remove linefeed char
2292            #
2293            for forbidden_char in ['\r', '\n']:
2294                filename = filename.replace(forbidden_char,'')
[465]2295
[507]2296            ## We try to normalize the filename to utf-8 NFC if we can.
2297            #  Files uploaded from OS X might be in NFD.
2298            #  Check python version and then try it
2299            #
2300            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2301            #   try:
2302            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2303            #   except TypeError:
2304            #       pass
[100]2305
[507]2306            ## Make the filename unique for this ticket
2307            #
2308            num = 0
2309            unique_filename = filename
2310            dummy_filename, ext = os.path.splitext(filename)
[134]2311
[507]2312            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2313                num += 1
2314                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2315               
[550]2316            s = '\t Attachment with filename %s will be saved as %s' % (filename, unique_filename)
[549]2317            self.print_unicode(s)
[100]2318
[507]2319            attachment_names.add(unique_filename)
[236]2320
[507]2321            renamed_parts.append((filename, unique_filename, part))
2322   
2323        return renamed_parts
2324           
2325           
2326    def attachment_exists(self, filename):
[250]2327
[549]2328        self.logger.debug("function attachment_exists")
[250]2329
[549]2330        s = '\t check if attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2331        self.print_unicode(s)
2332
[553]2333        ## Do we have a valid ticket id
[507]2334        #
2335        if not self.id:
2336            return False
[250]2337
[507]2338        try:
2339            if self.system == 'discussion':
[553]2340
2341                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s' % (self.id,), filename)
2342
[585]2343            elif self.system == 'blog':
2344
2345                att = attachment.Attachment(self.env, 'blog', '%s' % (self.id,), filename)
2346
[507]2347            else:
[553]2348
2349                att = attachment.Attachment(self.env, 'ticket', self.id, filename)
2350
[507]2351            return True
[553]2352
[507]2353        except attachment.ResourceNotFound:
[553]2354
[507]2355            return False
[343]2356
2357########## TRAC Ticket Text ###########################################################
[507]2358           
2359    def get_body_text(self, message_parts):
2360        """
2361        """
2362        self.logger.debug('function get_body_text()')
[498]2363
[507]2364        body_text = []
2365       
2366        for part in message_parts:
2367       
2368            ## Plain text part, append it
2369            #
2370            if not isinstance(part, tuple):
2371                body_text.extend(part.strip().splitlines())
2372                body_text.append("")
2373                continue
[490]2374
[507]2375            (original, filename, part) = part
2376            inline = self.inline_part(part)
[491]2377
[507]2378            ## Skip generation of attachment link if html is converted to text
2379            #
2380            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2381                s = 'Skipping attachment link for html part: %s' %(filename)
2382                self.print_unicode(s)
2383                continue
2384           
2385            if part.get_content_maintype() == 'image' and inline:
[553]2386
[507]2387                if self.system != 'discussion':
2388                    s = 'wiki image link for: %s' %(filename)
2389                    self.print_unicode(s)
2390                    body_text.append('[[Image(%s)]]' % filename)
[553]2391
[507]2392                body_text.append("")
[553]2393
[507]2394            else:
[553]2395
[507]2396                if self.system != 'discussion':
[553]2397
[507]2398                    s = 'wiki attachment link for: %s' %(filename)
2399                    self.print_unicode(s)
2400                    body_text.append('[attachment:"%s"]' % filename)
[553]2401
[507]2402                body_text.append("")
[490]2403
[507]2404        ## Convert list body_texts to string
2405        #
2406        body_text = '\r\n'.join(body_text)
2407        return body_text
[236]2408
[507]2409    def html_mailto_link(self, subject):
2410        """
2411        This function returns a HTML mailto tag with the ticket id and author email address
2412        """
2413        self.logger.debug("function html_mailto_link")
[576]2414
[507]2415        if not self.author:
2416            author = self.email_addr
2417        else:   
2418            author = self.author
[343]2419
[507]2420        if not self.parameters.mailto_cc:
2421            self.parameters.mailto_cc = ''
[592]2422           
2423        ## Bug in urllib.quote function
2424        #
2425        if isinstance(subject, unicode):
2426            subject = subject.encode('utf-8')
[434]2427
[507]2428        ## use urllib to escape the chars
2429        #
[517]2430        s = '%s?Subject=%s&cc=%s' %(
[507]2431               urllib.quote(self.email_addr),
2432               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2433               urllib.quote(self.parameters.mailto_cc)
2434               )
[343]2435
[507]2436        if self.VERSION in [ 0.10 ]:
2437            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)
2438        else:
2439            s = '[mailto:"%s" Reply to: %s]' %(s, author)
[506]2440
[507]2441        self.logger.debug("\tmailto link %s" %s)
2442        return s
[343]2443
2444########## TRAC notify section ###########################################################
2445
[507]2446    def notify(self, tkt, new=True, modtime=0):
2447        """
2448        A wrapper for the TRAC notify function. So we can use templates
2449        """
2450        self.logger.debug('function notify()')
[392]2451
[508]2452
[633]2453        class Email2TracNotifyEmail(TicketNotifyEmail):
2454
2455            def __init__(self, env):
2456                TicketNotifyEmail.__init__(self, env)
2457                self.email2trac_notify_reporter = None
2458                self.email2trac_replyto = None
2459
2460            def send(self, torcpts, ccrcpts):
2461                #print 'Yes this works'
2462                dest = self.reporter or 'anonymous'
2463                hdrs = {}
2464                hdrs['Message-ID'] = self.get_message_id(dest, self.modtime)
2465                hdrs['X-Trac-Ticket-ID'] = str(self.ticket.id)
2466                hdrs['X-Trac-Ticket-URL'] = self.data['ticket']['link']
2467                if not self.newticket:
2468                    msgid = self.get_message_id(dest)
2469                    hdrs['In-Reply-To'] = msgid
2470                    hdrs['References'] = msgid
2471
2472
2473                if self.email2trac_notify_reporter:
2474                    if not self.email2trac_notify_reporter in torcpts:
2475                        torcpts.append(sender_email)
2476
2477                if self.email2trac_replyto:
2478                    # use to rewrite reply to
2479                    # hdrs does not work, multiple reply addresses
2480                    #hdrs['Reply-To'] = 'bas.van.der.vlies@gmail.com'
2481                    self.replyto_email = self.email2trac_replyto
2482       
2483                NotifyEmail.send(self, torcpts, ccrcpts, hdrs)
2484
[508]2485        if self.parameters.dry_run  :
[561]2486                self.logger.info('DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter'])
[507]2487                return
[644]2488
[507]2489        try:
[392]2490
[644]2491
[507]2492            ## create false {abs_}href properties, to trick Notify()
2493            #
[540]2494            if self.VERSION in [0.10]:
[507]2495                self.env.abs_href = Href(self.get_config('project', 'url'))
2496                self.env.href = Href(self.get_config('project', 'url'))
[22]2497
[633]2498            tn = Email2TracNotifyEmail(self.env)
[213]2499
[633]2500            ## additionally append sender (regardeless of settings in trac.ini)
2501            #
2502            if self.parameters.notify_reporter:
2503
2504                self.logger.debug('\t Notify reporter set')
2505
2506                if not self.email_header_acl('notify_reporter_black_list', self.email_addr, False):
2507                    tn.email2trac_notify_reporter = self.email_addr
2508
2509            if self.parameters.notify_replyto_rewrite:
2510
2511                self.logger.debug('\t Notify replyto rewrite set')
2512
2513                action, value = self.parameters.notify_replyto_rewrite.split(':')
2514
2515                if action in ['use_mail_domain']:
[634]2516                    self.logger.debug('\t\t use_mail_domain:%s' %value)
[633]2517                    tn.email2trac_replyto = '%s@%s' %(self.id, value)
2518
2519                elif action in ['use_trac_smtp_replyto']:
[634]2520                    self.logger.debug('\t\t use_trac_smtp_replyto delimiter:%s' %value)
[633]2521                    dummy = self.smtp_replyto.split('@')
2522                    if len(dummy) > 1:
2523                        tn.email2trac_replyto = '%s%s%s@%s' %(dummy[0], value, self.id, dummy[1])
2524                    else:
2525                        tn.email2trac_replyto = '%s%s%s' %(dummy[0], value, self.id)
2526
[507]2527            if self.parameters.alternate_notify_template:
[222]2528
[507]2529                if self.VERSION >= 0.11:
[222]2530
[507]2531                    from trac.web.chrome import Chrome
[222]2532
[507]2533                    if  self.parameters.alternate_notify_template_update and not new:
2534                        tn.template_name = self.parameters.alternate_notify_template_update
2535                    else:
2536                        tn.template_name = self.parameters.alternate_notify_template
[222]2537
[507]2538                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2539                       
2540                else:
[222]2541
[507]2542                    tn.template_name = self.parameters.alternate_notify_template
[42]2543
[507]2544            tn.notify(tkt, new, modtime)
[41]2545
[507]2546        except Exception, e:
2547            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
[41]2548
[508]2549########## END Class Definition  ########################################################
[22]2550
[74]2551
[343]2552########## Parse Config File  ###########################################################
[22]2553
2554def ReadConfig(file, name):
[507]2555    """
2556    Parse the config file
2557    """
2558    if not os.path.isfile(file):
2559        print 'File %s does not exist' %file
2560        sys.exit(1)
[22]2561
[507]2562    config = trac_config.Configuration(file)
2563   
2564    parentdir = config.get('DEFAULT', 'parentdir')
2565    sections = config.sections()
[22]2566
[507]2567    ## use some trac internals to get the defaults
2568    #
2569    tmp = config.parser.defaults()
2570    project =  SaraDict()
[485]2571
[507]2572    for option, value in tmp.items():
2573        try:
2574            project[option] = int(value)
2575        except ValueError:
2576            project[option] = value
[485]2577
[507]2578    if name:
2579        if name in sections:
2580            project =  SaraDict()
2581            for option, value in  config.options(name):
2582                try:
2583                    project[option] = int(value)
2584                except ValueError:
2585                    project[option] = value
[485]2586
[507]2587        elif not parentdir:
2588            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2589            print "or set parentdir in the [DEFAULT] section"
2590            sys.exit(1)
[22]2591
[507]2592    ## If parentdir then set project dir to parentdir + name
2593    #
2594    if not project.has_key('project'):
2595        if not parentdir:
2596            print "You must set project or parentdir in your configuration file"
2597            sys.exit(1)
2598        elif not name:
2599            print "You must configure a  project section in your configuration file"
2600        else:
2601            project['project'] = os.path.join(parentdir, name)
[22]2602
[528]2603    ##
2604    # Save the project name
2605    #
2606    project['project_name'] = os.path.basename(project['project'])
2607
[507]2608    return project
[22]2609
[410]2610########## Setup Logging ###############################################################
[87]2611
[419]2612def setup_log(parameters, project_name, interactive=None):
[507]2613    """
[516]2614    Setup logging
[410]2615
[507]2616    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
[420]2617    would be interpreted by the ConfigParser itself.
[507]2618    """
2619    logger = logging.getLogger('email2trac %s' %project_name)
[410]2620
[507]2621    if interactive:
2622        parameters.log_type = 'stderr'
[414]2623
[507]2624    if not parameters.log_type:
2625        if sys.platform in ['win32', 'cygwin']:
[521]2626            parameters.log_type = 'eventlog'
[507]2627        else:
2628            parameters.log_type = 'syslog'
[410]2629
[507]2630    if parameters.log_type == 'file':
[415]2631
[507]2632        if not parameters.log_file:
2633            parameters.log_file = 'email2trac.log'
[415]2634
[507]2635        if not os.path.isabs(parameters.log_file):
2636            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
[410]2637
[507]2638        log_handler = logging.FileHandler(parameters.log_file)
[415]2639
[507]2640    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2641        ## Requires win32 extensions
2642        #
[532]2643        logid = "email2trac"
[507]2644        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
[410]2645
[507]2646    elif parameters.log_type in ('syslog', 'unix'):
2647        log_handler = logging.handlers.SysLogHandler('/dev/log')
[410]2648
[507]2649    elif parameters.log_type in ('stderr'):
2650        log_handler = logging.StreamHandler(sys.stderr)
[410]2651
[507]2652    else:
2653        log_handler = logging.handlers.BufferingHandler(0)
[410]2654
[507]2655    if parameters.log_format:
2656        parameters.log_format = parameters.log_format.replace('$(', '%(')
2657    else:
2658        parameters.log_format = '%(name)s: %(message)s'
[516]2659        if parameters.log_type in ('file', 'stderr'):
2660            parameters.log_format = '%(asctime)s ' + parameters.log_format
[410]2661
[507]2662    log_formatter = logging.Formatter(parameters.log_format)
2663    log_handler.setFormatter(log_formatter)
2664    logger.addHandler(log_handler)
[410]2665
[507]2666    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2667        logger.setLevel(logging.DEBUG)
2668        parameters.debug = 1
[410]2669
[507]2670    elif parameters.log_level in ['INFO'] or parameters.verbose:
2671        logger.setLevel(logging.INFO)
[410]2672
[507]2673    elif parameters.log_level in ['WARNING']:
2674        logger.setLevel(logging.WARNING)
[410]2675
[507]2676    elif parameters.log_level in ['ERROR']:
2677        logger.setLevel(logging.ERROR)
[410]2678
[507]2679    elif parameters.log_level in ['CRITICAL']:
2680        logger.setLevel(logging.CRITICAL)
[410]2681
[507]2682    else:
2683        logger.setLevel(logging.INFO)
[410]2684
[507]2685    return logger
[410]2686
[633]2687########## Own TicketNotifyEmail class ###############################################################
[410]2688
[22]2689if __name__ == '__main__':
[507]2690    ## Default config file
2691    #
[622]2692    agilo = False
[645]2693    bh_product = None
[507]2694    configfile = '@email2trac_conf@'
2695    project = ''
2696    component = ''
2697    ticket_prefix = 'default'
2698    dry_run = None
2699    verbose = None
2700    debug_interactive = None
[621]2701    virtualenv = '@virtualenv@'
[202]2702
[645]2703    SHORT_OPT = 'AB:cdE:hf:np:t:v'
[650]2704    LONG_OPT  =  ['agilo', 'bh_product=', 'component=', 'debug',
[645]2705                  'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=',
2706                  'virtualenv=', 'verbose']
[201]2707
[507]2708    try:
2709        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2710    except getopt.error,detail:
2711        print __doc__
2712        print detail
2713        sys.exit(1)
2714   
2715    project_name = None
2716    for opt,value in opts:
2717        if opt in [ '-h', '--help']:
2718            print __doc__
2719            sys.exit(0)
[622]2720        elif opt in ['-A', '--agilo']:
2721            agilo = True
[650]2722        elif opt in ['-B', '--bh_product']:
[645]2723            bh_product = value
[507]2724        elif opt in ['-c', '--component']:
2725            component = value
2726        elif opt in ['-d', '--debug']:
2727            debug_interactive = 1
[625]2728        elif opt in ['-E', '--virtualenv']:
[621]2729            virtualenv = value
[507]2730        elif opt in ['-f', '--file']:
2731            configfile = value
2732        elif opt in ['-n', '--dry-run']:
2733            dry_run = True
2734        elif opt in ['-p', '--project']:
2735            project_name = value
2736        elif opt in ['-t', '--ticket_prefix']:
2737            ticket_prefix = value
2738        elif opt in ['-v', '--verbose']:
2739            verbose = True
[621]2740
2741    if virtualenv and os.path.exists(virtualenv):
2742        activate_this = os.path.join(virtualenv, 'bin/activate_this.py')
2743        if os.path.exists(activate_this):
2744            execfile(activate_this, dict(__file__=activate_this))
2745
2746    try:
2747        from trac import __version__ as trac_version
2748        from trac import config as trac_config
2749
2750    except ImportError, detail:
2751        print "Can not find a a valid trac installation, solutions could be:"
2752        print "\tset PYTHONPATH"
2753        print "\tuse the --virtualenv <dir> option"
2754        sys.exit(1)
[507]2755   
2756    settings = ReadConfig(configfile, project_name)
[410]2757
[507]2758    ## The default prefix for ticket values in email2trac.conf
2759    #
2760    settings.ticket_prefix = ticket_prefix
2761    settings.dry_run = dry_run
2762    settings.verbose = verbose
[410]2763
[645]2764    if bh_product:
2765        settings.bh_product = bh_product
2766
[507]2767    if not settings.debug and debug_interactive:
2768        settings.debug = debug_interactive
[412]2769
[507]2770    if not settings.project:
2771        print __doc__
2772        print 'No Trac project is defined in the email2trac config file.'
2773        sys.exit(1)
[419]2774
[507]2775    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2776   
2777    if component:
2778        settings['component'] = component
[202]2779
[615]2780    ## We are only interested in the major versions
2781    # 0.12.3 --> 0.12
2782    # 1.0.2  --> 1.0
[507]2783    #
[615]2784    l = trac_version.split('.')
2785    version = '.'.join(l[0:2])
[373]2786
[507]2787    logger.debug("Found trac version: %s" %(version))
2788   
2789    try:
2790        if version == '0.10':
2791            from trac import attachment
2792            from trac.env import Environment
2793            from trac.ticket import Ticket
2794            from trac.web.href import Href
2795            from trac import util
2796            from trac.ticket.web_ui import TicketModule
[486]2797
[507]2798            #
2799            # return  util.text.to_unicode(str)
2800            #
2801            # see http://projects.edgewall.com/trac/changeset/2799
2802            from trac.ticket.notification import TicketNotifyEmail
2803            from trac import config as trac_config
2804            from trac.core import TracError
[359]2805
[633]2806        elif version in ['0.11', '0.12', '0.13', '1.0', '1.1']:
[507]2807            from trac import attachment
2808            from trac import config as trac_config
2809            from trac import util
2810            from trac.core import TracError
[554]2811            from trac.env import Environment
[507]2812            from trac.perm import PermissionSystem
[539]2813            from trac.perm import PermissionCache
[538]2814            from trac.test import Mock, MockPerm
[554]2815            from trac.ticket.api import TicketSystem
[507]2816            from trac.ticket.web_ui import TicketModule
[541]2817            from trac.web.href import Href
[368]2818
[645]2819            try:
2820                import pkg_resources
2821                pkg = pkg_resources.get_distribution('BloodhoundMultiProduct')
2822                bloodhound = pkg.version.split()[:2]
2823            except pkg_resources.DistributionNotFound:
2824                # assume no bloodhound
2825                bloodhound = None
2826
2827            if bloodhound:
2828                from multiproduct.env import Environment, ProductEnvironment
2829                from multiproduct.ticket.web_ui import (ProductTicketModule
2830                                                        as TicketModule)
2831                logger.debug("Found Bloodhound Distribution")
2832
2833                if not settings.bh_product:
2834                    print __doc__
2835                    print 'No Bloodhound project defined (bh_project) in section:%s email2trac config file.' %(settings.project)
2836                    sys.exit(1)
2837
[622]2838            if agilo:
2839
2840                try:
[646]2841
2842                    from agilo.ticket.model import AgiloTicket as Ticket
2843
[622]2844                except ImportError, detail:
2845
[646]2846                    try:
2847                        from agilo.ticket.model import Ticket
2848                    except ImportError, detail:
2849                        logger.error('Could not find Trac  Agilo environemnt')
2850                        sys.exit(0)
2851
[622]2852            else:
2853
2854                from trac.ticket import Ticket
2855
[541]2856            #
2857            # return  util.text.to_unicode(str)
2858            #
2859            # see http://projects.edgewall.com/trac/changeset/2799
2860            from trac.ticket.notification import TicketNotifyEmail
[633]2861            from trac.notification import NotifyEmail
[541]2862
[507]2863        else:
2864            logger.error('TRAC version %s is not supported' %version)
[519]2865            sys.exit(0)
[182]2866
[507]2867        ## Must be set before environment is created
2868        #
2869        if settings.has_key('python_egg_cache'):
2870            python_egg_cache = str(settings['python_egg_cache'])
2871            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
[291]2872
[507]2873        if settings.debug > 0:
2874            logger.debug('Loading environment %s', settings.project)
[359]2875
[507]2876        try:
2877            env = Environment(settings['project'], create=0)
[645]2878
2879            if bloodhound:
2880                ## possibly overkill testing if the multiproduct schema is a
2881                # new enough version
2882                #
2883                from multiproduct.env import MultiProductSystem
2884                mps = MultiProductSystem(env)
2885                if mps.get_version() > 4:
2886                    try:
2887                        env = ProductEnvironment(env, settings.bh_product, create=0)
2888                    except LookupError:
2889                        logger.error('%s is not a valid Bloodhound' %settings.bh_product)
2890                        sys.exit(0)
2891               
[507]2892        except IOError, detail:
[534]2893            logger.error("trac error: %s" %detail)
[519]2894            sys.exit(0)
[621]2895        except TracError, detail:
2896            logger.error("trac error: %s" %detail)
2897            sys.exit(0)
[333]2898
[507]2899        tktparser = TicketEmailParser(env, settings, logger, float(version))
2900        tktparser.parse(sys.stdin)
[22]2901
[507]2902    ## Catch all errors and use the logging module
2903    #
2904    except Exception, error:
[187]2905
[507]2906        etype, evalue, etb = sys.exc_info()
2907        for e in traceback.format_exception(etype, evalue, etb):
2908            logger.critical(e)
[187]2909
[507]2910        if m:
[528]2911            tktparser.save_email_for_debug(m, settings.project_name, True)
[97]2912
[507]2913        sys.exit(1)
[633]2914
[22]2915# EOB
Note: See TracBrowser for help on using the repository browser.