source: trunk/email2trac.py.in @ 330

Last change on this file since 330 was 330, checked in by bas, 12 years ago

email2trac.py.in:

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