source: trunk/email2trac.py.in @ 516

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

applied patch from Dennis McRitchie?, closes #236

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