source: trunk/email2trac.py.in @ 319

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

When there a problems with saving attachments. Show it as comment in the ticket, closes #165

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 42.7 KB
Line 
1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
22"""
23email2trac.py -- Email tickets to Trac.
24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
38 * See https://subtrac.sara.nl/oss/email2trac/
39
40 * Create an config file:
41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
44    trac_version : 0.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 319 2010-02-22 14:13:34Z 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                error_with_attachments =  self.attachments(message_parts)
930
931                if error_with_attachments:
932                        changed = True
933                        comment = '%s\n%s\n' %(comment, error_with_attachments)
934
935                if changed:
936                        if self.DRY_RUN:
937                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
938                        else:
939                                tkt.save_changes(self.author, comment)
940                                #print tkt.get_changelog(self.db, when)
941
942                if self.notification and not spam:
943                        self.notify(tkt, True)
944
945
946        def blog(self, id):
947                """
948                The blog create/update function
949                """
950                # import the modules
951                #
952                from tracfullblog.core import FullBlogCore
953                from tracfullblog.model import BlogPost, BlogComment
954                from trac.test import Mock, MockPerm
955
956                # instantiate blog core
957                blog = FullBlogCore(self.env)
958                req = Mock(authname='anonymous', perm=MockPerm(), args={})
959
960                if id:
961
962                        # update blog
963                        #
964                        comment = BlogComment(self.env, id)
965                        comment.author = self.author
966
967                        message_parts = self.get_message_parts(m)
968                        comment.comment = self.body_text(message_parts)
969
970                        blog.create_comment(req, comment)
971
972                else:
973                        # create blog
974                        #
975                        import time
976                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
977
978                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
979                       
980                        post.author = self.author
981                        post.title = self.email_to_unicode(m['Subject'])
982
983                        message_parts = self.get_message_parts(m)
984                        post.body = self.body_text(message_parts)
985                       
986                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
987
988        def save_message_for_debug(self):
989                """
990                Save the messages. So we can use it for debug purposes
991                """
992               
993
994        def parse(self, fp):
995                global m
996
997                m = email.message_from_file(fp)
998               
999                if not m:
1000                        if self.DEBUG:
1001                                print "TD: This is not a valid email message format"
1002                        return
1003                       
1004                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1005                try:
1006                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1007                except AttributeError, detail:
1008                        pass
1009
1010                if self.DEBUG > 1:        # save the entire e-mail message text
1011                        self.save_email_for_debug(m, True)
1012
1013                self.db = self.env.get_db_cnx()
1014                self.get_sender_info(m)
1015
1016                if not self.email_header_acl('white_list', self.email_addr, True):
1017                        if self.DEBUG > 1 :
1018                                print 'Message rejected : %s not in white list' %(self.email_addr)
1019                        return False
1020
1021                if self.email_header_acl('black_list', self.email_addr, False):
1022                        if self.DEBUG > 1 :
1023                                print 'Message rejected : %s in black list' %(self.email_addr)
1024                        return False
1025
1026                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1027                        if self.DEBUG > 1 :
1028                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1029                        return False
1030
1031                # If drop the message
1032                #
1033                if self.spam(m) == 'drop':
1034                        return False
1035
1036                elif self.spam(m) == 'spam':
1037                        spam_msg = True
1038                else:
1039                        spam_msg = False
1040
1041                if self.get_config('notification', 'smtp_enabled') in ['true']:
1042                        self.notification = 1
1043                else:
1044                        self.notification = 0
1045
1046
1047                # Check if  FullBlogPlugin is installed
1048                #
1049                blog_enabled = None
1050                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1051                        blog_enabled = True
1052                       
1053                if not m['Subject']:
1054                        return False
1055                else:
1056                        subject  = self.email_to_unicode(m['Subject'])         
1057
1058                #
1059                # [hic] #1529: Re: LRZ
1060                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1061                #
1062                TICKET_RE = re.compile(r"""
1063                        (?P<blog>blog:(?P<blog_id>\w*))
1064                        |(?P<new_fields>[#][?].*)
1065                        |(?P<reply>[#][\d]+:)
1066                        |(?P<reply_fields>[#][\d]+\?.*?:)
1067                        """, re.VERBOSE)
1068
1069                # Find out if this is a ticket or a blog
1070                #
1071                result =  TICKET_RE.search(subject)
1072
1073                if result:
1074                        if result.group('blog'):
1075                                if blog_enabled:
1076                                        self.blog(result.group('blog_id'))
1077                                else:
1078                                        if self.DEBUG:
1079                                                print "Fullblog plugin is not installed"
1080                                                return
1081
1082                        # update ticket + fields
1083                        #
1084                        if result.group('reply_fields') and self.TICKET_UPDATE:
1085                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
1086
1087                        # Update ticket
1088                        #
1089                        elif result.group('reply') and self.TICKET_UPDATE:
1090                                self.ticket_update(m, result.group('reply'), spam_msg)
1091
1092                        # New ticket + fields
1093                        #
1094                        elif result.group('new_fields'):
1095                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1096
1097                # Create ticket
1098                #
1099                else:
1100                        self.new_ticket(m, subject, spam_msg)
1101
1102        def strip_signature(self, text):
1103                """
1104                Strip signature from message, inspired by Mailman software
1105                """
1106                body = []
1107                for line in text.splitlines():
1108                        if line == '-- ':
1109                                break
1110                        body.append(line)
1111
1112                return ('\n'.join(body))
1113
1114        def reflow(self, text, delsp = 0):
1115                """
1116                Reflow the message based on the format="flowed" specification (RFC 3676)
1117                """
1118                flowedlines = []
1119                quotelevel = 0
1120                prevflowed = 0
1121
1122                for line in text.splitlines():
1123                        from re import match
1124                       
1125                        # Figure out the quote level and the content of the current line
1126                        m = match('(>*)( ?)(.*)', line)
1127                        linequotelevel = len(m.group(1))
1128                        line = m.group(3)
1129
1130                        # Determine whether this line is flowed
1131                        if line and line != '-- ' and line[-1] == ' ':
1132                                flowed = 1
1133                        else:
1134                                flowed = 0
1135
1136                        if flowed and delsp and line and line[-1] == ' ':
1137                                line = line[:-1]
1138
1139                        # If the previous line is flowed, append this line to it
1140                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1141                                flowedlines[-1] += line
1142                        # Otherwise, start a new line
1143                        else:
1144                                flowedlines.append('>' * linequotelevel + line)
1145
1146                        prevflowed = flowed
1147                       
1148
1149                return '\n'.join(flowedlines)
1150
1151        def strip_quotes(self, text):
1152                """
1153                Strip quotes from message by Nicolas Mendoza
1154                """
1155                body = []
1156                for line in text.splitlines():
1157                        if line.startswith(self.EMAIL_QUOTE):
1158                                continue
1159                        body.append(line)
1160
1161                return ('\n'.join(body))
1162
1163        def inline_properties(self, text):
1164                """
1165                Parse text if we use inline keywords to set ticket fields
1166                """
1167                if self.DEBUG:
1168                        print 'TD: inline_properties function'
1169
1170                properties = dict()
1171                body = list()
1172
1173                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1174
1175                for line in text.splitlines():
1176                        match = INLINE_EXP.match(line)
1177                        if match:
1178                                keyword, value = match.groups()
1179                                self.properties[keyword] = value.strip()
1180                                if self.DEBUG:
1181                                        print "TD: inline properties: %s : %s" %(keyword,value)
1182                        else:
1183                                body.append(line)
1184                               
1185                return '\n'.join(body)
1186
1187
1188        def wrap_text(self, text, replace_whitespace = False):
1189                """
1190                Will break a lines longer then given length into several small
1191                lines of size given length
1192                """
1193                import textwrap
1194
1195                LINESEPARATOR = '\n'
1196                reformat = ''
1197
1198                for s in text.split(LINESEPARATOR):
1199                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1200                        if tmp:
1201                                reformat = '%s\n%s' %(reformat,tmp)
1202                        else:
1203                                reformat = '%s\n' %reformat
1204
1205                return reformat
1206
1207                # Python2.4 and higher
1208                #
1209                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1210                #
1211
1212
1213        def get_message_parts(self, msg):
1214                """
1215                parses the email message and returns a list of body parts and attachments
1216                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1217                """
1218                if self.VERBOSE:
1219                        print "VB: get_message_parts()"
1220
1221                message_parts = list()
1222       
1223                ALTERNATIVE_MULTIPART = False
1224
1225                for part in msg.walk():
1226                        if self.DEBUG:
1227                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1228                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1229
1230                        ## Check content type
1231                        #
1232                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1233
1234                                if self.DEBUG:
1235                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1236
1237                                continue
1238
1239                        ## Catch some mulitpart execptions
1240                        #
1241                        if part.get_content_type() == 'multipart/alternative':
1242                                ALTERNATIVE_MULTIPART = True
1243                                continue
1244
1245                        ## Skip multipart containers
1246                        #
1247                        if part.get_content_maintype() == 'multipart':
1248                                if self.DEBUG:
1249                                        print "TD: Skipping multipart container"
1250                                continue
1251                       
1252                        ## 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"
1253                        #
1254                        inline = self.inline_part(part)
1255
1256                        ## Drop HTML message
1257                        #
1258                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1259                                if part.get_content_type() == 'text/html':
1260                                        if self.DEBUG:
1261                                                print "TD: Skipping alternative HTML message"
1262
1263                                        ALTERNATIVE_MULTIPART = False
1264                                        continue
1265
1266                        ## Inline text parts are where the body is
1267                        #
1268                        if part.get_content_type() == 'text/plain' and inline:
1269                                if self.DEBUG:
1270                                        print 'TD:               Inline body part'
1271
1272                                # Try to decode, if fails then do not decode
1273                                #
1274                                body_text = part.get_payload(decode=1)
1275                                if not body_text:                       
1276                                        body_text = part.get_payload(decode=0)
1277
1278                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1279                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1280
1281                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1282                                        body_text = self.reflow(body_text, delsp == 'yes')
1283       
1284                                if self.STRIP_SIGNATURE:
1285                                        body_text = self.strip_signature(body_text)
1286
1287                                if self.STRIP_QUOTES:
1288                                        body_text = self.strip_quotes(body_text)
1289
1290                                if self.INLINE_PROPERTIES:
1291                                        body_text = self.inline_properties(body_text)
1292
1293                                if self.USE_TEXTWRAP:
1294                                        body_text = self.wrap_text(body_text)
1295
1296                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1297                                #
1298                                charset = part.get_content_charset()
1299                                if not charset:
1300                                        charset = 'iso-8859-15'
1301
1302                                try:
1303                                        ubody_text = unicode(body_text, charset)
1304
1305                                except UnicodeError, detail:
1306                                        ubody_text = unicode(body_text, 'iso-8859-15')
1307
1308                                except LookupError, detail:
1309                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1310
1311                                if self.VERBATIM_FORMAT:
1312                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1313                                else:
1314                                        message_parts.append('%s' %ubody_text)
1315                        else:
1316                                if self.DEBUG:
1317                                        try:
1318                                                print 'TD:               Filename: %s' % part.get_filename()
1319                                        except UnicodeEncodeError, detail:
1320                                                print 'TD:               Filename: Can not be printed due to non-ascci characters'
1321
1322                                ## Convert 7-bit filename to 8 bits value
1323                                #
1324                                filename = self.email_to_unicode(part.get_filename())
1325                                message_parts.append((filename, part))
1326
1327                return message_parts
1328               
1329        def unique_attachment_names(self, message_parts):
1330                """
1331                """
1332                renamed_parts = []
1333                attachment_names = set()
1334
1335                for part in message_parts:
1336                       
1337                        ## If not an attachment, leave it alone
1338                        #
1339                        if not isinstance(part, tuple):
1340                                renamed_parts.append(part)
1341                                continue
1342                               
1343                        (filename, part) = part
1344
1345                        ## If no filename, use a default one
1346                        #
1347                        if not filename:
1348                                filename = 'untitled-part'
1349
1350                                # Guess the extension from the content type, use non strict mode
1351                                # some additional non-standard but commonly used MIME types
1352                                # are also recognized
1353                                #
1354                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1355                                if not ext:
1356                                        ext = '.bin'
1357
1358                                filename = '%s%s' % (filename, ext)
1359
1360# We now use the attachment insert function
1361#
1362                        ## Discard relative paths in attachment names
1363                        #
1364                        #filename = filename.replace('\\', '/').replace(':', '/')
1365                        #filename = os.path.basename(filename)
1366                        #
1367                        # We try to normalize the filename to utf-8 NFC if we can.
1368                        # Files uploaded from OS X might be in NFD.
1369                        # Check python version and then try it
1370                        #
1371                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1372                        #       try:
1373                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1374                        #       except TypeError:
1375                        #               pass
1376
1377                        # Make the filename unique for this ticket
1378                        num = 0
1379                        unique_filename = filename
1380                        dummy_filename, ext = os.path.splitext(filename)
1381
1382                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
1383                                num += 1
1384                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1385                               
1386                        if self.DEBUG:
1387                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1388
1389                        attachment_names.add(unique_filename)
1390
1391                        renamed_parts.append((filename, unique_filename, part))
1392       
1393                return renamed_parts
1394                       
1395        def inline_part(self, part):
1396                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1397               
1398                       
1399        def attachment_exists(self, filename):
1400
1401                if self.DEBUG:
1402                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
1403
1404                # We have no valid ticket id
1405                #
1406                if not self.id:
1407                        return False
1408
1409                try:
1410                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1411                        return True
1412                except attachment.ResourceNotFound:
1413                        return False
1414                       
1415        def body_text(self, message_parts):
1416                body_text = []
1417               
1418                for part in message_parts:
1419                        # Plain text part, append it
1420                        if not isinstance(part, tuple):
1421                                body_text.extend(part.strip().splitlines())
1422                                body_text.append("")
1423                                continue
1424                               
1425                        (original, filename, part) = part
1426                        inline = self.inline_part(part)
1427                       
1428                        if part.get_content_maintype() == 'image' and inline:
1429                                body_text.append('[[Image(%s)]]' % filename)
1430                                body_text.append("")
1431                        else:
1432                                body_text.append('[attachment:"%s"]' % filename)
1433                                body_text.append("")
1434                               
1435                body_text = '\r\n'.join(body_text)
1436                return body_text
1437
1438        def notify(self, tkt, new=True, modtime=0):
1439                """
1440                A wrapper for the TRAC notify function. So we can use templates
1441                """
1442                if self.DRY_RUN:
1443                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1444                                return
1445                try:
1446                        # create false {abs_}href properties, to trick Notify()
1447                        #
1448                        if not self.VERSION == 0.11:
1449                                self.env.abs_href = Href(self.get_config('project', 'url'))
1450                                self.env.href = Href(self.get_config('project', 'url'))
1451
1452                        tn = TicketNotifyEmail(self.env)
1453
1454                        if self.notify_template:
1455
1456                                if self.VERSION == 0.11:
1457
1458                                        from trac.web.chrome import Chrome
1459
1460                                        if self.notify_template_update and not new:
1461                                                tn.template_name = self.notify_template_update
1462                                        else:
1463                                                tn.template_name = self.notify_template
1464
1465                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1466                                               
1467                                else:
1468
1469                                        tn.template_name = self.notify_template;
1470
1471                        tn.notify(tkt, new, modtime)
1472
1473                except Exception, e:
1474                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1475
1476        def html_mailto_link(self, subject, body):
1477                """
1478                This function returns a HTML mailto tag with the ticket id and author email address
1479                """
1480                if not self.author:
1481                        author = self.email_addr
1482                else:   
1483                        author = self.author
1484
1485                # use urllib to escape the chars
1486                #
1487                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1488                       urllib.quote(self.email_addr),
1489                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1490                           urllib.quote(self.MAILTO_CC)
1491                           )
1492
1493                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)
1494                return s
1495
1496        def attachments(self, message_parts, update=False):
1497                '''
1498                save any attachments as files in the ticket's directory
1499                '''
1500                if self.DRY_RUN:
1501                        print "DRY_RUN: no attachments saved"
1502                        return ''
1503
1504                count = 0
1505
1506                # Get Maxium attachment size
1507                #
1508                max_size = int(self.get_config('attachment', 'max_size'))
1509                status   = None
1510               
1511                for part in message_parts:
1512                        # Skip body parts
1513                        if not isinstance(part, tuple):
1514                                continue
1515                               
1516                        (original, filename, part) = part
1517                        #
1518                        # Must be tuneables HvB
1519                        #
1520                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1521                        text = part.get_payload(decode=1)
1522                        if not text:
1523                                text = '(None)'
1524                        fd.write(text)
1525                        fd.close()
1526
1527                        # get the file_size
1528                        #
1529                        stats = os.lstat(path)
1530                        file_size = stats[stat.ST_SIZE]
1531
1532                        # Check if the attachment size is allowed
1533                        #
1534                        if (max_size != -1) and (file_size > max_size):
1535                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1536                                        %(status, original, file_size, max_size)
1537
1538                                os.unlink(path)
1539                                continue
1540                        else:
1541                                count = count + 1
1542                                       
1543                        # Insert the attachment
1544                        #
1545                        fd = open(path, 'rb')
1546                        att = attachment.Attachment(self.env, 'ticket', self.id)
1547
1548                        # This will break the ticket_update system, the body_text is vaporized
1549                        # ;-(
1550                        #
1551                        if not update:
1552                                att.author = self.author
1553                                att.description = self.email_to_unicode('Added by email2trac')
1554
1555                        att.insert(filename, fd, file_size)
1556
1557                        #except  util.TracError, detail:
1558                        #       print detail
1559
1560                        # Remove the created temporary filename
1561                        #
1562                        fd.close()
1563                        os.unlink(path)
1564
1565                ## return error
1566                #
1567                return status
1568
1569
1570def mkdir_p(dir, mode):
1571        '''do a mkdir -p'''
1572
1573        arr = string.split(dir, '/')
1574        path = ''
1575        for part in arr:
1576                path = '%s/%s' % (path, part)
1577                try:
1578                        stats = os.stat(path)
1579                except OSError:
1580                        os.mkdir(path, mode)
1581
1582def ReadConfig(file, name):
1583        """
1584        Parse the config file
1585        """
1586        if not os.path.isfile(file):
1587                print 'File %s does not exist' %file
1588                sys.exit(1)
1589
1590        config = trac_config.Configuration(file)
1591
1592        # Use given project name else use defaults
1593        #
1594        if name:
1595                sections = config.sections()
1596                if not name in sections:
1597                        print "Not a valid project name: %s" %name
1598                        print "Valid names: %s" %sections
1599                        sys.exit(1)
1600
1601                project =  dict()
1602                for option, value in  config.options(name):
1603                        project[option] = value
1604
1605        else:
1606                # use some trac internals to get the defaults
1607                #
1608                project = config.parser.defaults()
1609
1610        return project
1611
1612
1613if __name__ == '__main__':
1614        # Default config file
1615        #
1616        configfile = '@email2trac_conf@'
1617        project = ''
1618        component = ''
1619        ticket_prefix = 'default'
1620        dry_run = None
1621        verbose = None
1622
1623        ENABLE_SYSLOG = 0
1624
1625
1626        SHORT_OPT = 'chf:np:t:v'
1627        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
1628
1629        try:
1630                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1631        except getopt.error,detail:
1632                print __doc__
1633                print detail
1634                sys.exit(1)
1635       
1636        project_name = None
1637        for opt,value in opts:
1638                if opt in [ '-h', '--help']:
1639                        print __doc__
1640                        sys.exit(0)
1641                elif opt in ['-c', '--component']:
1642                        component = value
1643                elif opt in ['-f', '--file']:
1644                        configfile = value
1645                elif opt in ['-n', '--dry-run']:
1646                        dry_run = True
1647                elif opt in ['-p', '--project']:
1648                        project_name = value
1649                elif opt in ['-t', '--ticket_prefix']:
1650                        ticket_prefix = value
1651                elif opt in ['-v', '--version']:
1652                        verbose = True
1653       
1654        settings = ReadConfig(configfile, project_name)
1655        if not settings.has_key('project'):
1656                print __doc__
1657                print 'No Trac project is defined in the email2trac config file.'
1658                sys.exit(1)
1659       
1660        if component:
1661                settings['component'] = component
1662
1663        # The default prefix for ticket values in email2trac.conf
1664        #
1665        settings['ticket_prefix'] = ticket_prefix
1666        settings['dry_run'] = dry_run
1667        settings['verbose'] = verbose
1668       
1669        if settings.has_key('trac_version'):
1670                version = settings['trac_version']
1671        else:
1672                version = trac_default_version
1673
1674
1675        #debug HvB
1676        #print settings
1677
1678        try:
1679                if version == '0.9':
1680                        from trac import attachment
1681                        from trac.env import Environment
1682                        from trac.ticket import Ticket
1683                        from trac.web.href import Href
1684                        from trac import util
1685                        from trac.Notify import TicketNotifyEmail
1686                elif version == '0.10':
1687                        from trac import attachment
1688                        from trac.env import Environment
1689                        from trac.ticket import Ticket
1690                        from trac.web.href import Href
1691                        from trac import util
1692                        #
1693                        # return  util.text.to_unicode(str)
1694                        #
1695                        # see http://projects.edgewall.com/trac/changeset/2799
1696                        from trac.ticket.notification import TicketNotifyEmail
1697                        from trac import config as trac_config
1698                elif version == '0.11':
1699                        from trac import attachment
1700                        from trac.env import Environment
1701                        from trac.ticket import Ticket
1702                        from trac.web.href import Href
1703                        from trac import config as trac_config
1704                        from trac import util
1705
1706
1707                        #
1708                        # return  util.text.to_unicode(str)
1709                        #
1710                        # see http://projects.edgewall.com/trac/changeset/2799
1711                        from trac.ticket.notification import TicketNotifyEmail
1712                else:
1713                        print 'TRAC version %s is not supported' %version
1714                        sys.exit(1)
1715                       
1716                if settings.has_key('enable_syslog'):
1717                        if SYSLOG_AVAILABLE:
1718                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1719
1720
1721                # Must be set before environment is created
1722                #
1723                if settings.has_key('python_egg_cache'):
1724                        python_egg_cache = str(settings['python_egg_cache'])
1725                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1726
1727                env = Environment(settings['project'], create=0)
1728                tktparser = TicketEmailParser(env, settings, float(version))
1729                tktparser.parse(sys.stdin)
1730
1731        # Catch all errors ans log to SYSLOG if we have enabled this
1732        # else stdout
1733        #
1734        except Exception, error:
1735                if ENABLE_SYSLOG:
1736                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1737
1738                        etype, evalue, etb = sys.exc_info()
1739                        for e in traceback.format_exception(etype, evalue, etb):
1740                                syslog.syslog(e)
1741
1742                        syslog.closelog()
1743                else:
1744                        traceback.print_exc()
1745
1746                if m:
1747                        tktparser.save_email_for_debug(m, True)
1748
1749                sys.exit(1)
1750# EOB
Note: See TracBrowser for help on using the repository browser.