source: trunk/email2trac.py.in @ 281

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

email2trac.py.in:

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