source: trunk/email2trac.py.in @ 318

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

Fixed an segv in run_email2trac.c with the new python_egg cache setting. see #174

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