source: trunk/email2trac.py.in @ 533

Last change on this file since 533 was 533, checked in by bas, 13 years ago

ticket_update_by_subject enhancement, closes #253

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