source: trunk/email2trac.py.in @ 496

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

applied patch that fixes some dicussion plugin problems, closes #231

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