source: trunk/email2trac.py.in @ 299

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

prevent a mail loop for new tickets when reply_all is set and cc-address is also ticket mail address, closes #172

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 40.1 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 299 2010-01-11 12:31:12Z 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                #
445                self.author = self.email_addr
446
447                if self.IGNORE_TRAC_USER_SETTINGS:
448                        return
449
450                # Is this a registered user, use email address as search key:
451                # result:
452                #   u : login name
453                #   n : Name that the user has set in the settings tab
454                #   e : email address that the user has set in the settings tab
455                #
456                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
457                        if e and (e.lower() == self.email_addr.lower()) ]
458
459                if len(users) == 1:
460                        self.email_from = users[0][0]
461                        self.author = users[0][0]
462
463        def set_reply_fields(self, ticket, message):
464                """
465                Set all the right fields for a new ticket
466                """
467                if self.DEBUG:
468                        print 'TD: set_reply_fields'
469
470                ## Only use name or email adress
471                #ticket['reporter'] = self.email_from
472                ticket['reporter'] = self.author
473
474
475                # Put all CC-addresses in ticket CC field
476                #
477                if self.REPLY_ALL:
478
479                        email_cc = ''
480
481                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
482
483                        if not cc_addrs:
484                                return
485
486                        ## Build a list of forbidden CC addresses
487                        #
488                        to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
489                        to_list = list()
490                        for n,e in to_addrs:
491                                to_list.append(e)
492                               
493                        # Remove reporter email address if notification is
494                        # on
495                        #
496                        if self.notification:
497                                try:
498                                        cc_addrs.remove((self.author, self.email_addr))
499                                except ValueError, detail:
500                                        pass
501
502                        for name,addr in cc_addrs:
503               
504                                ## Prevent mail loop
505                                #
506                                if addr in to_list:
507                                        if self.DEBUG:
508                                                print "Skipping %s mail address for CC-field" %(addr)
509                                        continue
510
511                                if email_cc:
512                                        email_cc = '%s, %s' %(email_cc, addr)
513                                else:
514                                        email_cc = addr
515
516                        if email_cc:
517                                if self.DEBUG:
518                                        print 'TD: set_reply_fields: %s' %email_cc
519
520                                ticket['cc'] = self.email_to_unicode(email_cc)
521
522        def save_email_for_debug(self, message, tempfile=False):
523                if tempfile:
524                        import tempfile
525                        msg_file = tempfile.mktemp('.email2trac')
526                else:
527                        #msg_file = '/var/tmp/msg.txt'
528                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
529
530                print 'TD: saving email to %s' % msg_file
531                fx = open(msg_file, 'wb')
532                fx.write('%s' % message)
533                fx.close()
534                try:
535                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
536                except OSError:
537                        pass
538
539        def str_to_dict(self, s):
540                """
541                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
542                """
543
544                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
545
546                result = dict()
547                for field in fields:
548                        try:
549                                index, value = string.split(field, '=')
550
551                                # We can not change the description of a ticket via the subject
552                                # line. The description is the body of the email
553                                #
554                                if index.lower() in ['description']:
555                                        continue
556
557                                if value:
558                                        result[index.lower()] = value
559
560                        except ValueError:
561                                pass
562                return result
563
564        def update_ticket_fields(self, ticket, user_dict, use_default=None):
565                """
566                This will update the ticket fields. It will check if the
567                given fields are known and if the right values are specified
568                It will only update the ticket field value:
569                        - If the field is known
570                        - If the value supplied is valid for the ticket field.
571                          If not then there are two options:
572                           1) Skip the value (use_default=None)
573                           2) Set default value for field (use_default=1)
574                """
575
576                # Build a system dictionary from the ticket fields
577                # with field as index and option as value
578                #
579                sys_dict = dict()
580                for field in ticket.fields:
581                        try:
582                                sys_dict[field['name']] = field['options']
583
584                        except KeyError:
585                                sys_dict[field['name']] = None
586                                pass
587
588                # Check user supplied fields an compare them with the
589                # system one's
590                #
591                for field,value in user_dict.items():
592                        if self.DEBUG >= 10:
593                                print  'user_field\t %s = %s' %(field,value)
594
595                        if sys_dict.has_key(field):
596
597                                # Check if value is an allowed system option, if TypeError then
598                                # every value is allowed
599                                #
600                                try:
601                                        if value in sys_dict[field]:
602                                                ticket[field] = value
603                                        else:
604                                                # Must we set a default if value is not allowed
605                                                #
606                                                if use_default:
607                                                        value = self.get_config('ticket', 'default_%s' %(field) )
608                                                        ticket[field] = value
609
610                                except TypeError:
611                                        ticket[field] = value
612
613                                if self.DEBUG >= 10:
614                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
615                                       
616        def ticket_update(self, m, id, spam):
617                """
618                If the current email is a reply to an existing ticket, this function
619                will append the contents of this email to that ticket, instead of
620                creating a new one.
621                """
622                if self.DEBUG:
623                        print "TD: ticket_update: %s" %id
624
625                # Must we update ticket fields
626                #
627                update_fields = dict()
628                try:
629                        id, keywords = string.split(id, '?')
630
631                        # Skip the last ':' character
632                        #
633                        keywords = keywords[:-1]
634                        update_fields = self.str_to_dict(keywords)
635
636                        # Strip '#'
637                        #
638                        self.id = int(id[1:])
639
640                except ValueError:
641                        # Strip '#' and ':'
642                        #
643                        self.id = int(id[1:-1])
644
645
646                # When is the change committed
647                #
648                if self.VERSION == 0.11:
649                        utc = UTC()
650                        when = datetime.now(utc)
651                else:
652                        when = int(time.time())
653
654                try:
655                        tkt = Ticket(self.env, self.id, self.db)
656                except util.TracError, detail:
657                        # Not a valid ticket
658                        self.id = None
659                        return False
660
661                # How many changes has this ticket
662                cnum = len(tkt.get_changelog())
663
664
665                # reopen the ticket if it is was closed
666                # We must use the ticket workflow framework
667                #
668                if tkt['status'] in ['closed']:
669
670                        #print controller.actions['reopen']
671                        #
672                        # As reference 
673                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
674                        #
675                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
676                        #print 'controller : ', a
677                        #
678                        #b = controller.get_all_status()
679                        #print 'get all status: ', b
680                        #
681                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
682                        #print 'get_ticket_changes :', b
683
684                        if self.WORKFLOW and (self.VERSION in [0.11]) :
685                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
686                                from trac.test import Mock, MockPerm
687
688                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
689
690                                controller = ConfigurableTicketWorkflow(self.env)
691                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
692
693                                if self.DEBUG:
694                                        print 'TD: Workflow ticket update fields: ', fields
695
696                                for key in fields.keys():
697                                        tkt[key] = fields[key]
698
699                        else:
700                                tkt['status'] = 'reopened'
701                                tkt['resolution'] = ''
702
703                # Must we update some ticket fields properties
704                #
705                if update_fields:
706                        self.update_ticket_fields(tkt, update_fields)
707
708                message_parts = self.get_message_parts(m)
709                message_parts = self.unique_attachment_names(message_parts)
710
711                if self.EMAIL_HEADER:
712                        message_parts.insert(0, self.email_header_txt(m))
713
714                body_text = self.body_text(message_parts)
715
716                if body_text.strip() or update_fields:
717                        if self.DRY_RUN:
718                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
719                        else:
720                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
721                       
722
723                if self.VERSION  == 0.9:
724                        s = self.attachments(message_parts, True)
725                else:
726                        s = self.attachments(message_parts)
727
728                if self.notification and not spam:
729                        self.notify(tkt, False, when)
730
731                return True
732
733        def set_ticket_fields(self, ticket):
734                """
735                set the ticket fields to value specified
736                        - /etc/email2trac.conf with <prefix>_<field>
737                        - trac default values, trac.ini
738                """
739                user_dict = dict()
740
741                for field in ticket.fields:
742
743                        name = field['name']
744
745                        # skip some fields like resolution
746                        #
747                        if name in [ 'resolution' ]:
748                                continue
749
750                        # default trac value
751                        #
752                        if not field.get('custom'):
753                                value = self.get_config('ticket', 'default_%s' %(name) )
754                        else:
755                                value = field.get('value')
756                                options = field.get('options')
757                                if value and options and value not in options:
758                                        value = options[int(value)]
759
760                        if self.DEBUG > 10:
761                                print 'trac.ini name %s = %s' %(name, value)
762
763                        prefix = self.parameters['ticket_prefix']
764                        try:
765                                value = self.parameters['%s_%s' %(prefix, name)]
766                                if self.DEBUG > 10:
767                                        print 'email2trac.conf %s = %s ' %(name, value)
768
769                        except KeyError, detail:
770                                pass
771               
772                        if self.DEBUG:
773                                print 'user_dict[%s] = %s' %(name, value)
774
775                        user_dict[name] = value
776
777                self.update_ticket_fields(ticket, user_dict, use_default=1)
778
779                # Set status ticket
780                #`
781                ticket['status'] = 'new'
782
783
784
785        def new_ticket(self, msg, subject, spam, set_fields = None):
786                """
787                Create a new ticket
788                """
789                if self.DEBUG:
790                        print "TD: new_ticket"
791
792                tkt = Ticket(self.env)
793                self.set_ticket_fields(tkt)
794
795                # Old style setting for component, will be removed
796                #
797                if spam:
798                        tkt['component'] = 'Spam'
799
800                elif self.parameters.has_key('component'):
801                        tkt['component'] = self.parameters['component']
802
803                if not msg['Subject']:
804                        tkt['summary'] = u'(No subject)'
805                else:
806                        tkt['summary'] = subject
807
808                self.set_reply_fields(tkt, msg)
809
810                if set_fields:
811                        rest, keywords = string.split(set_fields, '?')
812
813                        if keywords:
814                                update_fields = self.str_to_dict(keywords)
815                                self.update_ticket_fields(tkt, update_fields)
816
817                # produce e-mail like header
818                #
819                head = ''
820                if self.EMAIL_HEADER > 0:
821                        head = self.email_header_txt(msg)
822
823                                       
824                message_parts = self.get_message_parts(msg)
825                if self.DEBUG:
826                        print 'TD: self.get_message_parts ',
827                        print message_parts
828
829                message_parts = self.unique_attachment_names(message_parts)
830                if self.DEBUG:
831                        print 'TD: self.unique_attachment_names',
832                        print message_parts
833               
834                if self.EMAIL_HEADER > 0:
835                        message_parts.insert(0, self.email_header_txt(msg))
836                       
837                body_text = self.body_text(message_parts)
838
839                tkt['description'] = body_text
840
841                #when = int(time.time())
842                #
843                utc = UTC()
844                when = datetime.now(utc)
845
846                if not self.DRY_RUN:
847                        self.id = tkt.insert()
848       
849                changed = False
850                comment = ''
851
852                # some routines in trac are dependend on ticket id     
853                # like alternate notify template
854                #
855                if self.notify_template:
856                        tkt['id'] = self.id
857                        changed = True
858
859                ## Rewrite the description if we have mailto enabled
860                #
861                if self.MAILTO:
862                        changed = True
863                        comment = u'\nadded mailto line\n'
864                        mailto = self.html_mailto_link( m['Subject'], body_text)
865
866                        tkt['description'] = u'%s\r\n%s%s\r\n' \
867                                %(head, mailto, body_text)
868       
869                ## Save the attachments to the ticket   
870                #
871                has_attachments =  self.attachments(message_parts)
872
873                #  Disabled
874                #if has_attachments:
875                #       changed = True
876                #       comment = '%s\n%s\n' %(comment, has_attachments)
877
878                if changed:
879                        if self.DRY_RUN:
880                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
881                        else:
882                                tkt.save_changes(self.author, comment)
883                                #print tkt.get_changelog(self.db, when)
884
885                if self.notification and not spam:
886                        self.notify(tkt, True)
887
888
889        def blog(self, id):
890                """
891                The blog create/update function
892                """
893                # import the modules
894                #
895                from tracfullblog.core import FullBlogCore
896                from tracfullblog.model import BlogPost, BlogCommen
897
898                # instantiate blog core
899                blog = FullBlogCore(self.env)
900                req = ''
901               
902                if id:
903
904                        # update blog
905                        #
906                        comment = BlogComment(self.env, id)
907                        comment.author = self.author
908                        comment.comment = self.get_body_text(m)
909                        blog.create_comment(req, comment)
910
911                else:
912                        # create blog
913                        #
914                        import time
915                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
916
917                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
918                       
919                        post.author = self.author
920                        post.title = self.email_to_unicode(m['Subject'])
921                        post.body = self.get_body_text(m)
922                       
923                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
924
925
926        def parse(self, fp):
927                global m
928
929                m = email.message_from_file(fp)
930               
931                if not m:
932                        if self.DEBUG:
933                                print "TD: This is not a valid email message format"
934                        return
935                       
936                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
937                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
938
939                if self.DEBUG > 1:        # save the entire e-mail message text
940                        message_parts = self.get_message_parts(m)
941                        message_parts = self.unique_attachment_names(message_parts)
942                        self.save_email_for_debug(m, True)
943                        body_text = self.body_text(message_parts)
944                        self.debug_body(body_text, True)
945                        self.debug_attachments(message_parts)
946
947                self.db = self.env.get_db_cnx()
948                self.get_sender_info(m)
949
950                if not self.email_header_acl('white_list', self.email_addr, True):
951                        if self.DEBUG > 1 :
952                                print 'Message rejected : %s not in white list' %(self.email_addr)
953                        return False
954
955                if self.email_header_acl('black_list', self.email_addr, False):
956                        if self.DEBUG > 1 :
957                                print 'Message rejected : %s in black list' %(self.email_addr)
958                        return False
959
960                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
961                        if self.DEBUG > 1 :
962                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
963                        return False
964
965                # If drop the message
966                #
967                if self.spam(m) == 'drop':
968                        return False
969
970                elif self.spam(m) == 'spam':
971                        spam_msg = True
972                else:
973                        spam_msg = False
974
975                if self.get_config('notification', 'smtp_enabled') in ['true']:
976                        self.notification = 1
977                else:
978                        self.notification = 0
979
980                # Check if  FullBlogPlugin is installed
981                #
982                blog_enabled = None
983                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
984                        blog_enabled = True
985                       
986                if not m['Subject']:
987                        return False
988                else:
989                        subject  = self.email_to_unicode(m['Subject'])         
990
991                #
992                # [hic] #1529: Re: LRZ
993                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
994                #
995                TICKET_RE = re.compile(r"""
996                        (?P<blog>blog:(?P<blog_id>\w*))
997                        |(?P<new_fields>[#][?].*)
998                        |(?P<reply>[#][\d]+:)
999                        |(?P<reply_fields>[#][\d]+\?.*?:)
1000                        """, re.VERBOSE)
1001
1002                # Find out if this is a ticket or a blog
1003                #
1004                result =  TICKET_RE.search(subject)
1005
1006                if result:
1007                        if result.group('blog') and blog_enabled:
1008                                self.blog(result.group('blog_id'))
1009
1010                        # update ticket + fields
1011                        #
1012                        if result.group('reply_fields') and self.TICKET_UPDATE:
1013                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
1014
1015                        # Update ticket
1016                        #
1017                        elif result.group('reply') and self.TICKET_UPDATE:
1018                                self.ticket_update(m, result.group('reply'), spam_msg)
1019
1020                        # New ticket + fields
1021                        #
1022                        elif result.group('new_fields'):
1023                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1024
1025                # Create ticket
1026                #
1027                else:
1028                        self.new_ticket(m, subject, spam_msg)
1029
1030        def strip_signature(self, text):
1031                """
1032                Strip signature from message, inspired by Mailman software
1033                """
1034                body = []
1035                for line in text.splitlines():
1036                        if line == '-- ':
1037                                break
1038                        body.append(line)
1039
1040                return ('\n'.join(body))
1041
1042        def reflow(self, text, delsp = 0):
1043                """
1044                Reflow the message based on the format="flowed" specification (RFC 3676)
1045                """
1046                flowedlines = []
1047                quotelevel = 0
1048                prevflowed = 0
1049
1050                for line in text.splitlines():
1051                        from re import match
1052                       
1053                        # Figure out the quote level and the content of the current line
1054                        m = match('(>*)( ?)(.*)', line)
1055                        linequotelevel = len(m.group(1))
1056                        line = m.group(3)
1057
1058                        # Determine whether this line is flowed
1059                        if line and line != '-- ' and line[-1] == ' ':
1060                                flowed = 1
1061                        else:
1062                                flowed = 0
1063
1064                        if flowed and delsp and line and line[-1] == ' ':
1065                                line = line[:-1]
1066
1067                        # If the previous line is flowed, append this line to it
1068                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1069                                flowedlines[-1] += line
1070                        # Otherwise, start a new line
1071                        else:
1072                                flowedlines.append('>' * linequotelevel + line)
1073
1074                        prevflowed = flowed
1075                       
1076
1077                return '\n'.join(flowedlines)
1078
1079        def strip_quotes(self, text):
1080                """
1081                Strip quotes from message by Nicolas Mendoza
1082                """
1083                body = []
1084                for line in text.splitlines():
1085                        if line.startswith(self.EMAIL_QUOTE):
1086                                continue
1087                        body.append(line)
1088
1089                return ('\n'.join(body))
1090
1091        def wrap_text(self, text, replace_whitespace = False):
1092                """
1093                Will break a lines longer then given length into several small
1094                lines of size given length
1095                """
1096                import textwrap
1097
1098                LINESEPARATOR = '\n'
1099                reformat = ''
1100
1101                for s in text.split(LINESEPARATOR):
1102                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1103                        if tmp:
1104                                reformat = '%s\n%s' %(reformat,tmp)
1105                        else:
1106                                reformat = '%s\n' %reformat
1107
1108                return reformat
1109
1110                # Python2.4 and higher
1111                #
1112                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1113                #
1114
1115
1116        def get_message_parts(self, msg):
1117                """
1118                parses the email message and returns a list of body parts and attachments
1119                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1120                """
1121                message_parts = []
1122       
1123                ALTERNATIVE_MULTIPART = False
1124
1125                for part in msg.walk():
1126                        if self.DEBUG:
1127                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1128                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1129
1130                        ## Check content type
1131                        #
1132                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1133
1134                                if self.DEBUG:
1135                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1136
1137                                continue
1138
1139                        ## Catch some mulitpart execptions
1140                        #
1141                        if part.get_content_type() == 'multipart/alternative':
1142                                ALTERNATIVE_MULTIPART = True
1143                                continue
1144
1145                        ## Skip multipart containers
1146                        #
1147                        if part.get_content_maintype() == 'multipart':
1148                                if self.DEBUG:
1149                                        print "TD: Skipping multipart container"
1150                                continue
1151                       
1152                        ## 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"
1153                        #
1154                        inline = self.inline_part(part)
1155
1156                        ## Drop HTML message
1157                        #
1158                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1159                                if part.get_content_type() == 'text/html':
1160                                        if self.DEBUG:
1161                                                print "TD: Skipping alternative HTML message"
1162
1163                                        ALTERNATIVE_MULTIPART = False
1164                                        continue
1165
1166                        ## Inline text parts are where the body is
1167                        #
1168                        if part.get_content_type() == 'text/plain' and inline:
1169                                if self.DEBUG:
1170                                        print 'TD:               Inline body part'
1171
1172                                # Try to decode, if fails then do not decode
1173                                #
1174                                body_text = part.get_payload(decode=1)
1175                                if not body_text:                       
1176                                        body_text = part.get_payload(decode=0)
1177
1178                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1179                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1180
1181                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1182                                        body_text = self.reflow(body_text, delsp == 'yes')
1183       
1184                                if self.STRIP_SIGNATURE:
1185                                        body_text = self.strip_signature(body_text)
1186
1187                                if self.STRIP_QUOTES:
1188                                        body_text = self.strip_quotes(body_text)
1189
1190                                if self.USE_TEXTWRAP:
1191                                        body_text = self.wrap_text(body_text)
1192
1193                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1194                                #
1195                                charset = part.get_content_charset()
1196                                if not charset:
1197                                        charset = 'iso-8859-15'
1198
1199                                try:
1200                                        ubody_text = unicode(body_text, charset)
1201
1202                                except UnicodeError, detail:
1203                                        ubody_text = unicode(body_text, 'iso-8859-15')
1204
1205                                except LookupError, detail:
1206                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1207
1208                                if self.VERBATIM_FORMAT:
1209                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1210                                else:
1211                                        message_parts.append('%s' %ubody_text)
1212                        else:
1213                                if self.DEBUG:
1214                                        print 'TD:               Filename: %s' % part.get_filename()
1215
1216                                message_parts.append((part.get_filename(), part))
1217
1218                return message_parts
1219               
1220        def unique_attachment_names(self, message_parts):
1221                """
1222                """
1223                renamed_parts = []
1224                attachment_names = set()
1225
1226                for part in message_parts:
1227                       
1228                        ## If not an attachment, leave it alone
1229                        #
1230                        if not isinstance(part, tuple):
1231                                renamed_parts.append(part)
1232                                continue
1233                               
1234                        (filename, part) = part
1235
1236                        ## If no filename, use a default one
1237                        #
1238                        if not filename:
1239                                filename = 'untitled-part'
1240
1241                                # Guess the extension from the content type, use non strict mode
1242                                # some additional non-standard but commonly used MIME types
1243                                # are also recognized
1244                                #
1245                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1246                                if not ext:
1247                                        ext = '.bin'
1248
1249                                filename = '%s%s' % (filename, ext)
1250
1251# We now use the attachment insert function
1252#
1253                        ## Discard relative paths in attachment names
1254                        #
1255                        #filename = filename.replace('\\', '/').replace(':', '/')
1256                        #filename = os.path.basename(filename)
1257                        #
1258                        # We try to normalize the filename to utf-8 NFC if we can.
1259                        # Files uploaded from OS X might be in NFD.
1260                        # Check python version and then try it
1261                        #
1262                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1263                        #       try:
1264                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1265                        #       except TypeError:
1266                        #               pass
1267
1268                        # Make the filename unique for this ticket
1269                        num = 0
1270                        unique_filename = filename
1271                        dummy_filename, ext = os.path.splitext(filename)
1272
1273                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
1274                                num += 1
1275                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1276                               
1277                        if self.DEBUG:
1278                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1279
1280                        attachment_names.add(unique_filename)
1281
1282                        renamed_parts.append((filename, unique_filename, part))
1283       
1284                return renamed_parts
1285                       
1286        def inline_part(self, part):
1287                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1288               
1289                       
1290        def attachment_exists(self, filename):
1291
1292                if self.DEBUG:
1293                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
1294
1295                # We have no valid ticket id
1296                #
1297                if not self.id:
1298                        return False
1299
1300                try:
1301                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1302                        return True
1303                except attachment.ResourceNotFound:
1304                        return False
1305                       
1306        def body_text(self, message_parts):
1307                body_text = []
1308               
1309                for part in message_parts:
1310                        # Plain text part, append it
1311                        if not isinstance(part, tuple):
1312                                body_text.extend(part.strip().splitlines())
1313                                body_text.append("")
1314                                continue
1315                               
1316                        (original, filename, part) = part
1317                        inline = self.inline_part(part)
1318                       
1319                        if part.get_content_maintype() == 'image' and inline:
1320                                body_text.append('[[Image(%s)]]' % filename)
1321                                body_text.append("")
1322                        else:
1323                                body_text.append('[attachment:"%s"]' % filename)
1324                                body_text.append("")
1325                               
1326                body_text = '\r\n'.join(body_text)
1327                return body_text
1328
1329        def notify(self, tkt, new=True, modtime=0):
1330                """
1331                A wrapper for the TRAC notify function. So we can use templates
1332                """
1333                if self.DRY_RUN:
1334                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1335                                return
1336                try:
1337                        # create false {abs_}href properties, to trick Notify()
1338                        #
1339                        if not self.VERSION == 0.11:
1340                                self.env.abs_href = Href(self.get_config('project', 'url'))
1341                                self.env.href = Href(self.get_config('project', 'url'))
1342
1343                        tn = TicketNotifyEmail(self.env)
1344
1345                        if self.notify_template:
1346
1347                                if self.VERSION == 0.11:
1348
1349                                        from trac.web.chrome import Chrome
1350
1351                                        if self.notify_template_update and not new:
1352                                                tn.template_name = self.notify_template_update
1353                                        else:
1354                                                tn.template_name = self.notify_template
1355
1356                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1357                                               
1358                                else:
1359
1360                                        tn.template_name = self.notify_template;
1361
1362                        tn.notify(tkt, new, modtime)
1363
1364                except Exception, e:
1365                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1366
1367        def html_mailto_link(self, subject, body):
1368                """
1369                This function returns a HTML mailto tag with the ticket id and author email address
1370                """
1371                if not self.author:
1372                        author = self.email_addr
1373                else:   
1374                        author = self.author
1375
1376                # use urllib to escape the chars
1377                #
1378                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1379                       urllib.quote(self.email_addr),
1380                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1381                           urllib.quote(self.MAILTO_CC)
1382                           )
1383
1384                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)
1385                return s
1386
1387        def attachments(self, message_parts, update=False):
1388                '''
1389                save any attachments as files in the ticket's directory
1390                '''
1391                if self.DRY_RUN:
1392                        print "DRY_RUN: no attachments saved"
1393                        return ''
1394
1395                count = 0
1396
1397                # Get Maxium attachment size
1398                #
1399                max_size = int(self.get_config('attachment', 'max_size'))
1400                status   = ''
1401               
1402                for part in message_parts:
1403                        # Skip body parts
1404                        if not isinstance(part, tuple):
1405                                continue
1406                               
1407                        (original, filename, part) = part
1408                        #
1409                        # Must be tuneables HvB
1410                        #
1411                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1412                        text = part.get_payload(decode=1)
1413                        if not text:
1414                                text = '(None)'
1415                        fd.write(text)
1416                        fd.close()
1417
1418                        # get the file_size
1419                        #
1420                        stats = os.lstat(path)
1421                        file_size = stats[stat.ST_SIZE]
1422
1423                        # Check if the attachment size is allowed
1424                        #
1425                        if (max_size != -1) and (file_size > max_size):
1426                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1427                                        %(status, original, file_size, max_size)
1428
1429                                os.unlink(path)
1430                                continue
1431                        else:
1432                                count = count + 1
1433                                       
1434                        # Insert the attachment
1435                        #
1436                        fd = open(path, 'rb')
1437                        att = attachment.Attachment(self.env, 'ticket', self.id)
1438
1439                        # This will break the ticket_update system, the body_text is vaporized
1440                        # ;-(
1441                        #
1442                        if not update:
1443                                att.author = self.author
1444                                att.description = self.email_to_unicode('Added by email2trac')
1445
1446                        att.insert(filename, fd, file_size)
1447
1448                        #except  util.TracError, detail:
1449                        #       print detail
1450
1451                        # Remove the created temporary filename
1452                        #
1453                        fd.close()
1454                        os.unlink(path)
1455
1456                # Return how many attachments
1457                #
1458                status = 'This message has %d attachment(s)\n%s' %(count, status)
1459                return status
1460
1461
1462def mkdir_p(dir, mode):
1463        '''do a mkdir -p'''
1464
1465        arr = string.split(dir, '/')
1466        path = ''
1467        for part in arr:
1468                path = '%s/%s' % (path, part)
1469                try:
1470                        stats = os.stat(path)
1471                except OSError:
1472                        os.mkdir(path, mode)
1473
1474def ReadConfig(file, name):
1475        """
1476        Parse the config file
1477        """
1478        if not os.path.isfile(file):
1479                print 'File %s does not exist' %file
1480                sys.exit(1)
1481
1482        config = trac_config.Configuration(file)
1483
1484        # Use given project name else use defaults
1485        #
1486        if name:
1487                sections = config.sections()
1488                if not name in sections:
1489                        print "Not a valid project name: %s" %name
1490                        print "Valid names: %s" %sections
1491                        sys.exit(1)
1492
1493                project =  dict()
1494                for option, value in  config.options(name):
1495                        project[option] = value
1496
1497        else:
1498                # use some trac internals to get the defaults
1499                #
1500                project = config.parser.defaults()
1501
1502        return project
1503
1504
1505if __name__ == '__main__':
1506        # Default config file
1507        #
1508        configfile = '@email2trac_conf@'
1509        project = ''
1510        component = ''
1511        ticket_prefix = 'default'
1512        dry_run = None
1513
1514        ENABLE_SYSLOG = 0
1515
1516
1517        SHORT_OPT = 'chf:np:t:'
1518        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
1519
1520        try:
1521                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1522        except getopt.error,detail:
1523                print __doc__
1524                print detail
1525                sys.exit(1)
1526       
1527        project_name = None
1528        for opt,value in opts:
1529                if opt in [ '-h', '--help']:
1530                        print __doc__
1531                        sys.exit(0)
1532                elif opt in ['-c', '--component']:
1533                        component = value
1534                elif opt in ['-f', '--file']:
1535                        configfile = value
1536                elif opt in ['-n', '--dry-run']:
1537                        dry_run = True
1538                elif opt in ['-p', '--project']:
1539                        project_name = value
1540                elif opt in ['-t', '--ticket_prefix']:
1541                        ticket_prefix = value
1542       
1543        settings = ReadConfig(configfile, project_name)
1544        if not settings.has_key('project'):
1545                print __doc__
1546                print 'No Trac project is defined in the email2trac config file.'
1547                sys.exit(1)
1548       
1549        if component:
1550                settings['component'] = component
1551
1552        # The default prefix for ticket values in email2trac.conf
1553        #
1554        settings['ticket_prefix'] = ticket_prefix
1555        settings['dry_run'] = dry_run
1556       
1557        if settings.has_key('trac_version'):
1558                version = settings['trac_version']
1559        else:
1560                version = trac_default_version
1561
1562
1563        #debug HvB
1564        #print settings
1565
1566        try:
1567                if version == '0.9':
1568                        from trac import attachment
1569                        from trac.env import Environment
1570                        from trac.ticket import Ticket
1571                        from trac.web.href import Href
1572                        from trac import util
1573                        from trac.Notify import TicketNotifyEmail
1574                elif version == '0.10':
1575                        from trac import attachment
1576                        from trac.env import Environment
1577                        from trac.ticket import Ticket
1578                        from trac.web.href import Href
1579                        from trac import util
1580                        #
1581                        # return  util.text.to_unicode(str)
1582                        #
1583                        # see http://projects.edgewall.com/trac/changeset/2799
1584                        from trac.ticket.notification import TicketNotifyEmail
1585                        from trac import config as trac_config
1586                elif version == '0.11':
1587                        from trac import attachment
1588                        from trac.env import Environment
1589                        from trac.ticket import Ticket
1590                        from trac.web.href import Href
1591                        from trac import config as trac_config
1592                        from trac import util
1593
1594
1595                        #
1596                        # return  util.text.to_unicode(str)
1597                        #
1598                        # see http://projects.edgewall.com/trac/changeset/2799
1599                        from trac.ticket.notification import TicketNotifyEmail
1600                else:
1601                        print 'TRAC version %s is not supported' %version
1602                        sys.exit(1)
1603                       
1604                if settings.has_key('enable_syslog'):
1605                        if SYSLOG_AVAILABLE:
1606                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1607
1608
1609                # Must be set before environment is created
1610                #
1611                if settings.has_key('python_egg_cache'):
1612                        python_egg_cache = str(settings['python_egg_cache'])
1613                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1614
1615                env = Environment(settings['project'], create=0)
1616                tktparser = TicketEmailParser(env, settings, float(version))
1617                tktparser.parse(sys.stdin)
1618
1619        # Catch all errors ans log to SYSLOG if we have enabled this
1620        # else stdout
1621        #
1622        except Exception, error:
1623                if ENABLE_SYSLOG:
1624                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1625
1626                        etype, evalue, etb = sys.exc_info()
1627                        for e in traceback.format_exception(etype, evalue, etb):
1628                                syslog.syslog(e)
1629
1630                        syslog.closelog()
1631                else:
1632                        traceback.print_exc()
1633
1634                if m:
1635                        tktparser.save_email_for_debug(m, True)
1636
1637                sys.exit(1)
1638# EOB
Note: See TracBrowser for help on using the repository browser.