source: trunk/email2trac.py.in @ 284

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

email2trac.py.in:

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