source: trunk/email2trac.py.in @ 336

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

removed obsolete function

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