source: trunk/email2trac.py.in @ 304

Last change on this file since 304 was 304, checked in by bas, 14 years ago

Prevent setting the reporter to the ticket email address

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