source: trunk/email2trac.py.in @ 290

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

email2trac.py.in:

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