source: trunk/email2trac.py.in @ 268

Last change on this file since 268 was 268, checked in by bas, 15 years ago

email2trac.py.in:

  • fixed an error in blog update function
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 40.6 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
30Changed By: Bas van der Vlies <basv@sara.nl>
31Date      : 13 September 2005
32Descr.    : Added config file and command line options, spam level
33            detection, reply address and mailto option. Unicode support
34
35Changed By: Walter de Jong <walter@sara.nl>
36Descr.    : multipart-message code and trac attachments
37
38
39The scripts reads emails from stdin and inserts directly into a Trac database.
40MIME headers are mapped as follows:
41
42        * From:      => Reporter
43                     => CC (Optional via reply_all option)
44        * Subject:   => Summary
45        * Body       => Description
46        * Component  => Can be set to SPAM via spam_level option
47
48How to use
49----------
50 * Create an config file:
51        [DEFAULT]                      # REQUIRED
52        project      : /data/trac/test # REQUIRED
53        debug        : 1               # OPTIONAL, if set print some DEBUG info
54        spam_level   : 4               # OPTIONAL, if set check for SPAM mail
55        reply_all    : 1               # OPTIONAL, if set then fill in ticket CC field
56        umask        : 022             # OPTIONAL, if set then use this umask for creation of the attachments
57        mailto_link  : 1               # OPTIONAL, if set then [mailto:<>] in description
58        mailto_cc    : basv@sara.nl    # OPTIONAL, use this address as CC in mailto line
59        ticket_update: 1               # OPTIONAL, if set then check if this is an update for a ticket
60        trac_version : 0.9             # OPTIONAL, default is 0.10
61
62        [jouvin]                       # OPTIONAL project declaration, if set both fields necessary
63        project      : /data/trac/jouvin # use -p|--project jouvin. 
64       
65 * default config file is : /etc/email2trac.conf
66
67 * Commandline opions:
68                -h,--help
69                -f,--file  <configuration file>
70                -n,--dry-run
71                -p, --project <project name>
72                -t, --ticket_prefix <name>
73
74SVN Info:
75        $Id: email2trac.py.in 268 2009-04-15 07:31:22Z bas $
76"""
77import os
78import sys
79import string
80import getopt
81import stat
82import time
83import email
84import email.Iterators
85import email.Header
86import re
87import urllib
88import unicodedata
89from stat import *
90import mimetypes
91import traceback
92
93
94# Will fail where unavailable, e.g. Windows
95#
96try:
97    import syslog
98    SYSLOG_AVAILABLE = True
99except ImportError:
100    SYSLOG_AVAILABLE = False
101
102from datetime import tzinfo, timedelta, datetime
103from trac import config as trac_config
104
105# Some global variables
106#
107trac_default_version = '0.10'
108m = None
109
110# A UTC class needed for trac version 0.11, added by
111# tbaschak at ktc dot mb dot ca
112#
113class UTC(tzinfo):
114        """UTC"""
115        ZERO = timedelta(0)
116        HOUR = timedelta(hours=1)
117       
118        def utcoffset(self, dt):
119                return self.ZERO
120               
121        def tzname(self, dt):
122                return "UTC"
123               
124        def dst(self, dt):
125                return self.ZERO
126
127
128class TicketEmailParser(object):
129        env = None
130        comment = '> '
131   
132        def __init__(self, env, parameters, version):
133                self.env = env
134
135                # Database connection
136                #
137                self.db = None
138
139                # Save parameters
140                #
141                self.parameters = parameters
142
143                # Some useful mail constants
144                #
145                self.author = None
146                self.email_addr = None
147                self.email_from = None
148                self.id = None
149
150                self.VERSION = version
151                self.DRY_RUN = parameters['dry_run']
152
153                self.get_config = self.env.config.get
154
155                if parameters.has_key('umask'):
156                        os.umask(int(parameters['umask'], 8))
157
158                if parameters.has_key('quote_attachment_filenames'):
159                        self.QUOTE_ATTACHMENT_FILENAMES = int(parameters['quote_attachment_filenames'])
160                else:
161                        self.QUOTE_ATTACHMENT_FILENAMES = 1
162
163                if parameters.has_key('debug'):
164                        self.DEBUG = int(parameters['debug'])
165                else:
166                        self.DEBUG = 0
167
168                if parameters.has_key('mailto_link'):
169                        self.MAILTO = int(parameters['mailto_link'])
170                        if parameters.has_key('mailto_cc'):
171                                self.MAILTO_CC = parameters['mailto_cc']
172                        else:
173                                self.MAILTO_CC = ''
174                else:
175                        self.MAILTO = 0
176
177                if parameters.has_key('spam_level'):
178                        self.SPAM_LEVEL = int(parameters['spam_level'])
179                else:
180                        self.SPAM_LEVEL = 0
181
182                if parameters.has_key('spam_header'):
183                        self.SPAM_HEADER = parameters['spam_header']
184                else:
185                        self.SPAM_HEADER = 'X-Spam-Score'
186
187                if parameters.has_key('email_quote'):
188                        self.EMAIL_QUOTE = str(parameters['email_quote'])
189                else:   
190                        self.EMAIL_QUOTE = '> '
191
192                if parameters.has_key('email_header'):
193                        self.EMAIL_HEADER = int(parameters['email_header'])
194                else:
195                        self.EMAIL_HEADER = 0
196
197                if parameters.has_key('alternate_notify_template'):
198                        self.notify_template = str(parameters['alternate_notify_template'])
199                else:
200                        self.notify_template = None
201
202                if parameters.has_key('alternate_notify_template_update'):
203                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
204                else:
205                        self.notify_template_update = None
206
207                if parameters.has_key('reply_all'):
208                        self.REPLY_ALL = int(parameters['reply_all'])
209                else:
210                        self.REPLY_ALL = 0
211
212                if parameters.has_key('ticket_update'):
213                        self.TICKET_UPDATE = int(parameters['ticket_update'])
214                else:
215                        self.TICKET_UPDATE = 0
216
217                if parameters.has_key('drop_spam'):
218                        self.DROP_SPAM = int(parameters['drop_spam'])
219                else:
220                        self.DROP_SPAM = 0
221
222                if parameters.has_key('verbatim_format'):
223                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
224                else:
225                        self.VERBATIM_FORMAT = 1
226
227                if parameters.has_key('reflow'):
228                        self.REFLOW = int(parameters['reflow'])
229                else:
230                        self.REFLOW = 1
231
232                if parameters.has_key('strip_signature'):
233                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
234                else:
235                        self.STRIP_SIGNATURE = 0
236
237                if parameters.has_key('strip_quotes'):
238                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
239                else:
240                        self.STRIP_QUOTES = 0
241
242                if parameters.has_key('use_textwrap'):
243                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
244                else:
245                        self.USE_TEXTWRAP = 0
246
247                if parameters.has_key('binhex'):
248                        self.BINHEX = parameters['binhex']
249                else:
250                        self.BINHEX = 'warn'
251
252                if parameters.has_key('applesingle'):
253                        self.APPLESINGLE = parameters['applesingle']
254                else:
255                        self.APPLESINGLE = 'warn'
256
257                if parameters.has_key('appledouble'):
258                        self.APPLEDOUBLE = parameters['appledouble']
259                else:
260                        self.APPLEDOUBLE = 'warn'
261
262                if parameters.has_key('python_egg_cache'):
263                        self.python_egg_cache = str(parameters['python_egg_cache'])
264                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
265
266                self.WORKFLOW = None
267                if parameters.has_key('workflow'):
268                        self.WORKFLOW = parameters['workflow']
269
270                # Use OS independend functions
271                #
272                self.TMPDIR = os.path.normcase('/tmp')
273                if parameters.has_key('tmpdir'):
274                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
275
276                if parameters.has_key('ignore_trac_user_settings'):
277                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
278                else:
279                        self.IGNORE_TRAC_USER_SETTINGS = 0
280
281        def spam(self, message):
282                """
283                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
284                # Note if Spam_level then '*' are included
285                """
286                spam = False
287                if message.has_key(self.SPAM_HEADER):
288                        spam_l = string.split(message[self.SPAM_HEADER])
289
290                        try:
291                                number = spam_l[0].count('*')
292                        except IndexError, detail:
293                                number = 0
294                               
295                        if number >= self.SPAM_LEVEL:
296                                spam = True
297                               
298                # treat virus mails as spam
299                #
300                elif message.has_key('X-Virus-found'):                 
301                        spam = True
302
303                # How to handle SPAM messages
304                #
305                if self.DROP_SPAM and spam:
306                        if self.DEBUG > 2 :
307                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
308
309                        return 'drop'   
310
311                elif spam:
312
313                        return 'Spam'   
314
315                else:
316
317                        return False
318
319        def email_header_acl(self, keyword, header_field, default):
320                """
321                This function wil check if the email address is allowed or denied
322                to send mail to the ticket list
323            """
324                try:
325                        mail_addresses = self.parameters[keyword]
326
327                        # Check if we have an empty string
328                        #
329                        if not mail_addresses:
330                                return default
331
332                except KeyError, detail:
333                        if self.DEBUG > 2 :
334                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
335
336                        return default
337
338                mail_addresses = string.split(mail_addresses, ',')
339
340                for entry in mail_addresses:
341                        entry = entry.strip()
342                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
343                        result =  TO_RE.search(header_field)
344                        if result:
345                                return True
346
347                return False
348
349        def email_to_unicode(self, message_str):
350                """
351                Email has 7 bit ASCII code, convert it to unicode with the charset
352        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
353                understands it.
354                """
355                results =  email.Header.decode_header(message_str)
356                str = None
357                for text,format in results:
358                        if format:
359                                try:
360                                        temp = unicode(text, format)
361                                except UnicodeError, detail:
362                                        # This always works
363                                        #
364                                        temp = unicode(text, 'iso-8859-15')
365                                except LookupError, detail:
366                                        #text = 'ERROR: Could not find charset: %s, please install' %format
367                                        #temp = unicode(text, 'iso-8859-15')
368                                        temp = message_str
369                                       
370                        else:
371                                temp = string.strip(text)
372                                temp = unicode(text, 'iso-8859-15')
373
374                        if str:
375                                str = '%s %s' %(str, temp)
376                        else:
377                                str = '%s' %temp
378
379                #str = str.encode('utf-8')
380                return str
381
382        def debug_body(self, message_body, tempfile=False):
383                if tempfile:
384                        import tempfile
385                        body_file = tempfile.mktemp('.email2trac')
386                else:
387                        body_file = os.path.join(self.TMPDIR, 'body.txt')
388
389                print 'TD: writing body (%s)' % body_file
390                fx = open(body_file, 'wb')
391                if not message_body:
392                        message_body = '(None)'
393                fx.write(message_body)
394                fx.close()
395                try:
396                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
397                except OSError:
398                        pass
399
400        def debug_attachments(self, message_parts):
401                n = 0
402                for part in message_parts:
403                        # Skip inline text parts
404                        if not isinstance(part, tuple):
405                                continue
406                               
407                        (original, filename, part) = part
408
409                        n = n + 1
410                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
411                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
412
413                        part_file = os.path.join(self.TMPDIR, filename)
414                        #part_file = '/var/tmp/part%d' % n
415                        print 'TD: writing part%d (%s)' % (n,part_file)
416                        fx = open(part_file, 'wb')
417                        text = part.get_payload(decode=1)
418                        if not text:
419                                text = '(None)'
420                        fx.write(text)
421                        fx.close()
422                        try:
423                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
424                        except OSError:
425                                pass
426
427        def email_header_txt(self, m):
428                """
429                Display To and CC addresses in description field
430                """
431                str = ''
432                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
433                if m['To'] and len(m['To']) > 0:
434                        str = "'''To:''' %s\r\n" %(m['To'])
435                if m['Cc'] and len(m['Cc']) > 0:
436                        str = "%s'''Cc:''' %s\r\n" % (str, m['Cc'])
437
438                return  self.email_to_unicode(str)
439
440
441        def get_sender_info(self, message):
442                """
443                Get the default author name and email address from the message
444                """
445
446                self.email_to = self.email_to_unicode(message['to'])
447                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
448
449                self.email_from = self.email_to_unicode(message['from'])
450                self.author, self.email_addr  = email.Utils.parseaddr(self.email_from)
451
452                # Trac can not handle author's name that contains spaces
453                #
454                self.author = self.email_addr
455
456                if self.IGNORE_TRAC_USER_SETTINGS:
457                        return
458
459                # Is this a registered user, use email address as search key:
460                # result:
461                #   u : login name
462                #   n : Name that the user has set in the settings tab
463                #   e : email address that the user has set in the settings tab
464                #
465                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
466                        if e and (e.lower() == self.email_addr.lower()) ]
467
468                if len(users) == 1:
469                        self.email_from = users[0][0]
470                        self.author = users[0][0]
471
472        def set_reply_fields(self, ticket, message):
473                """
474                Set all the right fields for a new ticket
475                """
476                ticket['reporter'] = self.email_from
477
478                # Put all CC-addresses in ticket CC field
479                #
480                if self.REPLY_ALL:
481                        #tos = message.get_all('to', [])
482                        ccs = message.get_all('cc', [])
483
484                        addrs = email.Utils.getaddresses(ccs)
485                        if not addrs:
486                                return
487
488                        # Remove reporter email address if notification is
489                        # on
490                        #
491                        if self.notification:
492                                try:
493                                        addrs.remove((self.author, self.email_addr))
494                                except ValueError, detail:
495                                        pass
496
497                        for name,mail in addrs:
498                                try:
499                                        mail_list = '%s, %s' %(mail_list, mail)
500                                except UnboundLocalError, detail:
501                                        mail_list = mail
502
503                        if mail_list:
504                                ticket['cc'] = self.email_to_unicode(mail_list)
505
506        def save_email_for_debug(self, message, tempfile=False):
507                if tempfile:
508                        import tempfile
509                        msg_file = tempfile.mktemp('.email2trac')
510                else:
511                        #msg_file = '/var/tmp/msg.txt'
512                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
513
514                print 'TD: saving email to %s' % msg_file
515                fx = open(msg_file, 'wb')
516                fx.write('%s' % message)
517                fx.close()
518                try:
519                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
520                except OSError:
521                        pass
522
523        def str_to_dict(self, str):
524                """
525                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
526                """
527
528                fields = string.split(str, ',')
529
530                result = dict()
531                for field in fields:
532                        try:
533                                index, value = string.split(field, '=')
534
535                                # We can not change the description of a ticket via the subject
536                                # line. The description is the body of the email
537                                #
538                                if index.lower() in ['description']:
539                                        continue
540
541                                if value:
542                                        result[index.lower()] = value
543
544                        except ValueError:
545                                pass
546
547                return result
548
549        def update_ticket_fields(self, ticket, user_dict, use_default=None):
550                """
551                This will update the ticket fields. It will check if the
552                given fields are known and if the right values are specified
553                It will only update the ticket field value:
554                        - If the field is known
555                        - If the value supplied is valid for the ticket field.
556                          If not then there are two options:
557                           1) Skip the value (use_default=None)
558                           2) Set default value for field (use_default=1)
559                """
560
561                # Build a system dictionary from the ticket fields
562                # with field as index and option as value
563                #
564                sys_dict = dict()
565                for field in ticket.fields:
566                        try:
567                                sys_dict[field['name']] = field['options']
568
569                        except KeyError:
570                                sys_dict[field['name']] = None
571                                pass
572
573                # Check user supplied fields an compare them with the
574                # system one's
575                #
576                for field,value in user_dict.items():
577                        if self.DEBUG >= 10:
578                                print  'user_field\t %s = %s' %(field,value)
579
580                        if sys_dict.has_key(field):
581
582                                # Check if value is an allowed system option, if TypeError then
583                                # every value is allowed
584                                #
585                                try:
586                                        if value in sys_dict[field]:
587                                                ticket[field] = value
588                                        else:
589                                                # Must we set a default if value is not allowed
590                                                #
591                                                if use_default:
592                                                        value = self.get_config('ticket', 'default_%s' %(field) )
593                                                        ticket[field] = value
594
595                                except TypeError:
596                                        ticket[field] = value
597
598                                if self.DEBUG >= 10:
599                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
600                                       
601        def ticket_update(self, m, id, spam):
602                """
603                If the current email is a reply to an existing ticket, this function
604                will append the contents of this email to that ticket, instead of
605                creating a new one.
606                """
607                if self.DEBUG:
608                        print "TD: ticket_update: %s" %id
609
610                # Must we update ticket fields
611                #
612                update_fields = dict()
613                try:
614                        id, keywords = string.split(id, '?')
615
616                        # Skip the last ':' character
617                        #
618                        keywords = keywords[:-1]
619                        update_fields = self.str_to_dict(keywords)
620
621                        # Strip '#'
622                        #
623                        self.id = int(id[1:])
624
625                except ValueError:
626                        # Strip '#' and ':'
627                        #
628                        self.id = int(id[1:-1])
629
630
631                # When is the change committed
632                #
633                #
634                if self.VERSION == 0.11:
635                        utc = UTC()
636                        when = datetime.now(utc)
637                else:
638                        when = int(time.time())
639
640                try:
641                        tkt = Ticket(self.env, self.id, self.db)
642                except util.TracError, detail:
643                        # Not a valid ticket
644                        self.id = None
645                        return False
646
647                # reopen the ticket if it is was closed
648                # We must use the ticket workflow framework
649                #
650                if tkt['status'] in ['closed']:
651
652                        #print controller.actions['reopen']
653                        #
654                        # As reference 
655                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
656                        #
657                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
658                        #print 'controller : ', a
659                        #
660                        #b = controller.get_all_status()
661                        #print 'get all status: ', b
662                        #
663                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
664                        #print 'get_ticket_changes :', b
665
666                        if self.WORKFLOW and (self.VERSION in ['0.11']) :
667                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
668                                from trac.test import Mock, MockPerm
669
670                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
671
672                                controller = ConfigurableTicketWorkflow(self.env)
673                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
674
675                                if self.DEBUG:
676                                        print 'TD: Workflow ticket update fields: ', fields
677
678                                for key in fields.keys():
679                                        tkt[key] = fields[key]
680
681                        else:
682                                tkt['status'] = 'reopened'
683                                tkt['resolution'] = ''
684
685                # Must we update some ticket fields properties
686                #
687                if update_fields:
688                        self.update_ticket_fields(tkt, update_fields)
689
690                message_parts = self.get_message_parts(m)
691                message_parts = self.unique_attachment_names(message_parts)
692
693                if self.EMAIL_HEADER:
694                        message_parts.insert(0, self.email_header_txt(m))
695
696                body_text = self.body_text(message_parts)
697
698                if body_text.strip() or update_fields:
699                        if self.DRY_RUN:
700                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
701                        else:
702                                tkt.save_changes(self.author, body_text, when)
703
704                if self.VERSION  == 0.9:
705                        str = self.attachments(message_parts, True)
706                else:
707                        str = self.attachments(message_parts)
708
709                if self.notification and not spam:
710                        self.notify(tkt, False, when)
711
712                return True
713
714        def set_ticket_fields(self, ticket):
715                """
716                set the ticket fields to value specified
717                        - /etc/email2trac.conf with <prefix>_<field>
718                        - trac default values, trac.ini
719                """
720                user_dict = dict()
721
722                for field in ticket.fields:
723
724                        name = field['name']
725
726                        # skip some fields like resolution
727                        #
728                        if name in [ 'resolution' ]:
729                                continue
730
731                        # default trac value
732                        #
733                        if not field.get('custom'):
734                                value = self.get_config('ticket', 'default_%s' %(name) )
735                        else:
736                                value = field.get('value')
737                                options = field.get('options')
738                                if value and options and value not in options:
739                                        value = options[int(value)]
740
741                        if self.DEBUG > 10:
742                                print 'trac.ini name %s = %s' %(name, value)
743
744                        prefix = self.parameters['ticket_prefix']
745                        try:
746                                value = self.parameters['%s_%s' %(prefix, name)]
747                                if self.DEBUG > 10:
748                                        print 'email2trac.conf %s = %s ' %(name, value)
749
750                        except KeyError, detail:
751                                pass
752               
753                        if self.DEBUG:
754                                print 'user_dict[%s] = %s' %(name, value)
755
756                        user_dict[name] = value
757
758                self.update_ticket_fields(ticket, user_dict, use_default=1)
759
760                # Set status ticket
761                #`
762                ticket['status'] = 'new'
763
764
765
766        def new_ticket(self, msg, subject, spam, set_fields = None):
767                """
768                Create a new ticket
769                """
770                if self.DEBUG:
771                        print "TD: new_ticket"
772
773                tkt = Ticket(self.env)
774                self.set_ticket_fields(tkt)
775
776                # Old style setting for component, will be removed
777                #
778                if spam:
779                        tkt['component'] = 'Spam'
780
781                elif self.parameters.has_key('component'):
782                        tkt['component'] = self.parameters['component']
783
784                if not msg['Subject']:
785                        tkt['summary'] = u'(No subject)'
786                else:
787                        tkt['summary'] = subject
788
789                self.set_reply_fields(tkt, msg)
790
791                if set_fields:
792                        rest, keywords = string.split(set_fields, '?')
793
794                        if keywords:
795                                update_fields = self.str_to_dict(keywords)
796                                self.update_ticket_fields(tkt, update_fields)
797
798                # produce e-mail like header
799                #
800                head = ''
801                if self.EMAIL_HEADER > 0:
802                        head = self.email_header_txt(msg)
803                       
804                message_parts = self.get_message_parts(msg)
805                message_parts = self.unique_attachment_names(message_parts)
806               
807                if self.EMAIL_HEADER > 0:
808                        message_parts.insert(0, self.email_header_txt(msg))
809                       
810                body_text = self.body_text(message_parts)
811
812                tkt['description'] = body_text
813
814                #when = int(time.time())
815                #
816                utc = UTC()
817                when = datetime.now(utc)
818
819                if not self.DRY_RUN:
820                        self.id = tkt.insert()
821               
822                changed = False
823                comment = ''
824
825                # Rewrite the description if we have mailto enabled
826                #
827                if self.MAILTO:
828                        changed = True
829                        comment = u'\nadded mailto line\n'
830                        mailto = self.html_mailto_link( m['Subject'], body_text)
831
832                        tkt['description'] = u'%s\r\n%s%s\r\n' \
833                                %(head, mailto, body_text)
834
835                str =  self.attachments(message_parts)
836                if str:
837                        changed = True
838                        comment = '%s\n%s\n' %(comment, str)
839
840                if changed:
841                        if self.DRY_RUN:
842                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
843                        else:
844                                tkt.save_changes(self.author, comment)
845                                #print tkt.get_changelog(self.db, when)
846
847                if self.notification and not spam:
848                        self.notify(tkt, True)
849
850
851        def blog(self, id):
852                """
853                The blog create/update function
854                """
855                # import the modules
856                #
857                from tracfullblog.core import FullBlogCore
858                from tracfullblog.model import BlogPost, BlogCommen
859
860                # instantiate blog core
861                blog = FullBlogCore(self.env)
862                req = ''
863               
864                if id:
865
866                        # update blog
867                        #
868                        comment = BlogComment(self.env, id)
869                        comment.author = self.author
870                        comment.comment = self.get_body_text(m)
871                        blog.create_comment(req, comment)
872
873                else:
874                        # create blog
875                        #
876                        import time
877                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
878
879                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
880                       
881                        post.author = self.author
882                        post.title = self.email_to_unicode(m['Subject'])
883                        post.body = self.get_body_text(m)
884                       
885                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
886
887
888        def parse(self, fp):
889                global m
890
891                m = email.message_from_file(fp)
892               
893                if not m:
894                        if self.DEBUG:
895                                print "TD: This is not a valid email message format"
896                        return
897                       
898                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
899                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
900
901                if self.DEBUG > 1:        # save the entire e-mail message text
902                        message_parts = self.get_message_parts(m)
903                        message_parts = self.unique_attachment_names(message_parts)
904                        self.save_email_for_debug(m, True)
905                        body_text = self.body_text(message_parts)
906                        self.debug_body(body_text, True)
907                        self.debug_attachments(message_parts)
908
909                self.db = self.env.get_db_cnx()
910                self.get_sender_info(m)
911
912                if not self.email_header_acl('white_list', self.email_addr, True):
913                        if self.DEBUG > 1 :
914                                print 'Message rejected : %s not in white list' %(self.email_addr)
915                        return False
916
917                if self.email_header_acl('black_list', self.email_addr, False):
918                        if self.DEBUG > 1 :
919                                print 'Message rejected : %s in black list' %(self.email_addr)
920                        return False
921
922                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
923                        if self.DEBUG > 1 :
924                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
925                        return False
926
927                # If drop the message
928                #
929                if self.spam(m) == 'drop':
930                        return False
931
932                elif self.spam(m) == 'spam':
933                        spam_msg = True
934
935                else:
936                        spam_msg = False
937
938                if self.get_config('notification', 'smtp_enabled') in ['true']:
939                        self.notification = 1
940                else:
941                        self.notification = 0
942
943                # Check if  FullBlogPlugin is installed
944                #
945                blog_enabled = None
946                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
947                        blog_enabled = True
948                       
949                if not m['Subject']:
950                        return False
951                else:
952                        subject  = self.email_to_unicode(m['Subject'])         
953
954                #
955                # [hic] #1529: Re: LRZ
956                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
957                #
958                TICKET_RE = re.compile(r"""
959                        (?P<blog>blog:(?P<blog_id>\w*))
960                        |(?P<new_fields>[#][?].*)
961                        |(?P<reply>[#][\d]+:)
962                        |(?P<reply_fields>[#][\d]+\?.*?:)
963                        """, re.VERBOSE)
964
965                # Find out if this is a ticket or a blog
966                #
967                result =  TICKET_RE.search(subject)
968
969                if result:
970                        if result.group('blog') and blog_enabled:
971                                self.blog(result.group('blog_id'))
972
973                        # update ticket + fields
974                        #
975                        if result.group('reply_fields') and self.TICKET_UPDATE:
976                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
977
978                        # Update ticket
979                        #
980                        elif result.group('reply') and self.TICKET_UPDATE:
981                                self.ticket_update(m, result.group('reply'), spam_msg)
982
983                        # New ticket + fields
984                        #
985                        elif result.group('new_fields'):
986                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
987
988                # Create ticket
989                #
990                else:
991                        self.new_ticket(m, subject, spam_msg)
992
993        def strip_signature(self, text):
994                """
995                Strip signature from message, inspired by Mailman software
996                """
997                body = []
998                for line in text.splitlines():
999                        if line == '-- ':
1000                                break
1001                        body.append(line)
1002
1003                return ('\n'.join(body))
1004
1005        def reflow(self, text, delsp = 0):
1006                """
1007                Reflow the message based on the format="flowed" specification (RFC 3676)
1008                """
1009                flowedlines = []
1010                quotelevel = 0
1011                prevflowed = 0
1012
1013                for line in text.splitlines():
1014                        from re import match
1015                       
1016                        # Figure out the quote level and the content of the current line
1017                        m = match('(>*)( ?)(.*)', line)
1018                        linequotelevel = len(m.group(1))
1019                        line = m.group(3)
1020
1021                        # Determine whether this line is flowed
1022                        if line and line != '-- ' and line[-1] == ' ':
1023                                flowed = 1
1024                        else:
1025                                flowed = 0
1026
1027                        if flowed and delsp and line and line[-1] == ' ':
1028                                line = line[:-1]
1029
1030                        # If the previous line is flowed, append this line to it
1031                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1032                                flowedlines[-1] += line
1033                        # Otherwise, start a new line
1034                        else:
1035                                flowedlines.append('>' * linequotelevel + line)
1036
1037                        prevflowed = flowed
1038                       
1039
1040                return '\n'.join(flowedlines)
1041
1042        def strip_quotes(self, text):
1043                """
1044                Strip quotes from message by Nicolas Mendoza
1045                """
1046                body = []
1047                for line in text.splitlines():
1048                        if line.startswith(self.EMAIL_QUOTE):
1049                                continue
1050                        body.append(line)
1051
1052                return ('\n'.join(body))
1053
1054        def wrap_text(self, text, replace_whitespace = False):
1055                """
1056                Will break a lines longer then given length into several small
1057                lines of size given length
1058                """
1059                import textwrap
1060
1061                LINESEPARATOR = '\n'
1062                reformat = ''
1063
1064                for s in text.split(LINESEPARATOR):
1065                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1066                        if tmp:
1067                                reformat = '%s\n%s' %(reformat,tmp)
1068                        else:
1069                                reformat = '%s\n' %reformat
1070
1071                return reformat
1072
1073                # Python2.4 and higher
1074                #
1075                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1076                #
1077
1078
1079        def get_message_parts(self, msg):
1080                """
1081                parses the email message and returns a list of body parts and attachments
1082                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1083                """
1084                message_parts = []
1085               
1086                # This is used to figure out when we are inside an AppleDouble container
1087                # AppleDouble containers consists of two parts: Mac-specific file data, and platform-independent data
1088                # We strip away Mac-specific stuff
1089                appledouble_parts = []
1090
1091                for part in msg.walk():
1092                        if self.DEBUG:
1093                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1094                               
1095                        # Check whether we just finished processing an AppleDouble container
1096                        if part not in appledouble_parts:
1097                                appledouble_parts = []
1098
1099                        # Special handling for BinHex attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1100                        if part.get_content_type() == 'application/mac-binhex40':
1101                                if self.BINHEX == 'warn':
1102                                        message_parts.append("'''A BinHex attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1103                                        continue
1104                                elif self.BINHEX == 'drop':
1105                                        continue
1106
1107                        # Special handling for AppleSingle attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1108                        if part.get_content_type() == 'application/applefile' and not part in appledouble_parts:
1109                                if self.APPLESINGLE == 'warn':
1110                                        message_parts.append("'''An AppleSingle attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1111                                        continue
1112                                elif self.APPLESINGLE == 'drop':
1113                                        continue
1114
1115                        # Special handling for the Mac-specific part of AppleDouble attachments. Options are strip (leave out with no warning), warn (and leave out), and keep
1116                        if part.get_content_type() == 'application/applefile':
1117                                if self.APPLEDOUBLE == 'warn':
1118                                        message_parts.append("'''The resource fork of an attachment named '%s' was removed.'''" % part.get_filename())
1119                                        continue
1120                                elif self.APPLEDOUBLE == 'strip':
1121                                        continue
1122
1123                        # If we entering an AppleDouble container, set up appledouble_parts so that we know what to do with its subparts
1124                        if part.get_content_type() == 'multipart/appledouble':
1125                                appledouble_parts = part.get_payload()
1126                                continue
1127
1128                        # Any other multipart/* is just a container for multipart messages
1129                        if part.get_content_maintype() == 'multipart':
1130                                continue
1131
1132                        # 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"
1133                        inline = self.inline_part(part)
1134
1135                        # Inline text parts are where the body is
1136                        if part.get_content_type() == 'text/plain' and inline:
1137                                if self.DEBUG:
1138                                        print 'TD:               Inline body part'
1139
1140                                # Try to decode, if fails then do not decode
1141                                #
1142                                body_text = part.get_payload(decode=1)
1143                                if not body_text:                       
1144                                        body_text = part.get_payload(decode=0)
1145
1146                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1147                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1148
1149                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1150                                        body_text = self.reflow(body_text, delsp == 'yes')
1151       
1152                                if self.STRIP_SIGNATURE:
1153                                        body_text = self.strip_signature(body_text)
1154
1155                                if self.STRIP_QUOTES:
1156                                        body_text = self.strip_quotes(body_text)
1157
1158                                if self.USE_TEXTWRAP:
1159                                        body_text = self.wrap_text(body_text)
1160
1161                                # Get contents charset (iso-8859-15 if not defined in mail headers)
1162                                #
1163                                charset = part.get_content_charset()
1164                                if not charset:
1165                                        charset = 'iso-8859-15'
1166
1167                                try:
1168                                        ubody_text = unicode(body_text, charset)
1169
1170                                except UnicodeError, detail:
1171                                        ubody_text = unicode(body_text, 'iso-8859-15')
1172
1173                                except LookupError, detail:
1174                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1175
1176                                if self.VERBATIM_FORMAT:
1177                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1178                                else:
1179                                        message_parts.append('%s' %ubody_text)
1180                        else:
1181                                if self.DEBUG:
1182                                        print 'TD:               Filename: %s' % part.get_filename()
1183
1184                                message_parts.append((part.get_filename(), part))
1185
1186                return message_parts
1187               
1188        def unique_attachment_names(self, message_parts):
1189                renamed_parts = []
1190                attachment_names = set()
1191                for part in message_parts:
1192                       
1193                        # If not an attachment, leave it alone
1194                        if not isinstance(part, tuple):
1195                                renamed_parts.append(part)
1196                                continue
1197                               
1198                        (filename, part) = part
1199                        # Decode the filename
1200                        if filename:
1201                                filename = self.email_to_unicode(filename)                     
1202                        # If no name, use a default one
1203                        else:
1204                                filename = 'untitled-part'
1205
1206                                # Guess the extension from the content type, use non strict mode
1207                                # some additional non-standard but commonly used MIME types
1208                                # are also recognized
1209                                #
1210                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1211                                if not ext:
1212                                        ext = '.bin'
1213
1214                                filename = '%s%s' % (filename, ext)
1215
1216                        # Discard relative paths in attachment names
1217                        filename = filename.replace('\\', '/').replace(':', '/')
1218                        filename = os.path.basename(filename)
1219
1220                        # We try to normalize the filename to utf-8 NFC if we can.
1221                        # Files uploaded from OS X might be in NFD.
1222                        # Check python version and then try it
1223                        #
1224                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1225                                try:
1226                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1227                                except TypeError:
1228                                        pass
1229                                       
1230                        if self.QUOTE_ATTACHMENT_FILENAMES:
1231                                filename = urllib.quote(filename)
1232
1233                        # Make the filename unique for this ticket
1234                        num = 0
1235                        unique_filename = filename
1236                        filename, ext = os.path.splitext(filename)
1237
1238                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
1239                                num += 1
1240                                unique_filename = "%s-%s%s" % (filename, num, ext)
1241                               
1242                        if self.DEBUG:
1243                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1244
1245                        attachment_names.add(unique_filename)
1246
1247                        renamed_parts.append((filename, unique_filename, part))
1248               
1249                return renamed_parts
1250                       
1251        def inline_part(self, part):
1252                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1253               
1254                       
1255        def attachment_exists(self, filename):
1256
1257                if self.DEBUG:
1258                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
1259
1260                # We have no valid ticket id
1261                #
1262                if not self.id:
1263                        return False
1264
1265                try:
1266                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1267                        return True
1268                except attachment.ResourceNotFound:
1269                        return False
1270                       
1271        def body_text(self, message_parts):
1272                body_text = []
1273               
1274                for part in message_parts:
1275                        # Plain text part, append it
1276                        if not isinstance(part, tuple):
1277                                body_text.extend(part.strip().splitlines())
1278                                body_text.append("")
1279                                continue
1280                               
1281                        (original, filename, part) = part
1282                        inline = self.inline_part(part)
1283                       
1284                        if part.get_content_maintype() == 'image' and inline:
1285                                body_text.append('[[Image(%s)]]' % filename)
1286                                body_text.append("")
1287                        else:
1288                                body_text.append('[attachment:"%s"]' % filename)
1289                                body_text.append("")
1290                               
1291                body_text = '\r\n'.join(body_text)
1292                return body_text
1293
1294        def notify(self, tkt, new=True, modtime=0):
1295                """
1296                A wrapper for the TRAC notify function. So we can use templates
1297                """
1298                if self.DRY_RUN:
1299                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1300                                return
1301                try:
1302                        # create false {abs_}href properties, to trick Notify()
1303                        #
1304                        if not self.VERSION == 0.11:
1305                                self.env.abs_href = Href(self.get_config('project', 'url'))
1306                                self.env.href = Href(self.get_config('project', 'url'))
1307
1308                        tn = TicketNotifyEmail(self.env)
1309
1310                        if self.notify_template:
1311
1312                                if self.VERSION == 0.11:
1313
1314                                        from trac.web.chrome import Chrome
1315
1316                                        if self.notify_template_update and not new:
1317                                                tn.template_name = self.notify_template_update
1318                                        else:
1319                                                tn.template_name = self.notify_template
1320
1321                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1322                                               
1323                                else:
1324
1325                                        tn.template_name = self.notify_template;
1326
1327                        tn.notify(tkt, new, modtime)
1328
1329                except Exception, e:
1330                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1331
1332        def html_mailto_link(self, subject, body):
1333                """
1334                This function returns a HTML mailto tag with the ticket id and author email address
1335                """
1336                if not self.author:
1337                        author = self.email_addr
1338                else:   
1339                        author = self.author
1340
1341                # use urllib to escape the chars
1342                #
1343                str = 'mailto:%s?Subject=%s&Cc=%s' %(
1344                       urllib.quote(self.email_addr),
1345                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1346                           urllib.quote(self.MAILTO_CC)
1347                           )
1348
1349                str = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(str, author)
1350                return str
1351
1352        def attachments(self, message_parts, update=False):
1353                '''
1354                save any attachments as files in the ticket's directory
1355                '''
1356                if self.DRY_RUN:
1357                        print "DRY_RUN: no attachments saved"
1358                        return ''
1359
1360                count = 0
1361
1362                # Get Maxium attachment size
1363                #
1364                max_size = int(self.get_config('attachment', 'max_size'))
1365                status   = ''
1366               
1367                for part in message_parts:
1368                        # Skip body parts
1369                        if not isinstance(part, tuple):
1370                                continue
1371                               
1372                        (original, filename, part) = part
1373                        #
1374                        # Must be tuneables HvB
1375                        #
1376                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1377                        text = part.get_payload(decode=1)
1378                        if not text:
1379                                text = '(None)'
1380                        fd.write(text)
1381                        fd.close()
1382
1383                        # get the file_size
1384                        #
1385                        stats = os.lstat(path)
1386                        file_size = stats[stat.ST_SIZE]
1387
1388                        # Check if the attachment size is allowed
1389                        #
1390                        if (max_size != -1) and (file_size > max_size):
1391                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1392                                        %(status, original, file_size, max_size)
1393
1394                                os.unlink(path)
1395                                continue
1396                        else:
1397                                count = count + 1
1398                                       
1399                        # Insert the attachment
1400                        #
1401                        fd = open(path, 'rb')
1402                        att = attachment.Attachment(self.env, 'ticket', self.id)
1403
1404                        # This will break the ticket_update system, the body_text is vaporized
1405                        # ;-(
1406                        #
1407                        if not update:
1408                                att.author = self.author
1409                                att.description = self.email_to_unicode('Added by email2trac')
1410
1411                        att.insert(filename, fd, file_size)
1412                        #except  util.TracError, detail:
1413                        #       print detail
1414
1415                        # Remove the created temporary filename
1416                        #
1417                        fd.close()
1418                        os.unlink(path)
1419
1420                # Return how many attachments
1421                #
1422                status = 'This message has %d attachment(s)\n%s' %(count, status)
1423                return status
1424
1425
1426def mkdir_p(dir, mode):
1427        '''do a mkdir -p'''
1428
1429        arr = string.split(dir, '/')
1430        path = ''
1431        for part in arr:
1432                path = '%s/%s' % (path, part)
1433                try:
1434                        stats = os.stat(path)
1435                except OSError:
1436                        os.mkdir(path, mode)
1437
1438def ReadConfig(file, name):
1439        """
1440        Parse the config file
1441        """
1442        if not os.path.isfile(file):
1443                print 'File %s does not exist' %file
1444                sys.exit(1)
1445
1446        config = trac_config.Configuration(file)
1447
1448        # Use given project name else use defaults
1449        #
1450        if name:
1451                sections = config.sections()
1452                if not name in sections:
1453                        print "Not a valid project name: %s" %name
1454                        print "Valid names: %s" %sections
1455                        sys.exit(1)
1456
1457                project =  dict()
1458                for option, value in  config.options(name):
1459                        project[option] = value
1460
1461        else:
1462                # use some trac internales to get the defaults
1463                #
1464                project = config.parser.defaults()
1465
1466        return project
1467
1468
1469if __name__ == '__main__':
1470        # Default config file
1471        #
1472        configfile = '@email2trac_conf@'
1473        project = ''
1474        component = ''
1475        ticket_prefix = 'default'
1476        dry_run = None
1477
1478        ENABLE_SYSLOG = 0
1479
1480
1481        SHORT_OPT = 'chf:np:t:'
1482        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
1483
1484        try:
1485                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1486        except getopt.error,detail:
1487                print __doc__
1488                print detail
1489                sys.exit(1)
1490       
1491        project_name = None
1492        for opt,value in opts:
1493                if opt in [ '-h', '--help']:
1494                        print __doc__
1495                        sys.exit(0)
1496                elif opt in ['-c', '--component']:
1497                        component = value
1498                elif opt in ['-f', '--file']:
1499                        configfile = value
1500                elif opt in ['-n', '--dry-run']:
1501                        dry_run = True
1502                elif opt in ['-p', '--project']:
1503                        project_name = value
1504                elif opt in ['-t', '--ticket_prefix']:
1505                        ticket_prefix = value
1506       
1507        settings = ReadConfig(configfile, project_name)
1508        if not settings.has_key('project'):
1509                print __doc__
1510                print 'No Trac project is defined in the email2trac config file.'
1511                sys.exit(1)
1512       
1513        if component:
1514                settings['component'] = component
1515
1516        # The default prefix for ticket values in email2trac.conf
1517        #
1518        settings['ticket_prefix'] = ticket_prefix
1519        settings['dry_run'] = dry_run
1520       
1521        if settings.has_key('trac_version'):
1522                version = settings['trac_version']
1523        else:
1524                version = trac_default_version
1525
1526
1527        #debug HvB
1528        #print settings
1529
1530        try:
1531                if version == '0.9':
1532                        from trac import attachment
1533                        from trac.env import Environment
1534                        from trac.ticket import Ticket
1535                        from trac.web.href import Href
1536                        from trac import util
1537                        from trac.Notify import TicketNotifyEmail
1538                elif version == '0.10':
1539                        from trac import attachment
1540                        from trac.env import Environment
1541                        from trac.ticket import Ticket
1542                        from trac.web.href import Href
1543                        from trac import util
1544                        #
1545                        # return  util.text.to_unicode(str)
1546                        #
1547                        # see http://projects.edgewall.com/trac/changeset/2799
1548                        from trac.ticket.notification import TicketNotifyEmail
1549                        from trac import config as trac_config
1550                elif version == '0.11':
1551                        from trac import attachment
1552                        from trac.env import Environment
1553                        from trac.ticket import Ticket
1554                        from trac.web.href import Href
1555                        from trac import config as trac_config
1556                        from trac import util
1557
1558
1559                        #
1560                        # return  util.text.to_unicode(str)
1561                        #
1562                        # see http://projects.edgewall.com/trac/changeset/2799
1563                        from trac.ticket.notification import TicketNotifyEmail
1564                else:
1565                        print 'TRAC version %s is not supported' %version
1566                        sys.exit(1)
1567                       
1568                if settings.has_key('enable_syslog'):
1569                        if SYSLOG_AVAILABLE:
1570                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1571
1572                env = Environment(settings['project'], create=0)
1573                tktparser = TicketEmailParser(env, settings, float(version))
1574                tktparser.parse(sys.stdin)
1575
1576        # Catch all errors ans log to SYSLOG if we have enabled this
1577        # else stdout
1578        #
1579        except Exception, error:
1580                if ENABLE_SYSLOG:
1581                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1582
1583                        etype, evalue, etb = sys.exc_info()
1584                        for e in traceback.format_exception(etype, evalue, etb):
1585                                syslog.syslog(e)
1586
1587                        syslog.closelog()
1588                else:
1589                        traceback.print_exc()
1590
1591                if m:
1592                        tktparser.save_email_for_debug(m, True)
1593
1594                sys.exit(1)
1595# EOB
Note: See TracBrowser for help on using the repository browser.