source: trunk/email2trac.py.in @ 280

Last change on this file since 280 was 278, checked in by bas, 15 years ago

email2trac.py.in:

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