source: trunk/email2trac.py.in @ 287

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

email2trac.py.in:

  • added var self.email_name. Now we can use the name and email address and decide which we use for reporter field ticket
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 40.8 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 287 2009-10-14 11:54:21Z 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     = Nome
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                if parameters.has_key('python_egg_cache'):
253                        self.python_egg_cache = str(parameters['python_egg_cache'])
254                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
255
256                self.WORKFLOW = None
257                if parameters.has_key('workflow'):
258                        self.WORKFLOW = parameters['workflow']
259
260                # Use OS independend functions
261                #
262                self.TMPDIR = os.path.normcase('/tmp')
263                if parameters.has_key('tmpdir'):
264                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
265
266                if parameters.has_key('ignore_trac_user_settings'):
267                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
268                else:
269                        self.IGNORE_TRAC_USER_SETTINGS = 0
270
271        def spam(self, message):
272                """
273                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
274                # Note if Spam_level then '*' are included
275                """
276                spam = False
277                if message.has_key(self.SPAM_HEADER):
278                        spam_l = string.split(message[self.SPAM_HEADER])
279
280                        try:
281                                number = spam_l[0].count('*')
282                        except IndexError, detail:
283                                number = 0
284                               
285                        if number >= self.SPAM_LEVEL:
286                                spam = True
287                               
288                # treat virus mails as spam
289                #
290                elif message.has_key('X-Virus-found'):                 
291                        spam = True
292
293                # How to handle SPAM messages
294                #
295                if self.DROP_SPAM and spam:
296                        if self.DEBUG > 2 :
297                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
298
299                        return 'drop'   
300
301                elif spam:
302
303                        return 'Spam'   
304
305                else:
306
307                        return False
308
309        def email_header_acl(self, keyword, header_field, default):
310                """
311                This function wil check if the email address is allowed or denied
312                to send mail to the ticket list
313            """
314                try:
315                        mail_addresses = self.parameters[keyword]
316
317                        # Check if we have an empty string
318                        #
319                        if not mail_addresses:
320                                return default
321
322                except KeyError, detail:
323                        if self.DEBUG > 2 :
324                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
325
326                        return default
327
328                mail_addresses = string.split(mail_addresses, ',')
329
330                for entry in mail_addresses:
331                        entry = entry.strip()
332                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
333                        result =  TO_RE.search(header_field)
334                        if result:
335                                return True
336
337                return False
338
339        def email_to_unicode(self, message_str):
340                """
341                Email has 7 bit ASCII code, convert it to unicode with the charset
342        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
343                understands it.
344                """
345                results =  email.Header.decode_header(message_str)
346                str = None
347                for text,format in results:
348                        if format:
349                                try:
350                                        temp = unicode(text, format)
351                                except UnicodeError, detail:
352                                        # This always works
353                                        #
354                                        temp = unicode(text, 'iso-8859-15')
355                                except LookupError, detail:
356                                        #text = 'ERROR: Could not find charset: %s, please install' %format
357                                        #temp = unicode(text, 'iso-8859-15')
358                                        temp = message_str
359                                       
360                        else:
361                                temp = string.strip(text)
362                                temp = unicode(text, 'iso-8859-15')
363
364                        if str:
365                                str = '%s %s' %(str, temp)
366                        else:
367                                str = '%s' %temp
368
369                #str = str.encode('utf-8')
370                return str
371
372        def debug_body(self, message_body, tempfile=False):
373                if tempfile:
374                        import tempfile
375                        body_file = tempfile.mktemp('.email2trac')
376                else:
377                        body_file = os.path.join(self.TMPDIR, 'body.txt')
378
379                print 'TD: writing body (%s)' % body_file
380                fx = open(body_file, 'wb')
381                if not message_body:
382                        message_body = '(None)'
383
384                message_body = message_body.encode('utf-8')
385                #message_body = unicode(message_body, 'iso-8859-15')
386
387                fx.write(message_body)
388                fx.close()
389                try:
390                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
391                except OSError:
392                        pass
393
394        def debug_attachments(self, message_parts):
395                n = 0
396                for part in message_parts:
397                        # Skip inline text parts
398                        if not isinstance(part, tuple):
399                                continue
400                               
401                        (original, filename, part) = part
402
403                        n = n + 1
404                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
405                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
406
407                        part_file = os.path.join(self.TMPDIR, filename)
408                        #part_file = '/var/tmp/part%d' % n
409                        print 'TD: writing part%d (%s)' % (n,part_file)
410                        fx = open(part_file, 'wb')
411                        text = part.get_payload(decode=1)
412                        if not text:
413                                text = '(None)'
414                        fx.write(text)
415                        fx.close()
416                        try:
417                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
418                        except OSError:
419                                pass
420
421        def email_header_txt(self, m):
422                """
423                Display To and CC addresses in description field
424                """
425                str = ''
426                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
427                if m['To'] and len(m['To']) > 0:
428                        str = "'''To:''' %s\r\n" %(m['To'])
429                if m['Cc'] and len(m['Cc']) > 0:
430                        str = "%s'''Cc:''' %s\r\n" % (str, m['Cc'])
431
432                return  self.email_to_unicode(str)
433
434
435        def get_sender_info(self, message):
436                """
437                Get the default author name and email address from the message
438                """
439
440                self.email_to = self.email_to_unicode(message['to'])
441                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
442
443                self.email_from = self.email_to_unicode(message['from'])
444                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
445
446                # Trac can not handle author's name that contains spaces
447                #
448                self.author = self.email_addr
449
450                if self.IGNORE_TRAC_USER_SETTINGS:
451                        return
452
453                # Is this a registered user, use email address as search key:
454                # result:
455                #   u : login name
456                #   n : Name that the user has set in the settings tab
457                #   e : email address that the user has set in the settings tab
458                #
459                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
460                        if e and (e.lower() == self.email_addr.lower()) ]
461
462                if len(users) == 1:
463                        self.email_from = users[0][0]
464                        self.author = users[0][0]
465
466        def set_reply_fields(self, ticket, message):
467                """
468                Set all the right fields for a new ticket
469                """
470
471                ## Only use name or email adress
472                #ticket['reporter'] = self.email_from
473                ticket['reporter'] = self.author
474
475
476                # Put all CC-addresses in ticket CC field
477                #
478                if self.REPLY_ALL:
479                        #tos = message.get_all('to', [])
480                        ccs = message.get_all('cc', [])
481
482                        addrs = email.Utils.getaddresses(ccs)
483                        if not addrs:
484                                return
485
486                        # Remove reporter email address if notification is
487                        # on
488                        #
489                        if self.notification:
490                                try:
491                                        addrs.remove((self.author, self.email_addr))
492                                except ValueError, detail:
493                                        pass
494
495                        for name,mail in addrs:
496                                try:
497                                        mail_list = '%s, %s' %(mail_list, mail)
498                                except UnboundLocalError, detail:
499                                        mail_list = mail
500
501                        if mail_list:
502                                ticket['cc'] = self.email_to_unicode(mail_list)
503
504        def save_email_for_debug(self, message, tempfile=False):
505                if tempfile:
506                        import tempfile
507                        msg_file = tempfile.mktemp('.email2trac')
508                else:
509                        #msg_file = '/var/tmp/msg.txt'
510                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
511
512                print 'TD: saving email to %s' % msg_file
513                fx = open(msg_file, 'wb')
514                fx.write('%s' % message)
515                fx.close()
516                try:
517                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
518                except OSError:
519                        pass
520
521        def str_to_dict(self, str):
522                """
523                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
524                """
525
526                fields = string.split(str, ',')
527
528                result = dict()
529                for field in fields:
530                        try:
531                                index, value = string.split(field, '=')
532
533                                # We can not change the description of a ticket via the subject
534                                # line. The description is the body of the email
535                                #
536                                if index.lower() in ['description']:
537                                        continue
538
539                                if value:
540                                        result[index.lower()] = value
541
542                        except ValueError:
543                                pass
544
545                return result
546
547        def update_ticket_fields(self, ticket, user_dict, use_default=None):
548                """
549                This will update the ticket fields. It will check if the
550                given fields are known and if the right values are specified
551                It will only update the ticket field value:
552                        - If the field is known
553                        - If the value supplied is valid for the ticket field.
554                          If not then there are two options:
555                           1) Skip the value (use_default=None)
556                           2) Set default value for field (use_default=1)
557                """
558
559                # Build a system dictionary from the ticket fields
560                # with field as index and option as value
561                #
562                sys_dict = dict()
563                for field in ticket.fields:
564                        try:
565                                sys_dict[field['name']] = field['options']
566
567                        except KeyError:
568                                sys_dict[field['name']] = None
569                                pass
570
571                # Check user supplied fields an compare them with the
572                # system one's
573                #
574                for field,value in user_dict.items():
575                        if self.DEBUG >= 10:
576                                print  'user_field\t %s = %s' %(field,value)
577
578                        if sys_dict.has_key(field):
579
580                                # Check if value is an allowed system option, if TypeError then
581                                # every value is allowed
582                                #
583                                try:
584                                        if value in sys_dict[field]:
585                                                ticket[field] = value
586                                        else:
587                                                # Must we set a default if value is not allowed
588                                                #
589                                                if use_default:
590                                                        value = self.get_config('ticket', 'default_%s' %(field) )
591                                                        ticket[field] = value
592
593                                except TypeError:
594                                        ticket[field] = value
595
596                                if self.DEBUG >= 10:
597                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
598                                       
599        def ticket_update(self, m, id, spam):
600                """
601                If the current email is a reply to an existing ticket, this function
602                will append the contents of this email to that ticket, instead of
603                creating a new one.
604                """
605                if self.DEBUG:
606                        print "TD: ticket_update: %s" %id
607
608                # Must we update ticket fields
609                #
610                update_fields = dict()
611                try:
612                        id, keywords = string.split(id, '?')
613
614                        # Skip the last ':' character
615                        #
616                        keywords = keywords[:-1]
617                        update_fields = self.str_to_dict(keywords)
618
619                        # Strip '#'
620                        #
621                        self.id = int(id[1:])
622
623                except ValueError:
624                        # Strip '#' and ':'
625                        #
626                        self.id = int(id[1:-1])
627
628
629                # When is the change committed
630                #
631                #
632                if self.VERSION == 0.11:
633                        utc = UTC()
634                        when = datetime.now(utc)
635                else:
636                        when = int(time.time())
637
638                try:
639                        tkt = Ticket(self.env, self.id, self.db)
640                except util.TracError, detail:
641                        # Not a valid ticket
642                        self.id = None
643                        return False
644
645                # reopen the ticket if it is was closed
646                # We must use the ticket workflow framework
647                #
648                if tkt['status'] in ['closed']:
649
650                        #print controller.actions['reopen']
651                        #
652                        # As reference 
653                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
654                        #
655                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
656                        #print 'controller : ', a
657                        #
658                        #b = controller.get_all_status()
659                        #print 'get all status: ', b
660                        #
661                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
662                        #print 'get_ticket_changes :', b
663
664                        if self.WORKFLOW and (self.VERSION in ['0.11']) :
665                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
666                                from trac.test import Mock, MockPerm
667
668                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
669
670                                controller = ConfigurableTicketWorkflow(self.env)
671                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
672
673                                if self.DEBUG:
674                                        print 'TD: Workflow ticket update fields: ', fields
675
676                                for key in fields.keys():
677                                        tkt[key] = fields[key]
678
679                        else:
680                                tkt['status'] = 'reopened'
681                                tkt['resolution'] = ''
682
683                # Must we update some ticket fields properties
684                #
685                if update_fields:
686                        self.update_ticket_fields(tkt, update_fields)
687
688                message_parts = self.get_message_parts(m)
689                message_parts = self.unique_attachment_names(message_parts)
690
691                if self.EMAIL_HEADER:
692                        message_parts.insert(0, self.email_header_txt(m))
693
694                body_text = self.body_text(message_parts)
695
696                if body_text.strip() or update_fields:
697                        if self.DRY_RUN:
698                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
699                        else:
700                                tkt.save_changes(self.author, body_text, when)
701
702                if self.VERSION  == 0.9:
703                        str = self.attachments(message_parts, True)
704                else:
705                        str = 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                str =  self.attachments(message_parts)
841                if str:
842                        changed = True
843                        comment = '%s\n%s\n' %(comment, str)
844
845                if changed:
846                        if self.DRY_RUN:
847                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
848                        else:
849                                tkt.save_changes(self.author, comment)
850                                #print tkt.get_changelog(self.db, when)
851
852                if self.notification and not spam:
853                        self.notify(tkt, True)
854
855
856        def blog(self, id):
857                """
858                The blog create/update function
859                """
860                # import the modules
861                #
862                from tracfullblog.core import FullBlogCore
863                from tracfullblog.model import BlogPost, BlogCommen
864
865                # instantiate blog core
866                blog = FullBlogCore(self.env)
867                req = ''
868               
869                if id:
870
871                        # update blog
872                        #
873                        comment = BlogComment(self.env, id)
874                        comment.author = self.author
875                        comment.comment = self.get_body_text(m)
876                        blog.create_comment(req, comment)
877
878                else:
879                        # create blog
880                        #
881                        import time
882                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
883
884                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
885                       
886                        post.author = self.author
887                        post.title = self.email_to_unicode(m['Subject'])
888                        post.body = self.get_body_text(m)
889                       
890                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
891
892
893        def parse(self, fp):
894                global m
895
896                m = email.message_from_file(fp)
897               
898                if not m:
899                        if self.DEBUG:
900                                print "TD: This is not a valid email message format"
901                        return
902                       
903                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
904                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
905
906                if self.DEBUG > 1:        # save the entire e-mail message text
907                        message_parts = self.get_message_parts(m)
908                        message_parts = self.unique_attachment_names(message_parts)
909                        self.save_email_for_debug(m, True)
910                        body_text = self.body_text(message_parts)
911                        self.debug_body(body_text, True)
912                        self.debug_attachments(message_parts)
913
914                self.db = self.env.get_db_cnx()
915                self.get_sender_info(m)
916
917                if not self.email_header_acl('white_list', self.email_addr, True):
918                        if self.DEBUG > 1 :
919                                print 'Message rejected : %s not in white list' %(self.email_addr)
920                        return False
921
922                if self.email_header_acl('black_list', self.email_addr, False):
923                        if self.DEBUG > 1 :
924                                print 'Message rejected : %s in black list' %(self.email_addr)
925                        return False
926
927                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
928                        if self.DEBUG > 1 :
929                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
930                        return False
931
932                # If drop the message
933                #
934                if self.spam(m) == 'drop':
935                        return False
936
937                elif self.spam(m) == 'spam':
938                        spam_msg = True
939                else:
940                        spam_msg = False
941
942                if self.get_config('notification', 'smtp_enabled') in ['true']:
943                        self.notification = 1
944                else:
945                        self.notification = 0
946
947                # Check if  FullBlogPlugin is installed
948                #
949                blog_enabled = None
950                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
951                        blog_enabled = True
952                       
953                if not m['Subject']:
954                        return False
955                else:
956                        subject  = self.email_to_unicode(m['Subject'])         
957
958                #
959                # [hic] #1529: Re: LRZ
960                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
961                #
962                TICKET_RE = re.compile(r"""
963                        (?P<blog>blog:(?P<blog_id>\w*))
964                        |(?P<new_fields>[#][?].*)
965                        |(?P<reply>[#][\d]+:)
966                        |(?P<reply_fields>[#][\d]+\?.*?:)
967                        """, re.VERBOSE)
968
969                # Find out if this is a ticket or a blog
970                #
971                result =  TICKET_RE.search(subject)
972
973                if result:
974                        if result.group('blog') and blog_enabled:
975                                self.blog(result.group('blog_id'))
976
977                        # update ticket + fields
978                        #
979                        if result.group('reply_fields') and self.TICKET_UPDATE:
980                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
981
982                        # Update ticket
983                        #
984                        elif result.group('reply') and self.TICKET_UPDATE:
985                                self.ticket_update(m, result.group('reply'), spam_msg)
986
987                        # New ticket + fields
988                        #
989                        elif result.group('new_fields'):
990                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
991
992                # Create ticket
993                #
994                else:
995                        self.new_ticket(m, subject, spam_msg)
996
997        def strip_signature(self, text):
998                """
999                Strip signature from message, inspired by Mailman software
1000                """
1001                body = []
1002                for line in text.splitlines():
1003                        if line == '-- ':
1004                                break
1005                        body.append(line)
1006
1007                return ('\n'.join(body))
1008
1009        def reflow(self, text, delsp = 0):
1010                """
1011                Reflow the message based on the format="flowed" specification (RFC 3676)
1012                """
1013                flowedlines = []
1014                quotelevel = 0
1015                prevflowed = 0
1016
1017                for line in text.splitlines():
1018                        from re import match
1019                       
1020                        # Figure out the quote level and the content of the current line
1021                        m = match('(>*)( ?)(.*)', line)
1022                        linequotelevel = len(m.group(1))
1023                        line = m.group(3)
1024
1025                        # Determine whether this line is flowed
1026                        if line and line != '-- ' and line[-1] == ' ':
1027                                flowed = 1
1028                        else:
1029                                flowed = 0
1030
1031                        if flowed and delsp and line and line[-1] == ' ':
1032                                line = line[:-1]
1033
1034                        # If the previous line is flowed, append this line to it
1035                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1036                                flowedlines[-1] += line
1037                        # Otherwise, start a new line
1038                        else:
1039                                flowedlines.append('>' * linequotelevel + line)
1040
1041                        prevflowed = flowed
1042                       
1043
1044                return '\n'.join(flowedlines)
1045
1046        def strip_quotes(self, text):
1047                """
1048                Strip quotes from message by Nicolas Mendoza
1049                """
1050                body = []
1051                for line in text.splitlines():
1052                        if line.startswith(self.EMAIL_QUOTE):
1053                                continue
1054                        body.append(line)
1055
1056                return ('\n'.join(body))
1057
1058        def wrap_text(self, text, replace_whitespace = False):
1059                """
1060                Will break a lines longer then given length into several small
1061                lines of size given length
1062                """
1063                import textwrap
1064
1065                LINESEPARATOR = '\n'
1066                reformat = ''
1067
1068                for s in text.split(LINESEPARATOR):
1069                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1070                        if tmp:
1071                                reformat = '%s\n%s' %(reformat,tmp)
1072                        else:
1073                                reformat = '%s\n' %reformat
1074
1075                return reformat
1076
1077                # Python2.4 and higher
1078                #
1079                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1080                #
1081
1082
1083        def get_message_parts(self, msg):
1084                """
1085                parses the email message and returns a list of body parts and attachments
1086                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1087                """
1088                message_parts = []
1089               
1090                # This is used to figure out when we are inside an AppleDouble container
1091                # AppleDouble containers consists of two parts: Mac-specific file data, and platform-independent data
1092                # We strip away Mac-specific stuff
1093                appledouble_parts = []
1094
1095                ALTERNATIVE_MULTIPART = False
1096
1097                for part in msg.walk():
1098                        if self.DEBUG:
1099                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1100                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1101
1102
1103                        # Check whether we just finished processing an AppleDouble container
1104                        if part not in appledouble_parts:
1105                                appledouble_parts = []
1106
1107                        ## Check content type
1108                        #
1109                        if part.get_content_type() == 'application/mac-binhex40':
1110                                #
1111                                # Special handling for BinHex attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1112                                #
1113                                if self.BINHEX == 'warn':
1114                                        message_parts.append("'''A BinHex attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1115                                        continue
1116                                elif self.BINHEX == 'drop':
1117                                        continue
1118
1119                        elif part.get_content_type() == 'application/applefile':
1120                                #
1121                                # 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
1122                                #
1123                               
1124                                if part in appledouble_parts:
1125                                        if self.APPLEDOUBLE == 'warn':
1126                                                message_parts.append("'''The resource fork of an attachment named '%s' was removed.'''" % part.get_filename())
1127                                                continue
1128                                        elif self.APPLEDOUBLE == 'strip':
1129                                                continue
1130                                else:
1131                                        if self.APPLESINGLE == 'warn':
1132                                                message_parts.append("'''An AppleSingle attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1133                                                continue
1134                                        elif self.APPLESINGLE == 'drop':
1135                                                continue
1136
1137                        elif part.get_content_type() == 'multipart/appledouble':
1138                                #
1139                                # If we entering an AppleDouble container, set up appledouble_parts so that we know what to do with its subparts
1140                                #
1141                                appledouble_parts = part.get_payload()
1142                                continue
1143
1144                        elif part.get_content_type() == 'multipart/alternative':
1145                                ALTERNATIVE_MULTIPART = True
1146                                continue
1147
1148                        # Skip multipart containers
1149                        #
1150                        if part.get_content_maintype() == 'multipart':
1151                                if self.DEBUG:
1152                                        print "TD: Skipping multipart container"
1153                                continue
1154                       
1155                        # 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"
1156                        inline = self.inline_part(part)
1157
1158                        # Drop HTML message
1159                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1160                                if part.get_content_type() == 'text/html':
1161                                        if self.DEBUG:
1162                                                print "TD: Skipping alternative HTML message"
1163
1164                                        ALTERNATIVE_MULTIPART = False
1165                                        continue
1166
1167                        # Inline text parts are where the body is
1168                        if part.get_content_type() == 'text/plain' and inline:
1169                                if self.DEBUG:
1170                                        print 'TD:               Inline body part'
1171
1172                                # Try to decode, if fails then do not decode
1173                                #
1174                                body_text = part.get_payload(decode=1)
1175                                if not body_text:                       
1176                                        body_text = part.get_payload(decode=0)
1177
1178                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1179                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1180
1181                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1182                                        body_text = self.reflow(body_text, delsp == 'yes')
1183       
1184                                if self.STRIP_SIGNATURE:
1185                                        body_text = self.strip_signature(body_text)
1186
1187                                if self.STRIP_QUOTES:
1188                                        body_text = self.strip_quotes(body_text)
1189
1190                                if self.USE_TEXTWRAP:
1191                                        body_text = self.wrap_text(body_text)
1192
1193                                # Get contents charset (iso-8859-15 if not defined in mail headers)
1194                                #
1195                                charset = part.get_content_charset()
1196                                if not charset:
1197                                        charset = 'iso-8859-15'
1198
1199                                try:
1200                                        ubody_text = unicode(body_text, charset)
1201
1202                                except UnicodeError, detail:
1203                                        ubody_text = unicode(body_text, 'iso-8859-15')
1204
1205                                except LookupError, detail:
1206                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1207
1208                                if self.VERBATIM_FORMAT:
1209                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1210                                else:
1211                                        message_parts.append('%s' %ubody_text)
1212                        else:
1213                                if self.DEBUG:
1214                                        print 'TD:               Filename: %s' % part.get_filename()
1215
1216                                message_parts.append((part.get_filename(), part))
1217
1218                return message_parts
1219               
1220        def unique_attachment_names(self, message_parts):
1221                renamed_parts = []
1222                attachment_names = set()
1223                for part in message_parts:
1224                       
1225                        # If not an attachment, leave it alone
1226                        if not isinstance(part, tuple):
1227                                renamed_parts.append(part)
1228                                continue
1229                               
1230                        (filename, part) = part
1231                        # Decode the filename
1232                        if filename:
1233                                filename = self.email_to_unicode(filename)                     
1234                        # If no name, use a default one
1235                        else:
1236                                filename = 'untitled-part'
1237
1238                                # Guess the extension from the content type, use non strict mode
1239                                # some additional non-standard but commonly used MIME types
1240                                # are also recognized
1241                                #
1242                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1243                                if not ext:
1244                                        ext = '.bin'
1245
1246                                filename = '%s%s' % (filename, ext)
1247
1248                        # Discard relative paths in attachment names
1249                        filename = filename.replace('\\', '/').replace(':', '/')
1250                        filename = os.path.basename(filename)
1251
1252                        # We try to normalize the filename to utf-8 NFC if we can.
1253                        # Files uploaded from OS X might be in NFD.
1254                        # Check python version and then try it
1255                        #
1256                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1257                                try:
1258                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1259                                except TypeError:
1260                                        pass
1261                                       
1262                        if self.QUOTE_ATTACHMENT_FILENAMES:
1263                                try:
1264                                        filename = urllib.quote(filename)
1265                                except KeyError, detail:
1266                                        filename = urllib.quote(filename.encode('utf-8'))
1267
1268                        # Make the filename unique for this ticket
1269                        num = 0
1270                        unique_filename = filename
1271                        filename, ext = os.path.splitext(filename)
1272
1273                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
1274                                num += 1
1275                                unique_filename = "%s-%s%s" % (filename, num, ext)
1276                               
1277                        if self.DEBUG:
1278                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1279
1280                        attachment_names.add(unique_filename)
1281
1282                        renamed_parts.append((filename, unique_filename, part))
1283               
1284                return renamed_parts
1285                       
1286        def inline_part(self, part):
1287                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1288               
1289                       
1290        def attachment_exists(self, filename):
1291
1292                if self.DEBUG:
1293                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
1294
1295                # We have no valid ticket id
1296                #
1297                if not self.id:
1298                        return False
1299
1300                try:
1301                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1302                        return True
1303                except attachment.ResourceNotFound:
1304                        return False
1305                       
1306        def body_text(self, message_parts):
1307                body_text = []
1308               
1309                for part in message_parts:
1310                        # Plain text part, append it
1311                        if not isinstance(part, tuple):
1312                                body_text.extend(part.strip().splitlines())
1313                                body_text.append("")
1314                                continue
1315                               
1316                        (original, filename, part) = part
1317                        inline = self.inline_part(part)
1318                       
1319                        if part.get_content_maintype() == 'image' and inline:
1320                                body_text.append('[[Image(%s)]]' % filename)
1321                                body_text.append("")
1322                        else:
1323                                body_text.append('[attachment:"%s"]' % filename)
1324                                body_text.append("")
1325                               
1326                body_text = '\r\n'.join(body_text)
1327                return body_text
1328
1329        def notify(self, tkt, new=True, modtime=0):
1330                """
1331                A wrapper for the TRAC notify function. So we can use templates
1332                """
1333                if self.DRY_RUN:
1334                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1335                                return
1336                try:
1337                        # create false {abs_}href properties, to trick Notify()
1338                        #
1339                        if not self.VERSION == 0.11:
1340                                self.env.abs_href = Href(self.get_config('project', 'url'))
1341                                self.env.href = Href(self.get_config('project', 'url'))
1342
1343                        tn = TicketNotifyEmail(self.env)
1344
1345                        if self.notify_template:
1346
1347                                if self.VERSION == 0.11:
1348
1349                                        from trac.web.chrome import Chrome
1350
1351                                        if self.notify_template_update and not new:
1352                                                tn.template_name = self.notify_template_update
1353                                        else:
1354                                                tn.template_name = self.notify_template
1355
1356                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1357                                               
1358                                else:
1359
1360                                        tn.template_name = self.notify_template;
1361
1362                        tn.notify(tkt, new, modtime)
1363
1364                except Exception, e:
1365                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1366
1367        def html_mailto_link(self, subject, body):
1368                """
1369                This function returns a HTML mailto tag with the ticket id and author email address
1370                """
1371                if not self.author:
1372                        author = self.email_addr
1373                else:   
1374                        author = self.author
1375
1376                # use urllib to escape the chars
1377                #
1378                str = 'mailto:%s?Subject=%s&Cc=%s' %(
1379                       urllib.quote(self.email_addr),
1380                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1381                           urllib.quote(self.MAILTO_CC)
1382                           )
1383
1384                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)
1385                return str
1386
1387        def attachments(self, message_parts, update=False):
1388                '''
1389                save any attachments as files in the ticket's directory
1390                '''
1391                if self.DRY_RUN:
1392                        print "DRY_RUN: no attachments saved"
1393                        return ''
1394
1395                count = 0
1396
1397                # Get Maxium attachment size
1398                #
1399                max_size = int(self.get_config('attachment', 'max_size'))
1400                status   = ''
1401               
1402                for part in message_parts:
1403                        # Skip body parts
1404                        if not isinstance(part, tuple):
1405                                continue
1406                               
1407                        (original, filename, part) = part
1408                        #
1409                        # Must be tuneables HvB
1410                        #
1411                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1412                        text = part.get_payload(decode=1)
1413                        if not text:
1414                                text = '(None)'
1415                        fd.write(text)
1416                        fd.close()
1417
1418                        # get the file_size
1419                        #
1420                        stats = os.lstat(path)
1421                        file_size = stats[stat.ST_SIZE]
1422
1423                        # Check if the attachment size is allowed
1424                        #
1425                        if (max_size != -1) and (file_size > max_size):
1426                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1427                                        %(status, original, file_size, max_size)
1428
1429                                os.unlink(path)
1430                                continue
1431                        else:
1432                                count = count + 1
1433                                       
1434                        # Insert the attachment
1435                        #
1436                        fd = open(path, 'rb')
1437                        att = attachment.Attachment(self.env, 'ticket', self.id)
1438
1439                        # This will break the ticket_update system, the body_text is vaporized
1440                        # ;-(
1441                        #
1442                        if not update:
1443                                att.author = self.author
1444                                att.description = self.email_to_unicode('Added by email2trac')
1445
1446                        att.insert(filename, fd, file_size)
1447                        #except  util.TracError, detail:
1448                        #       print detail
1449
1450                        # Remove the created temporary filename
1451                        #
1452                        fd.close()
1453                        os.unlink(path)
1454
1455                # Return how many attachments
1456                #
1457                status = 'This message has %d attachment(s)\n%s' %(count, status)
1458                return status
1459
1460
1461def mkdir_p(dir, mode):
1462        '''do a mkdir -p'''
1463
1464        arr = string.split(dir, '/')
1465        path = ''
1466        for part in arr:
1467                path = '%s/%s' % (path, part)
1468                try:
1469                        stats = os.stat(path)
1470                except OSError:
1471                        os.mkdir(path, mode)
1472
1473def ReadConfig(file, name):
1474        """
1475        Parse the config file
1476        """
1477        if not os.path.isfile(file):
1478                print 'File %s does not exist' %file
1479                sys.exit(1)
1480
1481        config = trac_config.Configuration(file)
1482
1483        # Use given project name else use defaults
1484        #
1485        if name:
1486                sections = config.sections()
1487                if not name in sections:
1488                        print "Not a valid project name: %s" %name
1489                        print "Valid names: %s" %sections
1490                        sys.exit(1)
1491
1492                project =  dict()
1493                for option, value in  config.options(name):
1494                        project[option] = value
1495
1496        else:
1497                # use some trac internals to get the defaults
1498                #
1499                project = config.parser.defaults()
1500
1501        return project
1502
1503
1504if __name__ == '__main__':
1505        # Default config file
1506        #
1507        configfile = '@email2trac_conf@'
1508        project = ''
1509        component = ''
1510        ticket_prefix = 'default'
1511        dry_run = None
1512
1513        ENABLE_SYSLOG = 0
1514
1515
1516        SHORT_OPT = 'chf:np:t:'
1517        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
1518
1519        try:
1520                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1521        except getopt.error,detail:
1522                print __doc__
1523                print detail
1524                sys.exit(1)
1525       
1526        project_name = None
1527        for opt,value in opts:
1528                if opt in [ '-h', '--help']:
1529                        print __doc__
1530                        sys.exit(0)
1531                elif opt in ['-c', '--component']:
1532                        component = value
1533                elif opt in ['-f', '--file']:
1534                        configfile = value
1535                elif opt in ['-n', '--dry-run']:
1536                        dry_run = True
1537                elif opt in ['-p', '--project']:
1538                        project_name = value
1539                elif opt in ['-t', '--ticket_prefix']:
1540                        ticket_prefix = value
1541       
1542        settings = ReadConfig(configfile, project_name)
1543        if not settings.has_key('project'):
1544                print __doc__
1545                print 'No Trac project is defined in the email2trac config file.'
1546                sys.exit(1)
1547       
1548        if component:
1549                settings['component'] = component
1550
1551        # The default prefix for ticket values in email2trac.conf
1552        #
1553        settings['ticket_prefix'] = ticket_prefix
1554        settings['dry_run'] = dry_run
1555       
1556        if settings.has_key('trac_version'):
1557                version = settings['trac_version']
1558        else:
1559                version = trac_default_version
1560
1561
1562        #debug HvB
1563        #print settings
1564
1565        try:
1566                if version == '0.9':
1567                        from trac import attachment
1568                        from trac.env import Environment
1569                        from trac.ticket import Ticket
1570                        from trac.web.href import Href
1571                        from trac import util
1572                        from trac.Notify import TicketNotifyEmail
1573                elif version == '0.10':
1574                        from trac import attachment
1575                        from trac.env import Environment
1576                        from trac.ticket import Ticket
1577                        from trac.web.href import Href
1578                        from trac import util
1579                        #
1580                        # return  util.text.to_unicode(str)
1581                        #
1582                        # see http://projects.edgewall.com/trac/changeset/2799
1583                        from trac.ticket.notification import TicketNotifyEmail
1584                        from trac import config as trac_config
1585                elif version == '0.11':
1586                        from trac import attachment
1587                        from trac.env import Environment
1588                        from trac.ticket import Ticket
1589                        from trac.web.href import Href
1590                        from trac import config as trac_config
1591                        from trac import util
1592
1593
1594                        #
1595                        # return  util.text.to_unicode(str)
1596                        #
1597                        # see http://projects.edgewall.com/trac/changeset/2799
1598                        from trac.ticket.notification import TicketNotifyEmail
1599                else:
1600                        print 'TRAC version %s is not supported' %version
1601                        sys.exit(1)
1602                       
1603                if settings.has_key('enable_syslog'):
1604                        if SYSLOG_AVAILABLE:
1605                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1606
1607                env = Environment(settings['project'], create=0)
1608                tktparser = TicketEmailParser(env, settings, float(version))
1609                tktparser.parse(sys.stdin)
1610
1611        # Catch all errors ans log to SYSLOG if we have enabled this
1612        # else stdout
1613        #
1614        except Exception, error:
1615                if ENABLE_SYSLOG:
1616                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1617
1618                        etype, evalue, etb = sys.exc_info()
1619                        for e in traceback.format_exception(etype, evalue, etb):
1620                                syslog.syslog(e)
1621
1622                        syslog.closelog()
1623                else:
1624                        traceback.print_exc()
1625
1626                if m:
1627                        tktparser.save_email_for_debug(m, True)
1628
1629                sys.exit(1)
1630# EOB
Note: See TracBrowser for help on using the repository browser.