source: trunk/email2trac.py.in @ 292

Last change on this file since 292 was 292, checked in by bas, 12 years ago

Do not show the message, 'This message has X attachment(s)' in the changelog of a ticket, closes #165

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