source: trunk/email2trac.py.in @ 335

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

Some more layout changes

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