source: trunk/email2trac.py.in @ 257

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

email2trac.py.in:

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