source: trunk/email2trac.py.in @ 334

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

changed some debugs to verbose messages

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